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, ShieldCheck, RefreshCw } from "lucide-react"; // Helper : télécharge un PDF annoté BAP depuis son URL de stockage // Nommage : AAAA-MM-JJ - Fournisseur - N°Facture.pdf function downloadBapPdf( pdfUrl: string, supplierName?: string, invoiceNumber?: string | null, validatedAt?: Date | string | null ) { const fallback = pdfUrl.split('/').pop() || 'BAP.pdf'; const downloadUrl = `/api/download-bap?pdfPath=${encodeURIComponent(pdfUrl)}`; const datePart = validatedAt ? new Date(validatedAt).toLocaleDateString('fr-CA') // AAAA-MM-JJ : new Date().toLocaleDateString('fr-CA'); const supplierPart = (supplierName || '').replace(/[^a-zA-Z0-9à-ÿ \-]/g, '').trim(); const numberPart = (invoiceNumber || '').replace(/[^a-zA-Z0-9\-]/g, '').trim(); const parts = [datePart, supplierPart, numberPart].filter(Boolean); const filename = parts.length > 0 ? `${parts.join(' - ')}.pdf` : fallback; const a = document.createElement('a'); a.href = downloadUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); } 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); // Map invoiceId -> pdfUrl après validation BAP (pour bouton téléchargement) const [bapPdfUrls, setBapPdfUrls] = useState>({}); 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 upsertLearningMutation = trpc.learnings.upsert.useMutation(); const reprocessMutation = trpc.invoices.reprocessSelected.useMutation({ onSuccess: (data) => { if (data.errors > 0) { toast.warning(`${data.processed} facture(s) retraitée(s), ${data.errors} erreur(s).`, { duration: 5000 }); } else { toast.success(`${data.processed} facture(s) retraitée(s) avec succès !`, { duration: 4000 }); } setSelectedIds([]); utils.invoices.list.invalidate(); }, onError: (error) => { toast.error(error.message || "Erreur lors du retraitement"); }, }); const validateBAPBulkMutation = trpc.invoices.validateBAPBulk.useMutation({ onSuccess: (data) => { if (data.processed === 0) { toast.info("Aucune facture éligible à valider en BAP."); } else { toast.success(`${data.processed} facture(s) validée(s) BAP avec succès !`, { duration: 4000 }); const pdfUrls = (data.results as any[]).filter(r => r.pdfUrl).map(r => r.pdfUrl as string); if (pdfUrls.length > 0) { pdfUrls.forEach(url => window.open(url, '_blank')); } } utils.invoices.list.invalidate(); }, onError: (error) => { toast.error(error.message || "Erreur lors de la validation BAP en masse"); }, }); const validateBAPMutation = trpc.invoices.validateBAP.useMutation({ onSuccess: (data) => { // Stocker le pdfUrl pour le bouton téléchargement if (data.invoiceId && data.pdfUrl) { setBapPdfUrls(prev => ({ ...prev, [data.invoiceId as number]: data.pdfUrl as string })); } if (data.exportMode === 'browser' && data.pdfUrl) { toast.success("Facture validée BAP ! Ouverture du PDF annoté...", { duration: 3000 }); window.open(data.pdfUrl, '_blank'); } else if (data.exportMode === 'folder' && data.exportPath) { toast.success(`Facture validée BAP ! PDF enregistré dans : ${data.exportPath}`, { duration: 6000 }); } else { 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 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é
{bapPdfUrls[invoice.id] && ( )}
) : ( )}
); })}
) : (

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