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
This commit is contained in:
SylvainP1 2026-04-14 08:42:13 +00:00
parent 7b2caa45fd
commit b454523241

View File

@ -13,7 +13,6 @@ import {
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { import {
ArrowLeft, ArrowLeft,
Save,
Send, Send,
Plus, Plus,
Trash2, Trash2,
@ -105,25 +104,19 @@ export default function TimesheetDetailPage() {
}, [timesheet]); }, [timesheet]);
const HOUR_OPTIONS = [0, 0.5, 1, 2, 3, 4, 5, 6, 7, 7.7]; 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<ReturnType<typeof setTimeout> | null>(null);
const localEntriesRef = useRef(localEntries);
localEntriesRef.current = localEntries;
const handleSetHours = (lineId: number, dateStr: string, hours: number) => { const doSave = useCallback(() => {
const key = `${lineId}-${dateStr}`;
setLocalEntries(prev => ({ ...prev, [key]: hours }));
setHasUnsavedChanges(true);
};
const mutateFnRef = useRef(upsertEntries.mutate);
mutateFnRef.current = upsertEntries.mutate;
const handleSave = useCallback(() => {
if (!timesheetId) return; if (!timesheetId) return;
const entries = localEntriesRef.current;
const entriesToSave: LocalEntry[] = []; const entriesToSave: LocalEntry[] = [];
Object.entries(localEntries).forEach(([key, hours]) => { Object.entries(entries).forEach(([key, hours]) => {
const [lineIdStr, date] = key.split("-", 2); const lineIdStr = key.split("-")[0];
// Reconstruct the full date string since we split by '-'
const fullDate = key.substring(lineIdStr.length + 1); const fullDate = key.substring(lineIdStr.length + 1);
if (hours > 0) { if (hours > 0) {
entriesToSave.push({ entriesToSave.push({
timesheetLineId: parseInt(lineIdStr), timesheetLineId: parseInt(lineIdStr),
@ -133,37 +126,84 @@ export default function TimesheetDetailPage() {
} }
}); });
mutateFnRef.current({ setSaveStatus("saving");
upsertEntries.mutate({
timesheetId, timesheetId,
data: { entries: entriesToSave } data: { entries: entriesToSave }
}, { }, {
onSuccess: () => { onSuccess: () => {
toast({ title: "Modifications sauvegardées" }); setSaveStatus("saved");
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) }); queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) });
setTimeout(() => setSaveStatus("idle"), 2000);
}, },
onError: () => { onError: () => {
setSaveStatus("idle");
toast({ title: "Erreur de sauvegarde", variant: "destructive" }); 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 = () => { const handleSubmit = () => {
if (hasUnsavedChanges) { if (saveTimerRef.current) {
toast({ title: "Veuillez sauvegarder avant de soumettre", variant: "destructive" }); clearTimeout(saveTimerRef.current);
return; saveTimerRef.current = null;
} }
if (confirm("Êtes-vous sûr de vouloir soumettre ce CRA ? Il ne pourra plus être modifié.")) { const flushAndSubmit = () => {
updateTimesheet.mutate({ if (confirm("Êtes-vous sûr de vouloir soumettre ce CRA ? Il ne pourra plus être modifié.")) {
id: timesheetId, updateTimesheet.mutate({
data: { status: "submitted" } id: timesheetId,
}, { data: { status: "submitted" }
onSuccess: () => { }, {
toast({ title: "CRA soumis avec succès" }); onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) }); 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() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{isEditable && ( {isEditable && (
<> <>
<Button <span className="text-xs text-muted-foreground flex items-center gap-1.5">
variant={hasUnsavedChanges ? "default" : "outline"} {saveStatus === "saving" && (
onClick={handleSave} <>
disabled={!hasUnsavedChanges || upsertEntries.isPending} <span className="h-2 w-2 rounded-full bg-amber-400 animate-pulse" />
className="gap-2" Enregistrement...
> </>
<Save className="h-4 w-4" /> )}
{hasUnsavedChanges ? "Sauvegarder les modifs" : "Sauvegardé"} {saveStatus === "saved" && (
</Button> <>
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
Enregistré
</>
)}
{saveStatus === "idle" && !hasUnsavedChanges && (
<>
<CheckCircle className="h-3.5 w-3.5 text-muted-foreground" />
À jour
</>
)}
</span>
<Button <Button
variant="default" variant="default"
className="gap-2 bg-green-600 hover:bg-green-700 text-white" className="gap-2 bg-green-600 hover:bg-green-700 text-white"
onClick={handleSubmit} onClick={handleSubmit}
disabled={hasUnsavedChanges || updateTimesheet.isPending} disabled={saveStatus === "saving" || updateTimesheet.isPending}
> >
<Send className="h-4 w-4" /> <Send className="h-4 w-4" />
Soumettre Soumettre