From 4c18d37beca34e96082ed593c4eb61edd06d95a9 Mon Sep 17 00:00:00 2001 From: mARTin Date: Sun, 8 Mar 2026 19:56:21 +0100 Subject: [PATCH] feat: account number display, weekly goals export/import, calendar event fixes - Show Account Number in Settings > Account for user identification - Add weekly goals to JSON export/import with upsert-based merge - Fix calendar events not appearing instantly (await cache ops, prevent provider force-refresh from overwriting optimistic updates) - Add Sign Out button in Settings > Account for mobile accessibility - Fix dateVerticalAlign persistence in profile API - Fix quote API error handling for non-JSON responses - Add Synology to CalendarEvent source type union v1.20.0 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- prisma/schema.prisma | 1 + src/app/api/calendar/events/route.ts | 27 ++++++--- src/app/api/user/export-data/route.ts | 14 +++++ src/app/api/user/import-data/route.ts | 35 +++++++++++ src/app/api/user/profile/route.ts | 5 +- src/components/WeeklyView.tsx | 85 ++++++++++++++++++++++++--- 7 files changed, 151 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index f65ab67..499d44e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.19.0", + "version": "1.20.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5e4b292..5d0cc2c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -75,6 +75,7 @@ model User { dateLayout String @default("right") mobileDateLayout String @default("below") dateAlignment String @default("center") + dateVerticalAlign String? @default("middle") hourLabelFormat String @default("short") showSubHourSlots Boolean @default(true) allDayPosition String @default("above") diff --git a/src/app/api/calendar/events/route.ts b/src/app/api/calendar/events/route.ts index 85ddc89..5eefb1c 100644 --- a/src/app/api/calendar/events/route.ts +++ b/src/app/api/calendar/events/route.ts @@ -65,9 +65,12 @@ export async function POST(request: NextRequest) { url, } as any); - // Update cache - upsertCachedEvent(userId, connection.id, connection.provider, event) - .catch(e => console.error('[CACHE] Failed to cache created event:', e)); + // Update cache - await to ensure it's ready before client refreshes + try { + await upsertCachedEvent(userId, connection.id, connection.provider, event); + } catch (e) { + console.error('[CACHE] Failed to cache created event:', e); + } return NextResponse.json({ event }); } catch (error: any) { @@ -109,9 +112,12 @@ export async function PATCH(request: NextRequest) { url, } as any); - // Update cache - upsertCachedEvent(userId, connection.id, connection.provider, event) - .catch(e => console.error('[CACHE] Failed to cache updated event:', e)); + // Update cache - await to ensure it's ready before client refreshes + try { + await upsertCachedEvent(userId, connection.id, connection.provider, event); + } catch (e) { + console.error('[CACHE] Failed to cache updated event:', e); + } return NextResponse.json({ event }); } catch (error: any) { @@ -145,9 +151,12 @@ export async function DELETE(request: NextRequest) { await deleteCalendarEvent(connection, calendarId, eventId); - // Remove from cache - deleteCachedEvent(userId, eventId, connection.provider) - .catch(e => console.error('[CACHE] Failed to delete cached event:', e)); + // Remove from cache - await to ensure consistency + try { + await deleteCachedEvent(userId, eventId, connection.provider); + } catch (e) { + console.error('[CACHE] Failed to delete cached event:', e); + } return NextResponse.json({ success: true }); } catch (error: any) { diff --git a/src/app/api/user/export-data/route.ts b/src/app/api/user/export-data/route.ts index b2bf32b..9b5d647 100644 --- a/src/app/api/user/export-data/route.ts +++ b/src/app/api/user/export-data/route.ts @@ -99,12 +99,26 @@ export async function GET() { } } + // Fetch weekly goals + const weeklyGoals = await prisma.weeklyGoal.findMany({ + where: { userId: user.id }, + orderBy: { weekStart: 'asc' }, + select: { + id: true, + weekStart: true, + text: true, + createdAt: true, + updatedAt: true, + }, + }); + const exportData = { exportVersion: 1, exportDate: new Date().toISOString(), somedayLists, projects, tasks: topLevelTasks, + weeklyGoals, }; const jsonContent = JSON.stringify(exportData, null, 2); diff --git a/src/app/api/user/import-data/route.ts b/src/app/api/user/import-data/route.ts index cfa103d..f4aff99 100644 --- a/src/app/api/user/import-data/route.ts +++ b/src/app/api/user/import-data/route.ts @@ -30,6 +30,14 @@ interface ImportTask { subTasks?: ImportTask[]; } +interface ImportWeeklyGoal { + id?: string; + weekStart: string; + text: string; + createdAt?: string; + updatedAt?: string; +} + interface ImportData { exportVersion: number; somedayLists?: Array<{ @@ -50,6 +58,7 @@ interface ImportData { updatedAt?: string; }>; tasks?: ImportTask[]; + weeklyGoals?: ImportWeeklyGoal[]; } export async function POST(request: Request) { @@ -81,6 +90,7 @@ export async function POST(request: Request) { const somedayLists = body.somedayLists || []; const projects = body.projects || []; const tasks = body.tasks || []; + const weeklyGoals = body.weeklyGoals || []; // ID mapping: old export ID -> new DB ID const listIdMap = new Map(); @@ -90,12 +100,14 @@ export async function POST(request: Request) { let importedLists = 0; let importedProjects = 0; let importedTasks = 0; + let importedGoals = 0; // In replace mode, delete all existing data first if (mode === 'replace') { await prisma.task.deleteMany({ where: { userId: user.id } }); await prisma.somedayList.deleteMany({ where: { userId: user.id } }); await prisma.project.deleteMany({ where: { userId: user.id } }); + await prisma.weeklyGoal.deleteMany({ where: { userId: user.id } }); } // 1. Import someday lists @@ -196,6 +208,28 @@ export async function POST(request: Request) { } } + // 4. Import weekly goals (upsert by weekStart to avoid duplicates) + for (const goal of weeklyGoals) { + const weekStart = new Date(goal.weekStart); + await prisma.weeklyGoal.upsert({ + where: { + userId_weekStart: { + userId: user.id, + weekStart, + }, + }, + update: { + text: goal.text, + }, + create: { + userId: user.id, + weekStart, + text: goal.text, + }, + }); + importedGoals++; + } + return NextResponse.json({ success: true, mode, @@ -203,6 +237,7 @@ export async function POST(request: Request) { somedayLists: importedLists, projects: importedProjects, tasks: importedTasks, + weeklyGoals: importedGoals, }, }); } catch (error) { diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts index 863408b..9c10ebe 100644 --- a/src/app/api/user/profile/route.ts +++ b/src/app/api/user/profile/route.ts @@ -44,6 +44,7 @@ export async function GET(request: NextRequest) { goalScope: true, dateLayout: true, mobileDateLayout: true, + dateVerticalAlign: true, headlineFont: true, headlineFontSize: true, headlineFontWeight: true, @@ -124,7 +125,7 @@ export async function PATCH(request: NextRequest) { weekdayColor, dateColor, taskColor, todayHighlightColor, pastDayColor, goalFallbackType, goalDefaultSentence, goalFontFamily, goalFontSize, goalFontWeight, goalScope, - dateLayout, mobileDateLayout, + dateLayout, mobileDateLayout, dateVerticalAlign, hourLabelFormat, showSubHourSlots, allDayPosition, cwFontFamily, cwFontSize, cwFontWeight, cwColor, yearFontFamily, yearFontSize, yearFontWeight, yearColor, @@ -187,6 +188,7 @@ export async function PATCH(request: NextRequest) { ...(goalScope !== undefined && { goalScope }), ...(dateLayout !== undefined && { dateLayout }), ...(mobileDateLayout !== undefined && { mobileDateLayout }), + ...(dateVerticalAlign !== undefined && { dateVerticalAlign }), ...(hourLabelFormat !== undefined && { hourLabelFormat }), ...(showSubHourSlots !== undefined && { showSubHourSlots }), ...(allDayPosition !== undefined && { allDayPosition }), @@ -274,6 +276,7 @@ export async function PATCH(request: NextRequest) { goalScope: true, dateLayout: true, mobileDateLayout: true, + dateVerticalAlign: true, cwFontFamily: true, cwFontSize: true, cwFontWeight: true, diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 3ef932c..ff138db 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -133,7 +133,7 @@ interface CalendarEvent { title: string; startTime: string; endTime: string; - source: "google" | "apple" | "outlook"; + source: "google" | "apple" | "outlook" | "synology"; calendarId?: string; calendarTitle?: string; calendarColor?: string; @@ -818,6 +818,8 @@ export default function WeeklyView() { showTaskCheckboxes?: boolean; quoteSourceUrls?: string[]; startDayOffset?: number; + id?: string; + accountNumber?: number; weekdayFormat?: "long" | "short" | "narrow" | "custom"; weekdayCase?: "normal" | "capitalize" | "uppercase"; customWeekdayNames?: string; @@ -1231,9 +1233,10 @@ export default function WeeklyView() { return [...prev, frontendEvent]; }); } - // Delay the force-refresh to give the provider time to propagate - // This prevents overwriting the optimistic update with stale data - setTimeout(() => fetchCalendarEvents(true), 3000); + // Re-read from cache (not a force-refresh from provider, which could + // overwrite the optimistic update if the provider hasn't propagated yet). + // The backend already cached the event via upsertCachedEvent. + setTimeout(() => fetchCalendarEvents(false), 2000); } catch (error: any) { console.error("Error saving event:", error); if (error.name === "AbortError") { @@ -1259,9 +1262,9 @@ export default function WeeklyView() { throw new Error(err.error || "Failed to delete event"); } - // Optimistically remove, then force refresh + // Optimistically remove, then re-read cache setRawCalendarEvents(prev => prev.filter(e => e.id !== eventId)); - fetchCalendarEvents(true); + setTimeout(() => fetchCalendarEvents(false), 2000); } catch (error) { console.error("Error deleting event:", error); throw error; @@ -1300,13 +1303,15 @@ export default function WeeklyView() { const urls = profile.quoteSourceUrls && profile.quoteSourceUrls.length > 0 ? profile.quoteSourceUrls - : [profile.quoteSourceUrl || "https://recite.vercel.app/api/random"]; + : profile.quoteSourceUrl ? [profile.quoteSourceUrl] : []; // Strategy: try sources until one works for (const url of urls) { try { const res = await fetch(url); if (!res.ok) continue; + const contentType = res.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) continue; const data = await res.json(); let quoteText = ""; @@ -8105,6 +8110,7 @@ function SettingsSidebar({ quoteSourceUrls?: string[]; startDayOffset?: number; id?: string; + accountNumber?: number; weekdayFormat?: "long" | "short" | "narrow" | "custom"; weekdayCase?: "normal" | "capitalize" | "uppercase"; customWeekdayNames?: string; @@ -11643,6 +11649,43 @@ function SettingsSidebar({ )} + {profile.accountNumber && ( +
+ + (e.target as HTMLInputElement).select()} + className="weekly-input" + style={{ + width: "100%", + padding: "8px", + border: "1px solid #eee", + borderRadius: "4px", + background: "#f5f5f5", + color: "#555", + fontSize: "0.85rem", + fontFamily: "monospace", + cursor: "text", + }} + /> + + {profile.language === "de" + ? "Ihre Kontonummer zur Identifikation bei E-Mail-Änderungen" + : "Your account number for identification when changing email"} + +
+ )}