From b45452324113802f1f34439b9c40406e2739da90 Mon Sep 17 00:00:00 2001 From: SylvainP1 <5533467-SylvainP1@users.noreply.replit.com> Date: Tue, 14 Apr 2026 08:42:13 +0000 Subject: [PATCH] Remove save button and implement auto-save functionality Replaces the manual save button with an automatic save feature that triggers after a short delay and updates the UI to reflect the save status. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 55837015-10e9-4be9-b857-7f5e6be73772 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 5bc80858-66c8-4834-9177-6c352a80c5e3 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1cc377db-7ea0-49f2-97ce-c3e87e0228cc/55837015-10e9-4be9-b857-7f5e6be73772/Sh1LoFS Replit-Helium-Checkpoint-Created: true --- .../cra-app/src/pages/timesheet-detail.tsx | 135 ++++++++++++------ 1 file changed, 93 insertions(+), 42 deletions(-) diff --git a/artifacts/cra-app/src/pages/timesheet-detail.tsx b/artifacts/cra-app/src/pages/timesheet-detail.tsx index 14c8f03..192f2ae 100644 --- a/artifacts/cra-app/src/pages/timesheet-detail.tsx +++ b/artifacts/cra-app/src/pages/timesheet-detail.tsx @@ -13,7 +13,6 @@ import { import { useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, - Save, Send, Plus, Trash2, @@ -105,25 +104,19 @@ export default function TimesheetDetailPage() { }, [timesheet]); const HOUR_OPTIONS = [0, 0.5, 1, 2, 3, 4, 5, 6, 7, 7.7]; + const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle"); + const saveTimerRef = useRef | null>(null); + const localEntriesRef = useRef(localEntries); + localEntriesRef.current = localEntries; - const handleSetHours = (lineId: number, dateStr: string, hours: number) => { - const key = `${lineId}-${dateStr}`; - setLocalEntries(prev => ({ ...prev, [key]: hours })); - setHasUnsavedChanges(true); - }; - - const mutateFnRef = useRef(upsertEntries.mutate); - mutateFnRef.current = upsertEntries.mutate; - - const handleSave = useCallback(() => { + const doSave = useCallback(() => { if (!timesheetId) return; - + const entries = localEntriesRef.current; + const entriesToSave: LocalEntry[] = []; - Object.entries(localEntries).forEach(([key, hours]) => { - const [lineIdStr, date] = key.split("-", 2); - // Reconstruct the full date string since we split by '-' - const fullDate = key.substring(lineIdStr.length + 1); - + Object.entries(entries).forEach(([key, hours]) => { + const lineIdStr = key.split("-")[0]; + const fullDate = key.substring(lineIdStr.length + 1); if (hours > 0) { entriesToSave.push({ timesheetLineId: parseInt(lineIdStr), @@ -133,37 +126,84 @@ export default function TimesheetDetailPage() { } }); - mutateFnRef.current({ + setSaveStatus("saving"); + upsertEntries.mutate({ timesheetId, data: { entries: entriesToSave } }, { onSuccess: () => { - toast({ title: "Modifications sauvegardées" }); + setSaveStatus("saved"); setHasUnsavedChanges(false); queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) }); + setTimeout(() => setSaveStatus("idle"), 2000); }, onError: () => { + setSaveStatus("idle"); toast({ title: "Erreur de sauvegarde", variant: "destructive" }); } }); - }, [timesheetId, localEntries, toast, queryClient]); + }, [timesheetId, upsertEntries, queryClient, toast]); + + const handleSetHours = (lineId: number, dateStr: string, hours: number) => { + const key = `${lineId}-${dateStr}`; + setLocalEntries(prev => ({ ...prev, [key]: hours })); + setHasUnsavedChanges(true); + + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); + saveTimerRef.current = setTimeout(() => doSave(), 800); + }; + + useEffect(() => { + return () => { + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); + }; + }, []); const handleSubmit = () => { - if (hasUnsavedChanges) { - toast({ title: "Veuillez sauvegarder avant de soumettre", variant: "destructive" }); - return; + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current); + saveTimerRef.current = null; } - - if (confirm("Êtes-vous sûr de vouloir soumettre ce CRA ? Il ne pourra plus être modifié.")) { - updateTimesheet.mutate({ - id: timesheetId, - data: { status: "submitted" } - }, { - onSuccess: () => { - toast({ title: "CRA soumis avec succès" }); - queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) }); + + const flushAndSubmit = () => { + if (confirm("Êtes-vous sûr de vouloir soumettre ce CRA ? Il ne pourra plus être modifié.")) { + updateTimesheet.mutate({ + id: timesheetId, + data: { status: "submitted" } + }, { + onSuccess: () => { + toast({ title: "CRA soumis avec succès" }); + queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) }); + } + }); + } + }; + + if (hasUnsavedChanges) { + const entries = localEntriesRef.current; + const entriesToSave: LocalEntry[] = []; + Object.entries(entries).forEach(([key, hours]) => { + const lineIdStr = key.split("-")[0]; + const fullDate = key.substring(lineIdStr.length + 1); + if (hours > 0) { + entriesToSave.push({ timesheetLineId: parseInt(lineIdStr), date: fullDate, hours }); } }); + setSaveStatus("saving"); + upsertEntries.mutate({ timesheetId, data: { entries: entriesToSave } }, { + onSuccess: () => { + setSaveStatus("idle"); + setHasUnsavedChanges(false); + queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) }); + flushAndSubmit(); + }, + onError: () => { + setSaveStatus("idle"); + toast({ title: "Erreur de sauvegarde, impossible de soumettre", variant: "destructive" }); + } + }); + } else { + flushAndSubmit(); } }; @@ -247,20 +287,31 @@ export default function TimesheetDetailPage() {
{isEditable && ( <> - + + {saveStatus === "saving" && ( + <> + + Enregistrement... + + )} + {saveStatus === "saved" && ( + <> + + Enregistré + + )} + {saveStatus === "idle" && !hasUnsavedChanges && ( + <> + + À jour + + )} +