Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 61 additions & 24 deletions src/components/ArchiveEditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
Calendar,
Clock,
Save,
Loader2,
Trash2,
Edit,
AlertTriangle,
Expand Down Expand Up @@ -103,6 +104,8 @@ export const ArchiveEditDialog: React.FC<ArchiveEditDialogProps> = ({
} = 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<Task | null>(null);
Expand All @@ -127,11 +130,29 @@ export const ArchiveEditDialog: React.FC<ArchiveEditDialogProps> = ({
});
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;
Expand Down Expand Up @@ -170,42 +191,50 @@ export const ArchiveEditDialog: React.FC<ArchiveEditDialogProps> = ({
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<DayRecord> = {
date: newStartTime.toDateString(),
startTime: newStartTime,
endTime: newEndTime,
notes: dayData.notes || undefined,
tasks: updatedTasks,
totalDuration: calculateTotalDuration(updatedTasks),
...(needsTaskUpdate ? { tasks: 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);
}
};

Expand Down Expand Up @@ -309,9 +338,17 @@ export const ArchiveEditDialog: React.FC<ArchiveEditDialogProps> = ({
<Button onClick={handleCancel} variant="outline" size="sm">
Cancel
</Button>
<Button onClick={handleSaveDay} size="sm">
<Save className="w-4 h-4 mr-2" />
Save
<Button
onClick={handleSaveDay}
size="sm"
disabled={!hasChanges || isSaving}
>
{isSaving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
{isSaving ? "Saving..." : hasChanges ? "Save Changes" : "No Changes"}
</Button>
</>
)}
Expand Down
16 changes: 12 additions & 4 deletions src/contexts/TimeTrackingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
}
Expand Down
Loading