From 829beddc863dd3cf9b3daee3e85b0590d52069f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 15 May 2026 12:41:52 +0000 Subject: [PATCH 1/2] Show save state on archive edit button The save button in ArchiveEditDialog now mirrors the behaviour of TaskEditDialog: it is disabled and reads "No Changes" when the form is clean, switches to "Save Changes" when any field or task has been modified, and shows a spinner ("Saving...") while the async persist is in flight. https://claude.ai/code/session_016b7TfPXSNPU8Jz7qyYMvRu --- src/components/ArchiveEditDialog.tsx | 38 +++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/components/ArchiveEditDialog.tsx b/src/components/ArchiveEditDialog.tsx index ea5d39e..f09536d 100644 --- a/src/components/ArchiveEditDialog.tsx +++ b/src/components/ArchiveEditDialog.tsx @@ -37,6 +37,7 @@ import { Calendar, Clock, Save, + Loader2, Trash2, Edit, AlertTriangle, @@ -103,6 +104,8 @@ export const ArchiveEditDialog: React.FC = ({ } = useTimeTracking(); const { toast } = useToast(); const [isEditing, setIsEditing] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [hasChanges, setHasChanges] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showRestoreDialog, setShowRestoreDialog] = useState(false); const [editingTask, setEditingTask] = useState(null); @@ -127,11 +130,29 @@ export const ArchiveEditDialog: React.FC = ({ }); setTasks([...day.tasks]); setIsEditing(false); + setHasChanges(false); setEditingTask(null); setShowDeleteConfirm(false); } }, [day, isOpen]); + // Track whether the form differs from the saved day + useEffect(() => { + if (!isEditing || !day) { + setHasChanges(false); + return; + } + const initialData = { + date: formatDateForInput(day.startTime), + startTime: formatTimeForInput(day.startTime), + endTime: formatTimeForInput(day.endTime), + notes: day.notes || "", + }; + const dayDataChanged = JSON.stringify(dayData) !== JSON.stringify(initialData); + const tasksChanged = JSON.stringify(tasks) !== JSON.stringify(day.tasks); + setHasChanges(dayDataChanged || tasksChanged); + }, [dayData, tasks, isEditing, day]); + const parseTimeInput = (timeStr: string, baseDate: Date): Date => { if (!timeStr || !timeStr.includes(":")) { return baseDate; @@ -200,12 +221,15 @@ export const ArchiveEditDialog: React.FC = ({ totalDuration: calculateTotalDuration(updatedTasks), }; + setIsSaving(true); try { await updateArchivedDay(day.id, updatedDay); setIsEditing(false); } catch (error) { console.error("Failed to save archived day:", error); toast({ title: "Save failed", description: "Failed to save changes. Please try again.", variant: "destructive" }); + } finally { + setIsSaving(false); } }; @@ -309,9 +333,17 @@ export const ArchiveEditDialog: React.FC = ({ - )} From dd7b6645b96576f99819c24312cf289a6a7654f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 15 May 2026 12:43:43 +0000 Subject: [PATCH 2/2] Reduce DB calls when saving archived day edits Two targeted improvements: 1. ArchiveEditDialog: only include tasks in the update payload when tasks were explicitly edited or the date changed (a date shift re-stamps all task timestamps). Previously tasks were always sent, causing an unnecessary SELECT + UPSERT even for notes-only edits. 2. TimeTrackingContext: on updateArchivedDay error, roll back only the affected day using its pre-update snapshot rather than re-fetching the entire archived-days collection from the database. https://claude.ai/code/session_016b7TfPXSNPU8Jz7qyYMvRu --- src/components/ArchiveEditDialog.tsx | 47 +++++++++++++++------------- src/contexts/TimeTrackingContext.tsx | 16 +++++++--- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/components/ArchiveEditDialog.tsx b/src/components/ArchiveEditDialog.tsx index f09536d..de57068 100644 --- a/src/components/ArchiveEditDialog.tsx +++ b/src/components/ArchiveEditDialog.tsx @@ -191,34 +191,39 @@ export const ArchiveEditDialog: React.FC = ({ newEndTime.setMonth(selectedDate.getMonth()); newEndTime.setDate(selectedDate.getDate()); - // Update all task timestamps to use the new date - const updatedTasks = tasks.map(task => { - const newTaskStartTime = new Date(task.startTime); - newTaskStartTime.setFullYear(selectedDate.getFullYear()); - newTaskStartTime.setMonth(selectedDate.getMonth()); - newTaskStartTime.setDate(selectedDate.getDate()); - - const newTaskEndTime = task.endTime ? new Date(task.endTime) : undefined; - if (newTaskEndTime) { - newTaskEndTime.setFullYear(selectedDate.getFullYear()); - newTaskEndTime.setMonth(selectedDate.getMonth()); - newTaskEndTime.setDate(selectedDate.getDate()); - } - - return { - ...task, - startTime: newTaskStartTime, - endTime: newTaskEndTime, - }; - }); + // Determine whether tasks need to be included in the update payload. + // A date change shifts every task's timestamps, so always send tasks then. + // Otherwise only send tasks if the user explicitly edited them. + const dateChanged = dayData.date !== formatDateForInput(day.startTime); + const tasksContentChanged = JSON.stringify(tasks) !== JSON.stringify(day.tasks); + const needsTaskUpdate = dateChanged || tasksContentChanged; + + // Re-stamp task timestamps only when necessary + const updatedTasks = needsTaskUpdate + ? tasks.map(task => { + const newTaskStartTime = new Date(task.startTime); + newTaskStartTime.setFullYear(selectedDate.getFullYear()); + newTaskStartTime.setMonth(selectedDate.getMonth()); + newTaskStartTime.setDate(selectedDate.getDate()); + + const newTaskEndTime = task.endTime ? new Date(task.endTime) : undefined; + if (newTaskEndTime) { + newTaskEndTime.setFullYear(selectedDate.getFullYear()); + newTaskEndTime.setMonth(selectedDate.getMonth()); + newTaskEndTime.setDate(selectedDate.getDate()); + } + + return { ...task, startTime: newTaskStartTime, endTime: newTaskEndTime }; + }) + : tasks; const updatedDay: Partial = { date: newStartTime.toDateString(), startTime: newStartTime, endTime: newEndTime, notes: dayData.notes || undefined, - tasks: updatedTasks, totalDuration: calculateTotalDuration(updatedTasks), + ...(needsTaskUpdate ? { tasks: updatedTasks } : {}), }; setIsSaving(true); diff --git a/src/contexts/TimeTrackingContext.tsx b/src/contexts/TimeTrackingContext.tsx index 0620ade..59f121d 100644 --- a/src/contexts/TimeTrackingContext.tsx +++ b/src/contexts/TimeTrackingContext.tsx @@ -801,8 +801,10 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ ) => { if (!dataService) return; - try { + // Capture original for targeted rollback on error + const originalDay = archivedDays.find(d => d.id === dayId); + try { // Optimistic update - update local state immediately for responsive UI setArchivedDays(prev => prev.map(day => (day.id === dayId ? { ...day, ...updates } : day)) @@ -814,9 +816,15 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ } catch (error) { console.error('❌ Error updating archived day:', error); - // On error, refresh from database to restore consistent state - const refreshedDays = await dataService.getArchivedDays(); - setArchivedDays(refreshedDays); + // Roll back only the affected day rather than re-fetching the entire archive + if (originalDay) { + setArchivedDays(prev => + prev.map(day => (day.id === dayId ? originalDay : day)) + ); + } else { + const refreshedDays = await dataService.getArchivedDays(); + setArchivedDays(refreshedDays); + } throw error; // Re-throw so the UI can handle it }