diff --git a/client/src/pages/History.tsx b/client/src/pages/History.tsx index 5c80c7f..34f9c6c 100644 --- a/client/src/pages/History.tsx +++ b/client/src/pages/History.tsx @@ -1,5 +1,6 @@ +import { useState, useMemo } from "react"; import DashboardLayout from "@/components/DashboardLayout"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Table, TableBody, @@ -10,7 +11,19 @@ import { } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { trpc } from "@/lib/trpc"; -import { History as HistoryIcon, FileText, Trash2 } from "lucide-react"; +import { + History as HistoryIcon, + FileText, + Trash2, + Eye, + CheckCircle2, + XCircle, + AlertTriangle, + CalendarDays, + RotateCcw, + Package, + X, +} from "lucide-react"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { @@ -24,12 +37,53 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +type StatusFilter = "all" | "imported" | "not_imported" | "duplicates" | "errors"; + +const MONTHS = [ + { value: "1", label: "Janvier" }, + { value: "2", label: "Février" }, + { value: "3", label: "Mars" }, + { value: "4", label: "Avril" }, + { value: "5", label: "Mai" }, + { value: "6", label: "Juin" }, + { value: "7", label: "Juillet" }, + { value: "8", label: "Août" }, + { value: "9", label: "Septembre" }, + { value: "10", label: "Octobre" }, + { value: "11", label: "Novembre" }, + { value: "12", label: "Décembre" }, +]; + +const YEARS = Array.from({ length: 5 }, (_, i) => String(new Date().getFullYear() - i)); export default function History() { const { data: logs, isLoading } = trpc.importLogs.getByUser.useQuery(); const deleteAllMutation = trpc.importLogs.deleteAll.useMutation(); const utils = trpc.useUtils(); - + + // Filtres + const [statusFilter, setStatusFilter] = useState("all"); + const [yearFilter, setYearFilter] = useState(String(new Date().getFullYear())); + const [monthFilter, setMonthFilter] = useState("all"); + + // Dialog visualisation détails + const [detailDialogOpen, setDetailDialogOpen] = useState(false); + const [selectedLog, setSelectedLog] = useState<(typeof logs extends (infer T)[] | undefined ? T : never) | null>(null); + const handleDeleteAll = async () => { try { await deleteAllMutation.mutateAsync(); @@ -37,22 +91,65 @@ export default function History() { toast.success("Logs supprimés", { description: "Tous les logs d'import ont été supprimés avec succès.", }); - } catch (error) { + } catch { toast.error("Erreur", { description: "Impossible de supprimer les logs d'import.", }); } }; + // Filtrage par période + const periodFiltered = useMemo(() => { + if (!logs) return []; + return logs.filter((log) => { + const d = new Date(log.importedAt); + if (yearFilter !== "all" && d.getFullYear() !== Number(yearFilter)) return false; + if (monthFilter !== "all" && (d.getMonth() + 1) !== Number(monthFilter)) return false; + return true; + }); + }, [logs, yearFilter, monthFilter]); + + // Compteurs sur la période filtrée + const counts = useMemo(() => { + const totalImported = periodFiltered.reduce((s, l) => s + l.invoicesImported, 0); + const totalDuplicates = periodFiltered.reduce((s, l) => s + l.duplicatesIgnored, 0); + const totalErrors = periodFiltered.reduce((s, l) => s + l.errors, 0); + const totalDetected = periodFiltered.reduce((s, l) => s + l.totalInvoicesDetected, 0); + const totalNotImported = totalDuplicates + totalErrors; + return { totalImported, totalDuplicates, totalErrors, totalDetected, totalNotImported }; + }, [periodFiltered]); + + // Filtrage par statut + const filteredLogs = useMemo(() => { + return periodFiltered.filter((log) => { + if (statusFilter === "all") return true; + if (statusFilter === "imported") return log.invoicesImported > 0; + if (statusFilter === "not_imported") return log.duplicatesIgnored > 0 || log.errors > 0; + if (statusFilter === "duplicates") return log.duplicatesIgnored > 0; + if (statusFilter === "errors") return log.errors > 0; + return true; + }); + }, [periodFiltered, statusFilter]); + + const hasPeriodFilter = yearFilter !== "all" || monthFilter !== "all"; + + const filterButtons: { key: StatusFilter; label: string; count: number; color: string; activeColor: string }[] = [ + { key: "all", label: "Tous", count: periodFiltered.length, color: "bg-gray-100 text-gray-700 hover:bg-gray-200", activeColor: "bg-gray-700 text-white" }, + { key: "imported", label: "Importées", count: counts.totalImported, color: "bg-green-50 text-green-700 hover:bg-green-100 border border-green-200", activeColor: "bg-green-600 text-white" }, + { key: "not_imported", label: "Non importées", count: counts.totalNotImported, color: "bg-orange-50 text-orange-700 hover:bg-orange-100 border border-orange-200", activeColor: "bg-orange-500 text-white" }, + { key: "duplicates", label: "Doublons", count: counts.totalDuplicates, color: "bg-yellow-50 text-yellow-700 hover:bg-yellow-100 border border-yellow-200", activeColor: "bg-yellow-500 text-white" }, + { key: "errors", label: "Erreurs", count: counts.totalErrors, color: "bg-red-50 text-red-700 hover:bg-red-100 border border-red-200", activeColor: "bg-red-600 text-white" }, + ]; + return (
+ {/* En-tête */}

Historique des imports

Consultez l'historique de tous vos imports de factures

- {logs && logs.length > 0 && ( @@ -83,72 +180,309 @@ export default function History() { )}
+ {/* Compteurs */} +
+ + + +
+

{counts.totalImported}

+

Factures importées

+
+
+
+ + + +
+

{counts.totalNotImported}

+

Non importées

+
+
+
+ + + +
+

{counts.totalDuplicates}

+

Doublons ignorés

+
+
+
+ + + +
+

{counts.totalErrors}

+

Erreurs

+
+
+
+
+ + {/* Filtres */} +
+ {/* Filtres statut */} +
+ {filterButtons.map((btn) => ( + + ))} +
+ + {/* Séparateur */} +
+ + {/* Filtre Période */} +
+ + + + {hasPeriodFilter && ( + + )} +
+ + {/* Résumé */} + + {filteredLogs.length} import{filteredLogs.length > 1 ? "s" : ""} — {counts.totalDetected} facture{counts.totalDetected > 1 ? "s" : ""} détectée{counts.totalDetected > 1 ? "s" : ""} + +
+ + {/* Tableau */} - - Logs d'import - Détails de chaque import avec statistiques - - + {isLoading ? ( -
Chargement...
- ) : logs && logs.length > 0 ? ( +
+ +

Chargement...

+
+ ) : filteredLogs.length > 0 ? ( - - Fichier - Date - Détectées - Importées - Doublons - Erreurs + + Fichier importé + Date d'import + Détectées + Importées + Doublons + Erreurs + Détails - {logs.map((log) => ( - - {log.fileName} - + {filteredLogs.map((log) => ( + + +
+ + {log.fileName} +
+
+ {new Date(log.importedAt).toLocaleString("fr-FR")} - - {log.totalInvoicesDetected} - - - - {log.invoicesImported} + + + {log.totalInvoicesDetected} - + + {log.invoicesImported > 0 ? ( + + {log.invoicesImported} + + ) : ( + 0 + )} + + {log.duplicatesIgnored > 0 ? ( - + {log.duplicatesIgnored} ) : ( - )} - + {log.errors > 0 ? ( - + {log.errors} ) : ( - )} + + +
))}
) : ( -
+
-

Aucun historique d'import

+

Aucun import trouvé

+

Modifiez les filtres pour afficher plus de résultats

)}
+ + {/* Dialog visualisation détails */} + + + + + + Détails de l'import + + + {selectedLog && ( +
+ {/* Infos générales */} +
+
+

