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:
parent
7b2caa45fd
commit
b454523241
@ -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<ReturnType<typeof setTimeout> | 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 '-'
|
||||
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() {
|
||||
<div className="flex items-center gap-3">
|
||||
{isEditable && (
|
||||
<>
|
||||
<Button
|
||||
variant={hasUnsavedChanges ? "default" : "outline"}
|
||||
onClick={handleSave}
|
||||
disabled={!hasUnsavedChanges || upsertEntries.isPending}
|
||||
className="gap-2"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{hasUnsavedChanges ? "Sauvegarder les modifs" : "Sauvegardé"}
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1.5">
|
||||
{saveStatus === "saving" && (
|
||||
<>
|
||||
<span className="h-2 w-2 rounded-full bg-amber-400 animate-pulse" />
|
||||
Enregistrement...
|
||||
</>
|
||||
)}
|
||||
{saveStatus === "saved" && (
|
||||
<>
|
||||
<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
|
||||
variant="default"
|
||||
className="gap-2 bg-green-600 hover:bg-green-700 text-white"
|
||||
onClick={handleSubmit}
|
||||
disabled={hasUnsavedChanges || updateTimesheet.isPending}
|
||||
disabled={saveStatus === "saving" || updateTimesheet.isPending}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
Soumettre
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user