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")
|
||||
goalFontSize String? @default("0.9rem")
|
||||
goalFontWeight String? @default("500")
|
||||
goalScope String @default("week") // "week" | "day"
|
||||
headlineFont String @default("Inter")
|
||||
headlineFontSize String? @default("1.25rem")
|
||||
headlineFontWeight String? @default("900")
|
||||
|
||||
@ -4,6 +4,8 @@ import { authOptions } from "@/lib/auth";
|
||||
import { google } from 'googleapis';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Google Calendar OAuth callback endpoint
|
||||
|
||||
@ -3,6 +3,8 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { google } from 'googleapis';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Initiate Google Calendar OAuth flow
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
||||
@ -4,7 +4,7 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
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();
|
||||
|
||||
@ -90,12 +90,17 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
} 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' }
|
||||
});
|
||||
|
||||
const connection = appleConn || remindersConn;
|
||||
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(':');
|
||||
@ -106,16 +111,17 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
for (const sourceList of lists) {
|
||||
try {
|
||||
console.log(`[IMPORT] Fetching reminders from list: ${sourceList.title} (guid: ${sourceList.id})`);
|
||||
const reminders = await fetchAppleReminders(email, password, sourceList.id);
|
||||
console.log(`[IMPORT] Fetched ${reminders.length} reminders from "${sourceList.title}"`);
|
||||
importedTasks.push(...reminders.map(r => ({
|
||||
title: r.title,
|
||||
description: r.description || '',
|
||||
externalId: r.guid,
|
||||
// sourceList.id is a CalDAV URL (from getAppleReminderLists)
|
||||
console.log(`[IMPORT] Fetching reminders from list: ${sourceList.title} (url: ${sourceList.id})`);
|
||||
const tasks = await fetchAppleTasks(email, password, sourceList.id);
|
||||
console.log(`[IMPORT] Fetched ${tasks.length} tasks from "${sourceList.title}"`);
|
||||
importedTasks.push(...tasks.map(t => ({
|
||||
title: t.title,
|
||||
description: t.description || '',
|
||||
externalId: t.id,
|
||||
externalListId: sourceList.id,
|
||||
dueDate: r.dueDate || null,
|
||||
status: r.isCompleted ? 'completed' : 'NEEDS-ACTION',
|
||||
dueDate: t.endDate ? new Date(t.endDate) : null,
|
||||
status: 'NEEDS-ACTION', // fetchTasks already filters out completed
|
||||
sourceListTitle: sourceList.title,
|
||||
})));
|
||||
} catch (e) {
|
||||
|
||||
@ -3,7 +3,7 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { createGoogleClient, fetchGoogleTaskLists } from '@/lib/google-tasks';
|
||||
import { fetchReminderLists } from '@/lib/apple-reminders';
|
||||
import { getAppleReminderLists } from '@/lib/apple-calendar';
|
||||
|
||||
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 })) });
|
||||
|
||||
} else if (provider === 'apple') {
|
||||
// Look for the apple-reminders connection
|
||||
const connection = await prisma.calendarConnection.findFirst({
|
||||
// Try CalDAV connections: prefer 'apple' (CalDAV with app-specific password),
|
||||
// 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' }
|
||||
});
|
||||
|
||||
const connection = appleConn || remindersConn;
|
||||
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(':');
|
||||
@ -58,13 +63,13 @@ export async function GET(req: NextRequest) {
|
||||
const password = connection.accessToken.slice(colonIdx + 1);
|
||||
|
||||
try {
|
||||
const reminderLists = await fetchReminderLists(email, password);
|
||||
return NextResponse.json({ lists: reminderLists.map(l => ({ id: l.guid, title: l.title })) });
|
||||
const reminderLists = await getAppleReminderLists(email, password);
|
||||
return NextResponse.json({ lists: reminderLists.map(l => ({ id: l.id, title: l.title })) });
|
||||
} 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({
|
||||
error: error.message || 'Failed to fetch Apple Reminder lists.',
|
||||
needsReconnect: error.message?.includes('2FA') || error.message?.includes('expired') || error.message?.includes('reconnect')
|
||||
error: error.message || 'Failed to fetch Apple Reminder lists via CalDAV.',
|
||||
needsReconnect: error.message?.includes('auth') || error.message?.includes('credentials') || error.message?.includes('401')
|
||||
}, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export async function PATCH(req: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { taskId, completed, title, action } = body;
|
||||
const { taskId, completed, title, action, scheduledDate, notes } = body;
|
||||
|
||||
if (!taskId) {
|
||||
return NextResponse.json({ error: 'Task ID required' }, { status: 400 });
|
||||
@ -47,25 +47,26 @@ export async function PATCH(req: NextRequest) {
|
||||
|
||||
if (action === 'delete') {
|
||||
await deleteGoogleTask(client, task.externalListId, task.externalId);
|
||||
} else if (title !== undefined && completed !== undefined) {
|
||||
await updateGoogleTask(client, task.externalListId, task.externalId, {
|
||||
title,
|
||||
status: completed ? 'completed' : 'needsAction',
|
||||
});
|
||||
} else if (title !== undefined) {
|
||||
await updateGoogleTask(client, task.externalListId, task.externalId, { title });
|
||||
} else if (completed !== undefined) {
|
||||
await updateGoogleTaskStatus(
|
||||
client,
|
||||
task.externalListId,
|
||||
task.externalId,
|
||||
completed ? 'completed' : 'needsAction'
|
||||
);
|
||||
} else {
|
||||
const updates: { title?: string; notes?: string; status?: 'needsAction' | 'completed'; due?: string | null } = {};
|
||||
if (title !== undefined) updates.title = title;
|
||||
if (notes !== undefined) updates.notes = notes;
|
||||
if (completed !== undefined) updates.status = completed ? 'completed' : 'needsAction';
|
||||
if (scheduledDate !== undefined) {
|
||||
// Google Tasks expects RFC 3339 date (YYYY-MM-DDT00:00:00.000Z)
|
||||
updates.due = scheduledDate ? new Date(scheduledDate).toISOString() : null;
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await updateGoogleTask(client, task.externalListId, task.externalId, updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (task.externalProvider === 'apple' && task.externalListId) {
|
||||
// Prefer 'apple' (CalDAV) connection, fall back to 'apple-reminders'
|
||||
const connection = await prisma.calendarConnection.findFirst({
|
||||
where: { userId: task.userId, provider: 'apple' }
|
||||
}) || await prisma.calendarConnection.findFirst({
|
||||
where: { userId: task.userId, provider: 'apple-reminders' }
|
||||
});
|
||||
|
||||
@ -99,6 +100,7 @@ export async function PATCH(req: NextRequest) {
|
||||
const updateData: any = {};
|
||||
if (completed !== undefined) updateData.completed = completed;
|
||||
if (title !== undefined) updateData.title = title;
|
||||
if (scheduledDate !== undefined) updateData.scheduledDate = scheduledDate ? new Date(scheduledDate) : null;
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
const updatedTask = await prisma.task.update({
|
||||
|
||||
@ -39,6 +39,7 @@ export async function GET(request: NextRequest) {
|
||||
goalFontFamily: true,
|
||||
goalFontSize: true,
|
||||
goalFontWeight: true,
|
||||
goalScope: true,
|
||||
headlineFont: true,
|
||||
headlineFontSize: true,
|
||||
headlineFontWeight: true,
|
||||
@ -98,7 +99,7 @@ export async function PATCH(request: NextRequest) {
|
||||
fontWeight, weekendColorSat, weekendColorSun,
|
||||
weekdayColor, dateColor, taskColor, todayHighlightColor,
|
||||
pastDayColor, goalFallbackType, goalDefaultSentence,
|
||||
goalFontFamily, goalFontSize, goalFontWeight
|
||||
goalFontFamily, goalFontSize, goalFontWeight, goalScope
|
||||
} = body;
|
||||
|
||||
const updateData: any = {
|
||||
@ -152,6 +153,7 @@ export async function PATCH(request: NextRequest) {
|
||||
...(goalFontFamily !== undefined && { goalFontFamily }),
|
||||
...(goalFontSize !== undefined && { goalFontSize }),
|
||||
...(goalFontWeight !== undefined && { goalFontWeight }),
|
||||
...(goalScope !== undefined && { goalScope }),
|
||||
};
|
||||
if (password && password.trim() !== "") {
|
||||
updateData.passwordHash = await bcrypt.hash(password, 10);
|
||||
@ -213,6 +215,7 @@ export async function PATCH(request: NextRequest) {
|
||||
goalFontFamily: true,
|
||||
goalFontSize: true,
|
||||
goalFontWeight: true,
|
||||
goalScope: true,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -178,6 +178,9 @@ const translations: Record<string, any> = {
|
||||
endHour: 'End of Day',
|
||||
weekAbbr: 'W',
|
||||
goalOfWeek: 'Goal of the Week',
|
||||
goalScope: 'Goal Scope',
|
||||
goalScopeWeek: 'Per Week',
|
||||
goalScopeDay: 'Per Day',
|
||||
goalFallback: 'Goal Fallback Type',
|
||||
defaultGoal: 'Custom Default Goal',
|
||||
showSomeday: 'Show Someday Section',
|
||||
@ -230,6 +233,9 @@ const translations: Record<string, any> = {
|
||||
endHour: 'Tagesende',
|
||||
weekAbbr: 'KW',
|
||||
goalOfWeek: 'Ziel der Woche',
|
||||
goalScope: 'Ziel-Zeitraum',
|
||||
goalScopeWeek: 'Pro Woche',
|
||||
goalScopeDay: 'Pro Tag',
|
||||
goalFallback: 'Ziel-Fallback-Typ',
|
||||
defaultGoal: 'Benutzerdefiniertes Standardziel',
|
||||
showSomeday: 'Irgendwann-Bereich anzeigen',
|
||||
@ -458,6 +464,7 @@ export default function WeeklyView() {
|
||||
goalFontFamily?: string;
|
||||
goalFontSize?: string;
|
||||
goalFontWeight?: string;
|
||||
goalScope?: 'week' | 'day';
|
||||
}>({
|
||||
name: session?.user?.name || '',
|
||||
email: session?.user?.email || '',
|
||||
@ -539,6 +546,7 @@ export default function WeeklyView() {
|
||||
// New UI State
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
|
||||
const [focusTimerDuration, setFocusTimerDuration] = useState(25);
|
||||
const [fontSize, setFontSize] = useState<'S' | 'M' | 'L'>('M');
|
||||
@ -848,11 +856,30 @@ export default function WeeklyView() {
|
||||
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(() => {
|
||||
const fetchGoal = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/goal?weekStart=${currentWeekStart.toISOString()}`);
|
||||
const res = await fetch(`/api/goal?weekStart=${goalDateKey}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setGoal(data.goal);
|
||||
@ -862,7 +889,7 @@ export default function WeeklyView() {
|
||||
}
|
||||
};
|
||||
fetchGoal();
|
||||
}, [currentWeekStart]);
|
||||
}, [goalDateKey]);
|
||||
|
||||
const saveGoal = async (newGoal: string) => {
|
||||
setGoal(newGoal);
|
||||
@ -871,7 +898,7 @@ export default function WeeklyView() {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
weekStart: currentWeekStart.toISOString(),
|
||||
weekStart: goalDateKey,
|
||||
text: newGoal,
|
||||
}),
|
||||
});
|
||||
@ -1660,6 +1687,16 @@ export default function WeeklyView() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
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) {
|
||||
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 newScheduledDate = scheduledDate ? formatDateToISO(scheduledDate) : undefined;
|
||||
const task = tasks.find(t => t.id === taskId);
|
||||
setTasks(tasks.map(t =>
|
||||
t.id === taskId
|
||||
? { ...t, dayOfWeek, startTime, scheduledDate: newScheduledDate || t.scheduledDate, updatedAt: new Date() }
|
||||
@ -1704,6 +1742,15 @@ export default function WeeklyView() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
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) {
|
||||
console.error('Error moving task:', error);
|
||||
}
|
||||
@ -1870,6 +1917,15 @@ export default function WeeklyView() {
|
||||
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) {
|
||||
console.error('Error rolling task:', error);
|
||||
}
|
||||
@ -1954,6 +2010,15 @@ export default function WeeklyView() {
|
||||
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) {
|
||||
console.error('Error moving task from someday to calendar:', error);
|
||||
}
|
||||
@ -2333,6 +2398,28 @@ export default function WeeklyView() {
|
||||
</button>
|
||||
</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 */}
|
||||
<button
|
||||
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
|
||||
}),
|
||||
});
|
||||
|
||||
// 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) {
|
||||
console.error('Error moving task to someday list:', error);
|
||||
}
|
||||
@ -4252,6 +4348,7 @@ function SettingsSidebar({
|
||||
goalFontFamily?: string;
|
||||
goalFontSize?: string;
|
||||
goalFontWeight?: string;
|
||||
goalScope?: 'week' | 'day';
|
||||
}>({
|
||||
name: '',
|
||||
email: '',
|
||||
@ -4363,6 +4460,7 @@ function SettingsSidebar({
|
||||
goalFontFamily: data.user.goalFontFamily || 'Inter',
|
||||
goalFontSize: data.user.goalFontSize || '0.9rem',
|
||||
goalFontWeight: data.user.goalFontWeight || '500',
|
||||
goalScope: data.user.goalScope || 'week',
|
||||
weekendColorSat: data.user.weekendColorSat || '#666666',
|
||||
weekendColorSun: data.user.weekendColorSun || '#dc2626',
|
||||
weekdayColor: data.user.weekdayColor || '#888888',
|
||||
@ -4636,7 +4734,8 @@ function SettingsSidebar({
|
||||
protectEventTimes: profile.protectEventTimes || protectEventTimes,
|
||||
focusTimerDuration: profile.focusTimerDuration || focusTimerDuration,
|
||||
focusBreakDuration: profile.focusBreakDuration || focusBreakDuration,
|
||||
pastDayColor: profile.pastDayColor
|
||||
pastDayColor: profile.pastDayColor,
|
||||
goalScope: profile.goalScope
|
||||
} as any);
|
||||
}
|
||||
|
||||
@ -5291,6 +5390,33 @@ function SettingsSidebar({
|
||||
</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 */}
|
||||
<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>
|
||||
|
||||
@ -79,12 +79,13 @@ export const fetchGoogleTasks = async (client: OAuth2Client, taskListId: string)
|
||||
/**
|
||||
* 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 });
|
||||
try {
|
||||
const requestBody: any = {};
|
||||
if (updates.title !== undefined) requestBody.title = updates.title;
|
||||
if (updates.notes !== undefined) requestBody.notes = updates.notes;
|
||||
if (updates.due !== undefined) requestBody.due = updates.due;
|
||||
if (updates.status !== undefined) {
|
||||
requestBody.status = updates.status;
|
||||
requestBody.completed = updates.status === 'completed' ? new Date().toISOString() : null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user