import { useState, useMemo, useEffect, useRef, useCallback } from "react"; import { useParams, Link } from "wouter"; import { useGetTimesheet, getGetTimesheetQueryKey, useUpdateTimesheet, useCreateTimesheetLine, useDeleteTimesheetLine, useUpsertTimeEntries, useListProjects, getListProjectsQueryKey } from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Save, Send, Plus, Trash2, CheckCircle, AlertCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { useToast } from "@/hooks/use-toast"; import { formatMonthYear, STATUS_LABELS, STATUS_COLORS, cn } from "@/lib/utils"; import { getDaysInMonth, isWeekend, format } from "date-fns"; import { fr } from "date-fns/locale"; type LocalEntry = { timesheetLineId: number; date: string; hours: number; }; export default function TimesheetDetailPage() { const params = useParams(); const timesheetId = parseInt(params.id || "0"); const { toast } = useToast(); const queryClient = useQueryClient(); const { data: timesheet, isLoading } = useGetTimesheet( timesheetId, { query: { enabled: !!timesheetId, queryKey: getGetTimesheetQueryKey(timesheetId) } } ); const updateTimesheet = useUpdateTimesheet(); const createLine = useCreateTimesheetLine(); const deleteLine = useDeleteTimesheetLine(); const upsertEntries = useUpsertTimeEntries(); // Local state for optimistic updates const [localEntries, setLocalEntries] = useState>({}); const [isAddingLine, setIsAddingLine] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const { data: projects } = useListProjects({ query: { queryKey: getListProjectsQueryKey() } }); useEffect(() => { if (timesheet?.lines) { const initial: Record = {}; timesheet.lines.forEach(line => { line.entries?.forEach(entry => { const dateStr = typeof entry.date === "string" && entry.date.length > 10 ? entry.date.slice(0, 10) : entry.date; initial[`${line.id}-${dateStr}`] = entry.hours; }); }); setLocalEntries(initial); setHasUnsavedChanges(false); } }, [timesheet]); // Derived calendar data const { days, daysArray, isEditable } = useMemo(() => { if (!timesheet) return { days: 0, daysArray: [], isEditable: false }; const daysCount = getDaysInMonth(new Date(timesheet.year, timesheet.month - 1)); const arr = Array.from({ length: daysCount }, (_, i) => { const date = new Date(timesheet.year, timesheet.month - 1, i + 1); return { dateStr: format(date, "yyyy-MM-dd"), dayNum: i + 1, isWeekendDay: isWeekend(date), dayName: format(date, "EE", { locale: fr }) }; }); return { days: daysCount, daysArray: arr, isEditable: timesheet.status === "draft" }; }, [timesheet]); const handleCellClick = (lineId: number, dateStr: string) => { if (!isEditable) return; const key = `${lineId}-${dateStr}`; const current = localEntries[key] || 0; // Cycle: 0 -> 0.5 -> 1 -> 0 let next = 0.5; if (current === 0.5) next = 1; if (current === 1) next = 0; setLocalEntries(prev => ({ ...prev, [key]: next })); setHasUnsavedChanges(true); }; const mutateFnRef = useRef(upsertEntries.mutate); mutateFnRef.current = upsertEntries.mutate; const handleSave = useCallback(() => { if (!timesheetId) return; 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); if (hours > 0) { entriesToSave.push({ timesheetLineId: parseInt(lineIdStr), date: fullDate, hours }); } }); mutateFnRef.current({ timesheetId, data: { entries: entriesToSave } }, { onSuccess: () => { toast({ title: "Modifications sauvegardées" }); setHasUnsavedChanges(false); queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) }); }, onError: () => { toast({ title: "Erreur de sauvegarde", variant: "destructive" }); } }); }, [timesheetId, localEntries, toast, queryClient]); const handleSubmit = () => { if (hasUnsavedChanges) { toast({ title: "Veuillez sauvegarder avant de soumettre", variant: "destructive" }); return; } 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 handleAddLine = (projectId: number) => { createLine.mutate({ timesheetId, data: { projectId } }, { onSuccess: () => { toast({ title: "Ligne ajoutée" }); setIsAddingLine(false); queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) }); }, onError: (err: any) => { toast({ title: "Erreur", description: err.message, variant: "destructive" }); } }); }; const handleDeleteLine = (lineId: number) => { if (confirm("Supprimer cette ligne et toutes ses saisies ?")) { deleteLine.mutate({ timesheetId, lineId }, { onSuccess: () => { toast({ title: "Ligne supprimée" }); queryClient.invalidateQueries({ queryKey: getGetTimesheetQueryKey(timesheetId) }); } }); } }; if (isLoading) { return
; } if (!timesheet) return
CRA introuvable
; // Calculate totals const rowTotals: Record = {}; const colTotals: Record = {}; let grandTotal = 0; timesheet.lines?.forEach(line => { let rowSum = 0; daysArray.forEach(({ dateStr }) => { const hours = localEntries[`${line.id}-${dateStr}`] || 0; rowSum += hours; colTotals[dateStr] = (colTotals[dateStr] || 0) + hours; }); rowTotals[line.id] = rowSum; grandTotal += rowSum; }); return (

{formatMonthYear(timesheet.year, timesheet.month)}

{STATUS_LABELS[timesheet.status]}

Collaborateur: {timesheet.collaborator}

{isEditable && ( <> )} {timesheet.status === "submitted" && ( En attente de validation )} {timesheet.status === "validated" && ( CRA Validé )}
{/* Actions bar */} {isEditable && (
Total facturable: {grandTotal}h
)} {/* The Grid */}
{daysArray.map(day => ( ))} {!timesheet.lines || timesheet.lines.length === 0 ? ( ) : ( timesheet.lines.map(line => ( {daysArray.map(day => { const key = `${line.id}-${day.dateStr}`; const val = localEntries[key] || 0; return ( ); })} )) )} {daysArray.map(day => ( ))}
Projet
{day.dayName} {day.dayNum}
Total
Aucun projet ajouté. Cliquez sur "Ajouter un projet" pour commencer.
{line.projectName}
{line.projectCode} {line.client ? `• ${line.client}` : ''}
{isEditable && ( )}
handleCellClick(line.id, day.dateStr)} className={cn( "p-0 text-center border-r transition-colors select-none", day.isWeekendDay ? "bg-muted/30" : "", isEditable ? "cursor-pointer hover:bg-primary/10" : "", val > 0 ? "bg-primary/5 text-primary font-medium" : "" )} >
{val === 0 ? - : val}
{rowTotals[line.id] > 0 ? ( {rowTotals[line.id]}h ) : "-"}
Total du jour {colTotals[day.dateStr] > 0 ? colTotals[day.dateStr] : ""} {grandTotal}h
Ajouter un projet
); }