Fichier

+

{selectedLog.fileName}

+
+
+

Date d'import

+

{new Date(selectedLog.importedAt).toLocaleString("fr-FR")}

+
+
+ + {/* Statistiques */} +
+
+

{selectedLog.totalInvoicesDetected}

+

Détectées

+
+
+

{selectedLog.invoicesImported}

+

Importées

+
+
+

{selectedLog.duplicatesIgnored}

+

Doublons

+
+
+

{selectedLog.errors}

+

Erreurs

+
+
+ + {/* Détails doublons */} + {selectedLog.duplicateDetails && (() => { + try { + const details = JSON.parse(selectedLog.duplicateDetails); + if (Array.isArray(details) && details.length > 0) { + return ( +
+

+ + Doublons ignorés ({details.length}) +

+
+ {details.map((d: string | { supplier?: string; invoiceNumber?: string; reason?: string }, i: number) => ( +
+ {typeof d === "string" ? d : `${d.supplier || ""} — ${d.invoiceNumber || ""} ${d.reason ? `(${d.reason})` : ""}`} +
+ ))} +
+
+ ); + } + } catch { /* JSON invalide */ } + return null; + })()} + + {/* Détails erreurs */} + {selectedLog.errorDetails && (() => { + try { + const details = JSON.parse(selectedLog.errorDetails); + if (Array.isArray(details) && details.length > 0) { + return ( +
+

+ + Erreurs ({details.length}) +

+
+ {details.map((d: string | { message?: string; file?: string }, i: number) => ( +
+ {typeof d === "string" ? d : `${d.file ? `[${d.file}] ` : ""}${d.message || ""}`} +
+ ))} +
+
+ ); + } + } catch { /* JSON invalide */ } + return null; + })()} +
+ )} +
+
); }