import { useState } from "react"; import DashboardLayout from "@/components/DashboardLayout"; 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 { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2 } from "lucide-react"; import * as XLSX from 'xlsx'; import { toast } from "sonner"; import { useLocation } from "wouter"; // Helper function to determine field color based on origin const getFieldColor = (invoice: any, fieldName: string): string => { try { const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : []; if (autoFilledFields.includes(fieldName)) { return "text-green-600 font-semibold"; // Auto-filled by rules } // If field has a value but is not auto-filled, it's manual if (invoice[fieldName]) { return "text-blue-600 font-semibold"; // Manually filled } } catch (error) { console.error("Error parsing autoFilledFields:", error); } return ""; // No color for empty fields }; export default function InvoicesBAP() { const [, setLocation] = useLocation(); const [searchQuery, setSearchQuery] = useState(""); const [selectedIds, setSelectedIds] = useState([]); const [statusFilter, setStatusFilter] = 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 { data: allInvoices, isLoading } = trpc.invoices.list.useQuery(); // Filter for BAP invoices only (Abonnement = NON, isSubscription = 0) const invoices = allInvoices?.filter(inv => inv.isSubscription === 0); const { data: departments } = trpc.departments.getByUser.useQuery(); const { data: allocations } = trpc.accountingAllocations.getByUser.useQuery(); 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, 'N\u00b0 Facture': inv.invoiceNumber, 'Date': inv.invoiceDate, 'N\u00b0 Bon de livraison': inv.deliveryNoteNumber, 'N\u00b0 Commande': inv.orderNumber, 'Montant': inv.totalAmount, 'Score': inv.qualityScore, 'Statut export': inv.exportStatus, 'Export\u00e9 le': inv.exportedAt, 'Cr\u00e9\u00e9 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 validateBAPMutation = trpc.invoices.validateBAP.useMutation({ onSuccess: () => { toast.success("Facture validée BAP avec succès !"); utils.invoices.list.invalidate(); }, onError: (error) => { toast.error(error.message || "Erreur lors de la validation BAP"); }, }); 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() }); } }; 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); 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; } 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 only exportable invoices const exportableIds = (filteredInvoices || []) .filter(inv => isEligibleForExport(inv)) .map(inv => inv.id); setSelectedIds(exportableIds); } 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) => { // Une facture BAP est exportable si : // 1. Score = 100% // 2. Type d'achat rempli // 3. Service concerné rempli // 4. Ventilation comptable remplie return ( (invoice.qualityScore || 0) === 100 && invoice.typeAchat && invoice.serviceConcerne && invoice.ventilationComptable ); }; const isEligibleForBAPValidation = (invoice: any) => { // Une facture peut être validée BAP si : // 1. Score = 100% // 2. Non encore exportée (exportStatus !== 'exported') // 3. Abonnement = NON (isSubscription = 0) // 4. Service concerné rempli // 5. Type d'achat rempli // 6. Ventilation comptable remplie return ( (invoice.qualityScore || 0) === 100 && invoice.exportStatus !== "exported" && invoice.isSubscription === 0 && invoice.serviceConcerne && invoice.typeAchat && invoice.ventilationComptable ); }; const getBAPValidationTooltip = (invoice: any): string => { const reasons: string[] = []; if ((invoice.qualityScore || 0) < 100) reasons.push("Score < 100%"); if (invoice.exportStatus === "exported") reasons.push("Déjà exportée"); if (invoice.isSubscription !== 0) reasons.push("Marquée comme abonnement"); if (!invoice.serviceConcerne) reasons.push("Service manquant"); if (!invoice.typeAchat) reasons.push("Type d'achat manquant"); if (!invoice.ventilationComptable) reasons.push("Ventilation manquante"); if (reasons.length === 0) return "Valider cette facture BAP"; return `Non éligible : ${reasons.join(", ")}`; }; const hasOnlyEligibleSelected = selectedIds.length > 0 && selectedIds.every(id => { const invoice = invoices?.find(inv => inv.id === id); return invoice && isEligibleForExport(invoice); }); const allEligibleSelected = (filteredInvoices?.length || 0) > 0 && (filteredInvoices || []).filter(inv => isEligibleForExport(inv)).every(inv => selectedIds.includes(inv.id)); return (

Factures BAP

Factures non-abonnement (Abonnement = NON)

{/* Search */}
setSearchQuery(e.target.value)} className="pl-10" />
{/* Status Filters */}
{/* Table */} {isLoading ? (
Chargement...
) : filteredInvoices && filteredInvoices.length > 0 ? (
Fournisseur N° Facture Date Montant Service Type achat Ventilation Abonnement Score Statut Actions {filteredInvoices.map((invoice) => { return ( handleSelectOne(invoice.id, checked as boolean)} disabled={!isEligibleForExport(invoice)} title={!isEligibleForExport(invoice) ? "Facture non exportable (score < 100% ou champs manquants)" : ""} /> {invoice.invoiceNumber || "-"} {invoice.invoiceDate ? new Date(invoice.invoiceDate).toLocaleDateString("fr-FR") : "-"} {invoice.totalAmount ? `${parseFloat(invoice.totalAmount as string).toFixed(2)} €` : "-"}
{getQualityBadge(invoice.qualityScore)} {isEligibleForExport(invoice) && (
)}
{getExportStatusBadge(invoice.exportStatus || "not_exported")}
{invoice.bapValidated === 1 ? (
Validé
) : ( )}
); })}
) : (

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}
); }