import { useState } from "react"; import DashboardLayout from "@/components/DashboardLayout"; import { useAuth } from "@/_core/hooks/useAuth"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { trpc } from "@/lib/trpc"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, Filter, LayoutList, LayoutGrid, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"; import * as XLSX from 'xlsx'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { toast } from "sonner"; import { useLocation } from "wouter"; type SortField = "invoiceDate" | "createdAt"; type SortDir = "asc" | "desc"; export default function Invoices() { const [, setLocation] = useLocation(); const [searchQuery, setSearchQuery] = useState(""); const [compactMode, setCompactMode] = useState(true); const [sortField, setSortField] = useState("createdAt"); const [sortDir, setSortDir] = useState("desc"); const [selectedIds, setSelectedIds] = useState([]); const [statusFilter, setStatusFilter] = useState("all"); const [recipientFilter, setRecipientFilter] = useState("all"); const [subscriptionFilter, setSubscriptionFilter] = useState("all"); // all | yes | no const [entityFilter, setEntityFilter] = useState("all"); // all | santinova | itinova const [ventilationFilter, setVentilationFilter] = useState("all"); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [invoiceToDelete, setInvoiceToDelete] = useState(null); const [addDialogOpen, setAddDialogOpen] = useState(false); const [addDialogType, setAddDialogType] = useState<"service" | "ventilation" | null>(null); const [newItemName, setNewItemName] = useState(""); const [pendingInvoiceId, setPendingInvoiceId] = useState(null); const [textDialogOpen, setTextDialogOpen] = useState(false); const [selectedInvoiceText, setSelectedInvoiceText] = useState(null); const { user } = useAuth(); const isAdmin = user?.role === 'admin'; const { data: invoices, isLoading } = trpc.invoices.list.useQuery(); const { data: departments } = trpc.departments.getByUser.useQuery(); const { data: allocations } = trpc.accountingAllocations.getByUser.useQuery(); const { data: allUsers } = trpc.admin.getAllUsers.useQuery(undefined, { enabled: isAdmin }); const utils = trpc.useUtils(); const exportExcelMutation = trpc.sftp.exportToExcel.useMutation({ onSuccess: (data) => { // Generate Excel file const worksheet = XLSX.utils.json_to_sheet(data.invoices.map(inv => ({ 'Fournisseur': inv.supplierName, 'Destinataire': (inv as any).recipientName, 'N° Facture': inv.invoiceNumber, 'Date': inv.invoiceDate, 'N° Bon de livraison': inv.deliveryNoteNumber, 'N° Commande': inv.orderNumber, 'Montant': inv.totalAmount, 'Score': inv.qualityScore, 'Statut export': inv.exportStatus, 'Exporté le': inv.exportedAt, 'Créé le': inv.createdAt, }))); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'Factures'); // Download Excel file XLSX.writeFile(workbook, `factures_${new Date().toISOString().split('T')[0]}.xlsx`); toast.success(`${data.invoices.length} facture(s) export\u00e9e(s) en Excel`); setSelectedIds([]); }, onError: (error) => { toast.error(error.message || "Erreur lors de l'export Excel"); }, }); const exportMutation = trpc.sftp.exportToPdf.useMutation({ onSuccess: (data) => { toast.success( `${data.copiedCount} facture(s) exportée(s) avec succès vers:\n${data.exportFolder}`, { duration: 5000 } ); setSelectedIds([]); utils.invoices.list.invalidate(); }, onError: (error) => { toast.error(error.message || "Erreur lors de l'export"); }, }); const deleteMutation = trpc.invoices.delete.useMutation({ onSuccess: () => { toast.success("Facture supprimée avec succès"); utils.invoices.list.invalidate(); setDeleteDialogOpen(false); setInvoiceToDelete(null); }, onError: (error) => { toast.error(error.message || "Erreur lors de la suppression"); }, }); const upsertLearningMutation = trpc.learnings.upsert.useMutation(); const updateFieldMutation = trpc.invoices.update.useMutation({ onSuccess: () => { toast.success("Facture mise à jour"); utils.invoices.list.invalidate(); }, onError: (error) => { toast.error(error.message || "Erreur lors de la mise à jour"); }, }); const createDepartmentMutation = trpc.departments.create.useMutation({ onSuccess: (newDept) => { toast.success("Service ajouté avec succès"); utils.departments.getByUser.invalidate(); // Update the invoice with the new department if (pendingInvoiceId) { updateFieldMutation.mutate({ id: pendingInvoiceId, data: { serviceConcerne: newDept.name }, }); } setAddDialogOpen(false); setNewItemName(""); setPendingInvoiceId(null); }, onError: (error) => { toast.error(error.message || "Erreur lors de l'ajout"); }, }); const createAllocationMutation = trpc.accountingAllocations.create.useMutation({ onSuccess: (newAlloc) => { toast.success("Ventilation ajoutée avec succès"); utils.accountingAllocations.getByUser.invalidate(); // Update the invoice with the new allocation if (pendingInvoiceId) { updateFieldMutation.mutate({ id: pendingInvoiceId, data: { ventilationComptable: newAlloc.name }, }); } setAddDialogOpen(false); setNewItemName(""); setPendingInvoiceId(null); }, onError: (error) => { toast.error(error.message || "Erreur lors de l'ajout"); }, }); const handleAddNewItem = () => { if (!newItemName.trim()) { toast.error("Le nom ne peut pas être vide"); return; } if (addDialogType === "service") { createDepartmentMutation.mutate({ name: newItemName.trim() }); } else if (addDialogType === "ventilation") { createAllocationMutation.mutate({ name: newItemName.trim() }); } }; // Collect unique recipients for the filter dropdown const uniqueRecipients = Array.from( new Set( (invoices || []).map(inv => (inv as any).recipientName).filter(Boolean) ) ).sort(); const uniqueVentilations = Array.from( new Set( (invoices || []).map(inv => (inv as any).ventilationComptable).filter(Boolean) ) ).sort(); const handleSort = (field: SortField) => { if (sortField === field) { setSortDir(d => d === "asc" ? "desc" : "asc"); } else { setSortField(field); setSortDir("desc"); } }; const SortIcon = ({ field }: { field: SortField }) => { if (sortField !== field) return ; return sortDir === "asc" ? : ; }; const filteredInvoices = invoices?.filter((inv) => { // Filter by search query if (searchQuery) { const query = searchQuery.toLowerCase(); const matchesSearch = inv.supplierName?.toLowerCase().includes(query) || inv.invoiceNumber?.toLowerCase().includes(query) || (inv as any).recipientName?.toLowerCase().includes(query); if (!matchesSearch) return false; } // Filter by export status if (statusFilter !== "all") { if (statusFilter === "exported" && inv.exportStatus !== "exported") return false; if (statusFilter === "not_exported" && inv.exportStatus !== "not_exported") return false; if (statusFilter === "export_error" && inv.exportStatus !== "export_error") return false; } // Filter by recipient if (recipientFilter !== "all") { if (recipientFilter === "__empty__") { if ((inv as any).recipientName) return false; } else { if ((inv as any).recipientName !== recipientFilter) return false; } } // Filter by subscription if (subscriptionFilter !== "all") { const isSub = (inv as any).isSubscription === 1 || (inv as any).isSubscription === true; if (subscriptionFilter === "yes" && !isSub) return false; if (subscriptionFilter === "no" && isSub) return false; } // Filter by entity (SANTINOVA = serviceConcerne === 'DSI SANTINOVA', ITINOVA = autre) if (entityFilter !== "all") { const service = ((inv as any).serviceConcerne || "").trim().toUpperCase(); if (entityFilter === "santinova" && service !== "DSI SANTINOVA") return false; if (entityFilter === "itinova" && service === "DSI SANTINOVA") return false; } // Filter by ventilation comptable if (ventilationFilter !== "all") { if (ventilationFilter === "__empty__") { if ((inv as any).ventilationComptable) return false; } else { if ((inv as any).ventilationComptable !== ventilationFilter) return false; } } return true; }); const statusCounts = { all: invoices?.length || 0, exported: invoices?.filter(inv => inv.exportStatus === "exported").length || 0, not_exported: invoices?.filter(inv => inv.exportStatus === "not_exported").length || 0, export_error: invoices?.filter(inv => inv.exportStatus === "export_error").length || 0, }; // Old filter logic (to be removed) const _oldFilteredInvoices = invoices?.filter((inv) => { if (!searchQuery) return true; const query = searchQuery.toLowerCase(); return ( inv.supplierName?.toLowerCase().includes(query) || inv.invoiceNumber?.toLowerCase().includes(query) ); }); const handleSelectAll = (checked: boolean) => { if (checked) { // Select all invoices (no restriction) const allIds = (filteredInvoices || []).map(inv => inv.id); setSelectedIds(allIds); } else { setSelectedIds([]); } }; const handleSelectOne = (id: number, checked: boolean) => { if (checked) { setSelectedIds([...selectedIds, id]); } else { setSelectedIds(selectedIds.filter(selectedId => selectedId !== id)); } }; const handleExport = () => { if (selectedIds.length === 0) { toast.error("Veuillez s\u00e9lectionner au moins une facture"); return; } exportMutation.mutate({ invoiceIds: selectedIds }); }; const handleExportExcel = () => { if (selectedIds.length === 0) { toast.error("Veuillez s\u00e9lectionner au moins une facture"); return; } exportExcelMutation.mutate({ invoiceIds: selectedIds }); }; const getExportStatusBadge = (status: string) => { switch (status) { case "exported": return Exporté; case "not_exported": return Non exporté; case "export_error": return Erreur export; default: return {status}; } }; const getQualityBadge = (score: number | null) => { if (score === null) return -; if (score === 100) return {score}%; if (score >= 80) return {score}%; return {score}%; }; const isEligibleForExport = (invoice: any) => { return (invoice.qualityScore || 0) === 100; }; const hasOnlyEligibleSelected = selectedIds.length > 0 && selectedIds.every(id => { const invoice = invoices?.find(inv => inv.id === id); return invoice && isEligibleForExport(invoice); }); const sortedInvoices = [...(filteredInvoices || [])].sort((a, b) => { let aVal: number, bVal: number; if (sortField === "invoiceDate") { aVal = a.invoiceDate ? new Date(a.invoiceDate).getTime() : 0; bVal = b.invoiceDate ? new Date(b.invoiceDate).getTime() : 0; } else { aVal = a.createdAt ? new Date(a.createdAt as unknown as string).getTime() : 0; bVal = b.createdAt ? new Date(b.createdAt as unknown as string).getTime() : 0; } return sortDir === "asc" ? aVal - bVal : bVal - aVal; }); const allEligibleSelected = (sortedInvoices.length || 0) > 0 && sortedInvoices.filter(inv => isEligibleForExport(inv)).every(inv => selectedIds.includes(inv.id)); return (

