fix: Switch Apple Reminders to CalDAV, restore date picker, add goal scope
- Replace broken CloudKit/pyicloud Apple Reminders integration with CalDAV-based approach (getAppleReminderLists, fetchTasks from apple-calendar.ts) in tasks/lists, tasks/import, and tasks/sync routes - Restore calendar date picker icon in header for jump-to-date navigation - Add goalScope field to User model with migration - Force-dynamic Google OAuth routes to fix redirect issues - Update Google Tasks client and sync logic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7f76774258
commit
dd8d471a1d
2
prisma/migrations/20260220_add_goal_scope/migration.sql
Normal file
2
prisma/migrations/20260220_add_goal_scope/migration.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "goalScope" TEXT NOT NULL DEFAULT 'week';
|
||||||
@ -46,6 +46,7 @@ model User {
|
|||||||
goalFontFamily String? @default("Inter")
|
goalFontFamily String? @default("Inter")
|
||||||
goalFontSize String? @default("0.9rem")
|
goalFontSize String? @default("0.9rem")
|
||||||
goalFontWeight String? @default("500")
|
goalFontWeight String? @default("500")
|
||||||
|
goalScope String @default("week") // "week" | "day"
|
||||||
headlineFont String @default("Inter")
|
headlineFont String @default("Inter")
|
||||||
headlineFontSize String? @default("1.25rem")
|
headlineFontSize String? @default("1.25rem")
|
||||||
headlineFontWeight String? @default("900")
|
headlineFontWeight String? @default("900")
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { authOptions } from "@/lib/auth";
|
|||||||
import { google } from 'googleapis';
|
import { google } from 'googleapis';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
// Google Calendar OAuth callback endpoint
|
// Google Calendar OAuth callback endpoint
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { getServerSession } from 'next-auth';
|
|||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
import { google } from 'googleapis';
|
import { google } from 'googleapis';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
// Initiate Google Calendar OAuth flow
|
// Initiate Google Calendar OAuth flow
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { getServerSession } from 'next-auth';
|
|||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { createGoogleClient, fetchGoogleTasks, fetchGoogleTaskLists } from '@/lib/google-tasks';
|
import { createGoogleClient, fetchGoogleTasks, fetchGoogleTaskLists } from '@/lib/google-tasks';
|
||||||
import { fetchReminders as fetchAppleReminders } from '@/lib/apple-reminders';
|
import { fetchTasks as fetchAppleTasks } from '@/lib/apple-calendar';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
@ -90,12 +90,17 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if (provider === 'apple') {
|
} else if (provider === 'apple') {
|
||||||
const connection = await prisma.calendarConnection.findFirst({
|
// Prefer 'apple' (CalDAV) connection, fall back to 'apple-reminders'
|
||||||
|
const appleConn = await prisma.calendarConnection.findFirst({
|
||||||
|
where: { userId: user.id, provider: 'apple' }
|
||||||
|
});
|
||||||
|
const remindersConn = await prisma.calendarConnection.findFirst({
|
||||||
where: { userId: user.id, provider: 'apple-reminders' }
|
where: { userId: user.id, provider: 'apple-reminders' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const connection = appleConn || remindersConn;
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return NextResponse.json({ error: 'Apple Reminders not connected' }, { status: 400 });
|
return NextResponse.json({ error: 'Apple Calendar not connected. Please connect Apple Calendar in Settings first.' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const colonIdx = connection.accessToken.indexOf(':');
|
const colonIdx = connection.accessToken.indexOf(':');
|
||||||
@ -106,16 +111,17 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
for (const sourceList of lists) {
|
for (const sourceList of lists) {
|
||||||
try {
|
try {
|
||||||
console.log(`[IMPORT] Fetching reminders from list: ${sourceList.title} (guid: ${sourceList.id})`);
|
// sourceList.id is a CalDAV URL (from getAppleReminderLists)
|
||||||
const reminders = await fetchAppleReminders(email, password, sourceList.id);
|
console.log(`[IMPORT] Fetching reminders from list: ${sourceList.title} (url: ${sourceList.id})`);
|
||||||
console.log(`[IMPORT] Fetched ${reminders.length} reminders from "${sourceList.title}"`);
|
const tasks = await fetchAppleTasks(email, password, sourceList.id);
|
||||||
importedTasks.push(...reminders.map(r => ({
|
console.log(`[IMPORT] Fetched ${tasks.length} tasks from "${sourceList.title}"`);
|
||||||
title: r.title,
|
importedTasks.push(...tasks.map(t => ({
|
||||||
description: r.description || '',
|
title: t.title,
|
||||||
externalId: r.guid,
|
description: t.description || '',
|
||||||
|
externalId: t.id,
|
||||||
externalListId: sourceList.id,
|
externalListId: sourceList.id,
|
||||||
dueDate: r.dueDate || null,
|
dueDate: t.endDate ? new Date(t.endDate) : null,
|
||||||
status: r.isCompleted ? 'completed' : 'NEEDS-ACTION',
|
status: 'NEEDS-ACTION', // fetchTasks already filters out completed
|
||||||
sourceListTitle: sourceList.title,
|
sourceListTitle: sourceList.title,
|
||||||
})));
|
})));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { getServerSession } from 'next-auth';
|
|||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { createGoogleClient, fetchGoogleTaskLists } from '@/lib/google-tasks';
|
import { createGoogleClient, fetchGoogleTaskLists } from '@/lib/google-tasks';
|
||||||
import { fetchReminderLists } from '@/lib/apple-reminders';
|
import { getAppleReminderLists } from '@/lib/apple-calendar';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
@ -44,13 +44,18 @@ export async function GET(req: NextRequest) {
|
|||||||
return NextResponse.json({ lists: lists.map(l => ({ id: l.id, title: l.title })) });
|
return NextResponse.json({ lists: lists.map(l => ({ id: l.id, title: l.title })) });
|
||||||
|
|
||||||
} else if (provider === 'apple') {
|
} else if (provider === 'apple') {
|
||||||
// Look for the apple-reminders connection
|
// Try CalDAV connections: prefer 'apple' (CalDAV with app-specific password),
|
||||||
const connection = await prisma.calendarConnection.findFirst({
|
// fall back to 'apple-reminders' credentials
|
||||||
|
const appleConn = await prisma.calendarConnection.findFirst({
|
||||||
|
where: { userId: user.id, provider: 'apple' }
|
||||||
|
});
|
||||||
|
const remindersConn = await prisma.calendarConnection.findFirst({
|
||||||
where: { userId: user.id, provider: 'apple-reminders' }
|
where: { userId: user.id, provider: 'apple-reminders' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const connection = appleConn || remindersConn;
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return NextResponse.json({ error: 'Apple Reminders not connected. Please connect Apple Reminders in Settings.' }, { status: 400 });
|
return NextResponse.json({ error: 'Apple Calendar not connected. Please connect Apple Calendar in Settings first (requires app-specific password).' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const colonIdx = connection.accessToken.indexOf(':');
|
const colonIdx = connection.accessToken.indexOf(':');
|
||||||
@ -58,13 +63,13 @@ export async function GET(req: NextRequest) {
|
|||||||
const password = connection.accessToken.slice(colonIdx + 1);
|
const password = connection.accessToken.slice(colonIdx + 1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reminderLists = await fetchReminderLists(email, password);
|
const reminderLists = await getAppleReminderLists(email, password);
|
||||||
return NextResponse.json({ lists: reminderLists.map(l => ({ id: l.guid, title: l.title })) });
|
return NextResponse.json({ lists: reminderLists.map(l => ({ id: l.id, title: l.title })) });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[TASKS/LISTS] Failed to fetch reminder lists:', error.message);
|
console.error('[TASKS/LISTS] Failed to fetch reminder lists via CalDAV:', error.message);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
error: error.message || 'Failed to fetch Apple Reminder lists.',
|
error: error.message || 'Failed to fetch Apple Reminder lists via CalDAV.',
|
||||||
needsReconnect: error.message?.includes('2FA') || error.message?.includes('expired') || error.message?.includes('reconnect')
|
needsReconnect: error.message?.includes('auth') || error.message?.includes('credentials') || error.message?.includes('401')
|
||||||
}, { status: 401 });
|
}, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export async function PATCH(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { taskId, completed, title, action } = body;
|
const { taskId, completed, title, action, scheduledDate, notes } = body;
|
||||||
|
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
return NextResponse.json({ error: 'Task ID required' }, { status: 400 });
|
return NextResponse.json({ error: 'Task ID required' }, { status: 400 });
|
||||||
@ -47,25 +47,26 @@ export async function PATCH(req: NextRequest) {
|
|||||||
|
|
||||||
if (action === 'delete') {
|
if (action === 'delete') {
|
||||||
await deleteGoogleTask(client, task.externalListId, task.externalId);
|
await deleteGoogleTask(client, task.externalListId, task.externalId);
|
||||||
} else if (title !== undefined && completed !== undefined) {
|
} else {
|
||||||
await updateGoogleTask(client, task.externalListId, task.externalId, {
|
const updates: { title?: string; notes?: string; status?: 'needsAction' | 'completed'; due?: string | null } = {};
|
||||||
title,
|
if (title !== undefined) updates.title = title;
|
||||||
status: completed ? 'completed' : 'needsAction',
|
if (notes !== undefined) updates.notes = notes;
|
||||||
});
|
if (completed !== undefined) updates.status = completed ? 'completed' : 'needsAction';
|
||||||
} else if (title !== undefined) {
|
if (scheduledDate !== undefined) {
|
||||||
await updateGoogleTask(client, task.externalListId, task.externalId, { title });
|
// Google Tasks expects RFC 3339 date (YYYY-MM-DDT00:00:00.000Z)
|
||||||
} else if (completed !== undefined) {
|
updates.due = scheduledDate ? new Date(scheduledDate).toISOString() : null;
|
||||||
await updateGoogleTaskStatus(
|
}
|
||||||
client,
|
if (Object.keys(updates).length > 0) {
|
||||||
task.externalListId,
|
await updateGoogleTask(client, task.externalListId, task.externalId, updates);
|
||||||
task.externalId,
|
}
|
||||||
completed ? 'completed' : 'needsAction'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (task.externalProvider === 'apple' && task.externalListId) {
|
else if (task.externalProvider === 'apple' && task.externalListId) {
|
||||||
|
// Prefer 'apple' (CalDAV) connection, fall back to 'apple-reminders'
|
||||||
const connection = await prisma.calendarConnection.findFirst({
|
const connection = await prisma.calendarConnection.findFirst({
|
||||||
|
where: { userId: task.userId, provider: 'apple' }
|
||||||
|
}) || await prisma.calendarConnection.findFirst({
|
||||||
where: { userId: task.userId, provider: 'apple-reminders' }
|
where: { userId: task.userId, provider: 'apple-reminders' }
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -99,6 +100,7 @@ export async function PATCH(req: NextRequest) {
|
|||||||
const updateData: any = {};
|
const updateData: any = {};
|
||||||
if (completed !== undefined) updateData.completed = completed;
|
if (completed !== undefined) updateData.completed = completed;
|
||||||
if (title !== undefined) updateData.title = title;
|
if (title !== undefined) updateData.title = title;
|
||||||
|
if (scheduledDate !== undefined) updateData.scheduledDate = scheduledDate ? new Date(scheduledDate) : null;
|
||||||
|
|
||||||
if (Object.keys(updateData).length > 0) {
|
if (Object.keys(updateData).length > 0) {
|
||||||
const updatedTask = await prisma.task.update({
|
const updatedTask = await prisma.task.update({
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export async function GET(request: NextRequest) {
|
|||||||
goalFontFamily: true,
|
goalFontFamily: true,
|
||||||
goalFontSize: true,
|
goalFontSize: true,
|
||||||
goalFontWeight: true,
|
goalFontWeight: true,
|
||||||
|
goalScope: true,
|
||||||
headlineFont: true,
|
headlineFont: true,
|
||||||
headlineFontSize: true,
|
headlineFontSize: true,
|
||||||
headlineFontWeight: true,
|
headlineFontWeight: true,
|
||||||
@ -98,7 +99,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
fontWeight, weekendColorSat, weekendColorSun,
|
fontWeight, weekendColorSat, weekendColorSun,
|
||||||
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
||||||
pastDayColor, goalFallbackType, goalDefaultSentence,
|
pastDayColor, goalFallbackType, goalDefaultSentence,
|
||||||
goalFontFamily, goalFontSize, goalFontWeight
|
goalFontFamily, goalFontSize, goalFontWeight, goalScope
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
@ -152,6 +153,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
...(goalFontFamily !== undefined && { goalFontFamily }),
|
...(goalFontFamily !== undefined && { goalFontFamily }),
|
||||||
...(goalFontSize !== undefined && { goalFontSize }),
|
...(goalFontSize !== undefined && { goalFontSize }),
|
||||||
...(goalFontWeight !== undefined && { goalFontWeight }),
|
...(goalFontWeight !== undefined && { goalFontWeight }),
|
||||||
|
...(goalScope !== undefined && { goalScope }),
|
||||||
};
|
};
|
||||||
if (password && password.trim() !== "") {
|
if (password && password.trim() !== "") {
|
||||||
updateData.passwordHash = await bcrypt.hash(password, 10);
|
updateData.passwordHash = await bcrypt.hash(password, 10);
|
||||||
@ -213,6 +215,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
goalFontFamily: true,
|
goalFontFamily: true,
|
||||||
goalFontSize: true,
|
goalFontSize: true,
|
||||||
goalFontWeight: true,
|
goalFontWeight: true,
|
||||||
|
goalScope: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -178,6 +178,9 @@ const translations: Record<string, any> = {
|
|||||||
endHour: 'End of Day',
|
endHour: 'End of Day',
|
||||||
weekAbbr: 'W',
|
weekAbbr: 'W',
|
||||||
goalOfWeek: 'Goal of the Week',
|
goalOfWeek: 'Goal of the Week',
|
||||||
|
goalScope: 'Goal Scope',
|
||||||
|
goalScopeWeek: 'Per Week',
|
||||||
|
goalScopeDay: 'Per Day',
|
||||||
goalFallback: 'Goal Fallback Type',
|
goalFallback: 'Goal Fallback Type',
|
||||||
defaultGoal: 'Custom Default Goal',
|
defaultGoal: 'Custom Default Goal',
|
||||||
showSomeday: 'Show Someday Section',
|
showSomeday: 'Show Someday Section',
|
||||||
@ -230,6 +233,9 @@ const translations: Record<string, any> = {
|
|||||||
endHour: 'Tagesende',
|
endHour: 'Tagesende',
|
||||||
weekAbbr: 'KW',
|
weekAbbr: 'KW',
|
||||||
goalOfWeek: 'Ziel der Woche',
|
goalOfWeek: 'Ziel der Woche',
|
||||||
|
goalScope: 'Ziel-Zeitraum',
|
||||||
|
goalScopeWeek: 'Pro Woche',
|
||||||
|
goalScopeDay: 'Pro Tag',
|
||||||
goalFallback: 'Ziel-Fallback-Typ',
|
goalFallback: 'Ziel-Fallback-Typ',
|
||||||
defaultGoal: 'Benutzerdefiniertes Standardziel',
|
defaultGoal: 'Benutzerdefiniertes Standardziel',
|
||||||
showSomeday: 'Irgendwann-Bereich anzeigen',
|
showSomeday: 'Irgendwann-Bereich anzeigen',
|
||||||
@ -458,6 +464,7 @@ export default function WeeklyView() {
|
|||||||
goalFontFamily?: string;
|
goalFontFamily?: string;
|
||||||
goalFontSize?: string;
|
goalFontSize?: string;
|
||||||
goalFontWeight?: string;
|
goalFontWeight?: string;
|
||||||
|
goalScope?: 'week' | 'day';
|
||||||
}>({
|
}>({
|
||||||
name: session?.user?.name || '',
|
name: session?.user?.name || '',
|
||||||
email: session?.user?.email || '',
|
email: session?.user?.email || '',
|
||||||
@ -539,6 +546,7 @@ export default function WeeklyView() {
|
|||||||
// New UI State
|
// New UI State
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
|
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
|
||||||
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||||
|
|
||||||
const [focusTimerDuration, setFocusTimerDuration] = useState(25);
|
const [focusTimerDuration, setFocusTimerDuration] = useState(25);
|
||||||
const [fontSize, setFontSize] = useState<'S' | 'M' | 'L'>('M');
|
const [fontSize, setFontSize] = useState<'S' | 'M' | 'L'>('M');
|
||||||
@ -848,11 +856,30 @@ export default function WeeklyView() {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Fetch goal for current week
|
// Compute the goal date key: for "week" scope, normalize to Monday of that week; for "day", use the exact date
|
||||||
|
const getGoalDateKey = useCallback((date: Date): string => {
|
||||||
|
const scope = profile.goalScope || 'week';
|
||||||
|
if (scope === 'day') {
|
||||||
|
const d = new Date(date);
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
return d.toISOString();
|
||||||
|
}
|
||||||
|
// Normalize to Monday of the week containing this date
|
||||||
|
const d = new Date(date);
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
const day = d.getDay(); // 0=Sun, 1=Mon, ...
|
||||||
|
const diff = day === 0 ? -6 : 1 - day; // Monday offset
|
||||||
|
d.setDate(d.getDate() + diff);
|
||||||
|
return d.toISOString();
|
||||||
|
}, [profile.goalScope]);
|
||||||
|
|
||||||
|
const goalDateKey = useMemo(() => getGoalDateKey(currentWeekStart), [currentWeekStart, getGoalDateKey]);
|
||||||
|
|
||||||
|
// Fetch goal for current week/day
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchGoal = async () => {
|
const fetchGoal = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/goal?weekStart=${currentWeekStart.toISOString()}`);
|
const res = await fetch(`/api/goal?weekStart=${goalDateKey}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setGoal(data.goal);
|
setGoal(data.goal);
|
||||||
@ -862,7 +889,7 @@ export default function WeeklyView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchGoal();
|
fetchGoal();
|
||||||
}, [currentWeekStart]);
|
}, [goalDateKey]);
|
||||||
|
|
||||||
const saveGoal = async (newGoal: string) => {
|
const saveGoal = async (newGoal: string) => {
|
||||||
setGoal(newGoal);
|
setGoal(newGoal);
|
||||||
@ -871,7 +898,7 @@ export default function WeeklyView() {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
weekStart: currentWeekStart.toISOString(),
|
weekStart: goalDateKey,
|
||||||
text: newGoal,
|
text: newGoal,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -1660,6 +1687,16 @@ export default function WeeklyView() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ id: taskId, markdownContent: notes }),
|
body: JSON.stringify({ id: taskId, markdownContent: notes }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync notes to external provider
|
||||||
|
const task = tasks.find(t => t.id === taskId);
|
||||||
|
if (task?.externalId && task?.externalProvider) {
|
||||||
|
fetch('/api/tasks/sync', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ taskId, notes }),
|
||||||
|
}).catch(e => console.error('Sync error:', e));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating task notes:', error);
|
console.error('Error updating task notes:', error);
|
||||||
}
|
}
|
||||||
@ -1692,6 +1729,7 @@ export default function WeeklyView() {
|
|||||||
|
|
||||||
const moveTaskToSlot = async (taskId: string, dayOfWeek: number, startTime: string, scheduledDate?: Date) => {
|
const moveTaskToSlot = async (taskId: string, dayOfWeek: number, startTime: string, scheduledDate?: Date) => {
|
||||||
const newScheduledDate = scheduledDate ? formatDateToISO(scheduledDate) : undefined;
|
const newScheduledDate = scheduledDate ? formatDateToISO(scheduledDate) : undefined;
|
||||||
|
const task = tasks.find(t => t.id === taskId);
|
||||||
setTasks(tasks.map(t =>
|
setTasks(tasks.map(t =>
|
||||||
t.id === taskId
|
t.id === taskId
|
||||||
? { ...t, dayOfWeek, startTime, scheduledDate: newScheduledDate || t.scheduledDate, updatedAt: new Date() }
|
? { ...t, dayOfWeek, startTime, scheduledDate: newScheduledDate || t.scheduledDate, updatedAt: new Date() }
|
||||||
@ -1704,6 +1742,15 @@ export default function WeeklyView() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ id: taskId, dayOfWeek, startTime, scheduledDate: newScheduledDate }),
|
body: JSON.stringify({ id: taskId, dayOfWeek, startTime, scheduledDate: newScheduledDate }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync due date change to external provider
|
||||||
|
if (task?.externalId && task?.externalProvider && newScheduledDate) {
|
||||||
|
fetch('/api/tasks/sync', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ taskId, scheduledDate: newScheduledDate }),
|
||||||
|
}).catch(e => console.error('Sync error:', e));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error moving task:', error);
|
console.error('Error moving task:', error);
|
||||||
}
|
}
|
||||||
@ -1870,6 +1917,15 @@ export default function WeeklyView() {
|
|||||||
startTime: resolvedStartTime
|
startTime: resolvedStartTime
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync due date change to external provider
|
||||||
|
if (task.externalId && task.externalProvider) {
|
||||||
|
fetch('/api/tasks/sync', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ taskId, scheduledDate: newScheduledDate }),
|
||||||
|
}).catch(e => console.error('Sync error:', e));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error rolling task:', error);
|
console.error('Error rolling task:', error);
|
||||||
}
|
}
|
||||||
@ -1954,6 +2010,15 @@ export default function WeeklyView() {
|
|||||||
startTime: targetSlot || ''
|
startTime: targetSlot || ''
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sync due date to external provider when moving from someday to calendar
|
||||||
|
if (draggedTask.externalId && draggedTask.externalProvider) {
|
||||||
|
fetch('/api/tasks/sync', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ taskId: draggedTask.id, scheduledDate: newScheduledDate }),
|
||||||
|
}).catch(e => console.error('Sync error:', e));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error moving task from someday to calendar:', error);
|
console.error('Error moving task from someday to calendar:', error);
|
||||||
}
|
}
|
||||||
@ -2333,6 +2398,28 @@ export default function WeeklyView() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Date Picker Toggle */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
className={`p-1.5 hover:bg-gray-100 rounded-md transition-colors ${showDatePicker ? 'text-teal-600 bg-teal-50' : 'text-gray-500 hover:text-black'}`}
|
||||||
|
onClick={() => setShowDatePicker(!showDatePicker)}
|
||||||
|
title="Jump to date"
|
||||||
|
>
|
||||||
|
<Calendar size={18} />
|
||||||
|
</button>
|
||||||
|
{showDatePicker && (
|
||||||
|
<SimpleDatePicker
|
||||||
|
selected={currentWeekStart}
|
||||||
|
onSelect={(date) => {
|
||||||
|
setCurrentWeekStart(getStartOfWeek(date));
|
||||||
|
setShowDatePicker(false);
|
||||||
|
}}
|
||||||
|
onClose={() => setShowDatePicker(false)}
|
||||||
|
language={language}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<button
|
<button
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-md text-gray-500 hover:text-black transition-colors"
|
className="p-1.5 hover:bg-gray-100 rounded-md text-gray-500 hover:text-black transition-colors"
|
||||||
@ -3115,6 +3202,15 @@ export default function WeeklyView() {
|
|||||||
startTime: null
|
startTime: null
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear due date in external provider when moving to someday
|
||||||
|
if (taskToMove.externalId && taskToMove.externalProvider) {
|
||||||
|
fetch('/api/tasks/sync', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ taskId: taskToMove.id, scheduledDate: null }),
|
||||||
|
}).catch(e => console.error('Sync error:', e));
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error moving task to someday list:', error);
|
console.error('Error moving task to someday list:', error);
|
||||||
}
|
}
|
||||||
@ -4252,6 +4348,7 @@ function SettingsSidebar({
|
|||||||
goalFontFamily?: string;
|
goalFontFamily?: string;
|
||||||
goalFontSize?: string;
|
goalFontSize?: string;
|
||||||
goalFontWeight?: string;
|
goalFontWeight?: string;
|
||||||
|
goalScope?: 'week' | 'day';
|
||||||
}>({
|
}>({
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
@ -4363,6 +4460,7 @@ function SettingsSidebar({
|
|||||||
goalFontFamily: data.user.goalFontFamily || 'Inter',
|
goalFontFamily: data.user.goalFontFamily || 'Inter',
|
||||||
goalFontSize: data.user.goalFontSize || '0.9rem',
|
goalFontSize: data.user.goalFontSize || '0.9rem',
|
||||||
goalFontWeight: data.user.goalFontWeight || '500',
|
goalFontWeight: data.user.goalFontWeight || '500',
|
||||||
|
goalScope: data.user.goalScope || 'week',
|
||||||
weekendColorSat: data.user.weekendColorSat || '#666666',
|
weekendColorSat: data.user.weekendColorSat || '#666666',
|
||||||
weekendColorSun: data.user.weekendColorSun || '#dc2626',
|
weekendColorSun: data.user.weekendColorSun || '#dc2626',
|
||||||
weekdayColor: data.user.weekdayColor || '#888888',
|
weekdayColor: data.user.weekdayColor || '#888888',
|
||||||
@ -4636,7 +4734,8 @@ function SettingsSidebar({
|
|||||||
protectEventTimes: profile.protectEventTimes || protectEventTimes,
|
protectEventTimes: profile.protectEventTimes || protectEventTimes,
|
||||||
focusTimerDuration: profile.focusTimerDuration || focusTimerDuration,
|
focusTimerDuration: profile.focusTimerDuration || focusTimerDuration,
|
||||||
focusBreakDuration: profile.focusBreakDuration || focusBreakDuration,
|
focusBreakDuration: profile.focusBreakDuration || focusBreakDuration,
|
||||||
pastDayColor: profile.pastDayColor
|
pastDayColor: profile.pastDayColor,
|
||||||
|
goalScope: profile.goalScope
|
||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5291,6 +5390,33 @@ function SettingsSidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Goal Scope Setting */}
|
||||||
|
<div style={{ background: 'var(--weekly-settings-item-bg)', padding: '16px', borderRadius: '8px', marginBottom: '1.5rem', border: '1px solid var(--weekly-border)' }}>
|
||||||
|
<label style={{ display: 'block', fontSize: '0.9rem', fontWeight: 600, color: 'var(--weekly-settings-label)', marginBottom: '12px' }}>{t.goalScope}</label>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setProfile({ ...profile, goalScope: 'week' })}
|
||||||
|
style={{
|
||||||
|
flex: 1, padding: '8px 12px', borderRadius: '6px',
|
||||||
|
border: profile.goalScope === 'week' || !profile.goalScope ? '2px solid var(--weekly-accent, #4A90D9)' : '1px solid #ddd',
|
||||||
|
background: profile.goalScope === 'week' || !profile.goalScope ? 'var(--weekly-accent-light, #e8f0fe)' : 'white',
|
||||||
|
fontWeight: profile.goalScope === 'week' || !profile.goalScope ? 600 : 400,
|
||||||
|
cursor: 'pointer', fontSize: '0.85rem'
|
||||||
|
}}
|
||||||
|
>{t.goalScopeWeek}</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setProfile({ ...profile, goalScope: 'day' })}
|
||||||
|
style={{
|
||||||
|
flex: 1, padding: '8px 12px', borderRadius: '6px',
|
||||||
|
border: profile.goalScope === 'day' ? '2px solid var(--weekly-accent, #4A90D9)' : '1px solid #ddd',
|
||||||
|
background: profile.goalScope === 'day' ? 'var(--weekly-accent-light, #e8f0fe)' : 'white',
|
||||||
|
fontWeight: profile.goalScope === 'day' ? 600 : 400,
|
||||||
|
cursor: 'pointer', fontSize: '0.85rem'
|
||||||
|
}}
|
||||||
|
>{t.goalScopeDay}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Goal Fallback Settings */}
|
{/* Goal Fallback Settings */}
|
||||||
<div style={{ background: 'var(--weekly-settings-item-bg)', padding: '16px', borderRadius: '8px', marginBottom: '1.5rem', border: '1px solid var(--weekly-border)' }}>
|
<div style={{ background: 'var(--weekly-settings-item-bg)', padding: '16px', borderRadius: '8px', marginBottom: '1.5rem', border: '1px solid var(--weekly-border)' }}>
|
||||||
<label style={{ display: 'block', fontSize: '0.9rem', fontWeight: 600, color: 'var(--weekly-settings-label)', marginBottom: '12px' }}>{t.goalOfWeek} Fallback</label>
|
<label style={{ display: 'block', fontSize: '0.9rem', fontWeight: 600, color: 'var(--weekly-settings-label)', marginBottom: '12px' }}>{t.goalOfWeek} Fallback</label>
|
||||||
|
|||||||
@ -79,12 +79,13 @@ export const fetchGoogleTasks = async (client: OAuth2Client, taskListId: string)
|
|||||||
/**
|
/**
|
||||||
* Update a Google Task status
|
* Update a Google Task status
|
||||||
*/
|
*/
|
||||||
export const updateGoogleTask = async (client: OAuth2Client, taskListId: string, taskId: string, updates: { title?: string; notes?: string; status?: 'needsAction' | 'completed' }): Promise<GoogleTask> => {
|
export const updateGoogleTask = async (client: OAuth2Client, taskListId: string, taskId: string, updates: { title?: string; notes?: string; status?: 'needsAction' | 'completed'; due?: string | null }): Promise<GoogleTask> => {
|
||||||
const service = google.tasks({ version: 'v1', auth: client });
|
const service = google.tasks({ version: 'v1', auth: client });
|
||||||
try {
|
try {
|
||||||
const requestBody: any = {};
|
const requestBody: any = {};
|
||||||
if (updates.title !== undefined) requestBody.title = updates.title;
|
if (updates.title !== undefined) requestBody.title = updates.title;
|
||||||
if (updates.notes !== undefined) requestBody.notes = updates.notes;
|
if (updates.notes !== undefined) requestBody.notes = updates.notes;
|
||||||
|
if (updates.due !== undefined) requestBody.due = updates.due;
|
||||||
if (updates.status !== undefined) {
|
if (updates.status !== undefined) {
|
||||||
requestBody.status = updates.status;
|
requestBody.status = updates.status;
|
||||||
requestBody.completed = updates.status === 'completed' ? new Date().toISOString() : null;
|
requestBody.completed = updates.status === 'completed' ? new Date().toISOString() : null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user