Factures

Gérez toutes vos factures importées

{/* ===== CARTOUCHE FILTRES ===== */}
{/* Ligne 1 : Recherche + affichage compact/détail */}
setSearchQuery(e.target.value)} className="pl-10 bg-white border-blue-200 focus:border-blue-400" />
{/* Ligne 2 : Filtres destinataire, abonnement, entité, ventilation */}
{/* Ligne 3 : Boutons statut export + tri */}
Trier :
{/* ===== FIN CARTOUCHE ===== */} {/* Table */} {isLoading ? (
Chargement...
) : sortedInvoices.length > 0 ? (
Fournisseur {!compactMode && Destinataire} N° Facture handleSort("invoiceDate")} > Date facture handleSort("createdAt")} > Date réception Montant {!compactMode && Service} {!compactMode && Abonnement} Score Statut Actions {sortedInvoices.map((invoice) => { return ( handleSelectOne(invoice.id, checked as boolean)} /> {!compactMode && {(invoice as any).recipientName || "-"}} {invoice.invoiceNumber || "-"} {invoice.invoiceDate ? new Date(invoice.invoiceDate).toLocaleDateString("fr-FR") : "-"} {invoice.createdAt ? new Date(invoice.createdAt as unknown as string).toLocaleDateString("fr-FR") : "-"} {invoice.totalAmount ? `${parseFloat(invoice.totalAmount as string).toFixed(2)} €` : "-"} {!compactMode && } {!compactMode && } {getQualityBadge(invoice.qualityScore)} {getExportStatusBadge(invoice.exportStatus || "not_exported")}
); })}
) : (

Aucune facture trouvée

{searchQuery ? "Essayez une autre recherche" : "Commencez par importer un fichier PDF"}

)}
{/* Info message */} {filteredInvoices && filteredInvoices.some(inv => (inv.qualityScore || 0) < 100) && (

Note : Seules les factures avec un score de qualité de 100% peuvent être exportées. Les factures avec un score inférieur sont grisées et ne peuvent pas être sélectionnées.

)}
{/* Delete Confirmation Dialog */} Êtes-vous sûr de vouloir supprimer cette facture ? Cette action est irréversible. La facture sera définitivement supprimée de la base de données. setInvoiceToDelete(null)}>Annuler { if (invoiceToDelete) { deleteMutation.mutate({ id: invoiceToDelete }); } }} className="bg-red-600 hover:bg-red-700" > Supprimer {/* Add New Item Dialog */} Ajouter {addDialogType === "service" ? "un nouveau service" : "une nouvelle ventilation"} Entrez le nom {addDialogType === "service" ? "du service" : "de la ventilation comptable"} à ajouter.
setNewItemName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleAddNewItem(); } }} autoFocus />
{/* Extracted Text Dialog */} Texte extrait de la facture Texte complet extrait du PDF lors de l'import
{selectedInvoiceText}
); }