import React, { useState, useMemo } 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 { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility"; import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2, ShieldCheck, RefreshCw, FolderDown, ChevronDown, ChevronRight, ChevronsUpDown, CalendarDays, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; // Helper : télécharge un ZIP de plusieurs PDFs annotés BAP async function downloadBapZip( files: Array<{ pdfPath: string; filename: string }> ) { const response = await fetch('/api/download-bap-zip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ files }), }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error((err as any).error || 'Erreur lors de la génération du ZIP'); } const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const zipFilename = `BAP_export_${new Date().toLocaleDateString('fr-CA')}.zip`; a.download = zipFilename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 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 datePart = validatedAt ? new Date(validatedAt).toLocaleDateString('fr-CA') // AAAA-MM-JJ : new Date().toLocaleDateString('fr-CA'); const supplierPart = (supplierName || '').replace(/[^a-zA-Z0-9\u00e0-\u00ff \-]/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; // Passer le filename au serveur pour qu'il soit dans Content-Disposition const downloadUrl = `/api/download-bap?pdfPath=${encodeURIComponent(pdfUrl)}&filename=${encodeURIComponent(filename)}`; 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, useSearch } from "wouter"; import { buildBapDetailLocation, parseBapFilters } from "@/lib/bapNavigation"; // 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 queryString = useSearch(); const currentYear = new Date().getFullYear(); const initialFilters = parseBapFilters(queryString, String(currentYear)); const [searchQuery, setSearchQuery] = useState(initialFilters.searchQuery); const [selectedIds, setSelectedIds] = useState([]); const [statusFilter, setStatusFilter] = useState(initialFilters.statusFilter); 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 [isZipDownloading, setIsZipDownloading] = useState(false); // Accordéon : lignes dépliées const [expandedIds, setExpandedIds] = useState>(new Set()); const [allExpanded, setAllExpanded] = useState(false); // Filtre par période const [selectedYear, setSelectedYear] = useState(initialFilters.selectedYear); const [selectedMonth, setSelectedMonth] = useState(initialFilters.selectedMonth); // "all" ou "01".."12" // Tri const [sortField, setSortField] = useState<"invoiceDate" | "createdAt">(initialFilters.sortField); const [sortDir, setSortDir] = useState<"asc" | "desc">(initialFilters.sortDir); const openInvoiceDetail = (invoiceId: number) => { setLocation(buildBapDetailLocation(invoiceId, { searchQuery, statusFilter, selectedYear, selectedMonth, sortField, sortDir, })); }; const toggleRow = (id: number) => { setExpandedIds(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const toggleAllRows = () => { if (allExpanded) { setExpandedIds(new Set()); setAllExpanded(false); } else { setExpandedIds(new Set((filteredInvoices || []).map(inv => inv.id))); setAllExpanded(true); } }; 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); // IDs des factures déjà validées BAP (pour charger leurs pdfUrl depuis bapHistory) // Stabilisé avec useMemo pour éviter les re-renders infinis (anti-pattern tRPC) const validatedInvoiceIds = useMemo( () => (invoices || []).filter(inv => inv.bapValidated === 1).map(inv => inv.id), // eslint-disable-next-line react-hooks/exhaustive-deps [invoices?.map(i => i.id).join(',')] ); const { data: persistedBapPdfUrls } = trpc.invoices.getBapPdfUrls.useQuery( { invoiceIds: validatedInvoiceIds }, { enabled: validatedInvoiceIds.length > 0 } ); // Fusionner les pdfUrl persistés (depuis bapHistory) avec ceux en mémoire (validation en cours) const allBapPdfUrls = { ...(persistedBapPdfUrls || {}), ...bapPdfUrls }; 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 })); } // Afficher l'erreur SharePoint si présente if ((data as any).sharepointUploadStatus === 'error') { toast.error(`Erreur export SharePoint : ${(data as any).sharepointUploadError || 'Erreur inconnue'}`, { duration: 8000 }); } else 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 as any).sharepointUploadStatus === 'success' && (data as any).sharepointUploadPath) { toast.success(`Facture validée BAP ! PDF déposé dans SharePoint`, { duration: 6000 }); } 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 regenerateBapPdfMutation = trpc.invoices.regenerateBapPdf.useMutation({ onSuccess: (data, variables) => { if (data.pdfUrl) { setBapPdfUrls(prev => ({ ...prev, [variables.invoiceId]: data.pdfUrl as string })); toast.success('PDF BAP régénéré avec succès !'); downloadBapPdf(data.pdfUrl); } utils.invoices.list.invalidate(); utils.invoices.getBapPdfUrls.invalidate(); }, onError: (error) => { toast.error(error.message || 'Erreur lors de la régénération du PDF BAP'); }, }); const devalidateBAPMutation = trpc.invoices.devalidateBAP.useMutation({ onSuccess: (data) => { toast.success(`${data.processed} facture(s) dévalidée(s) BAP avec succès`); setSelectedIds([]); utils.invoices.list.invalidate(); }, onError: (error) => { toast.error(error.message || "Erreur lors de la dé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() }); } }; // Déclarée ici (avant filteredInvoices et statusCounts) pour éviter le hoisting error const isEligibleForBAPValidation = (invoice: any) => { return ( meetsBapQualityThreshold(invoice.qualityScore) && invoice.exportStatus !== "exported" && invoice.isSubscription === 0 && invoice.serviceConcerne && invoice.typeAchat && invoice.ventilationComptable ); }; // "À compléter" = BAP non validé ET au moins un champ obligatoire manquant const isToComplete = (invoice: any) => { return ( invoice.bapValidated !== 1 && (!invoice.serviceConcerne || !invoice.typeAchat || !invoice.ventilationComptable) ); }; 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 period (year + optional month) if (selectedYear !== "all") { const invDate = inv.invoiceDate ? new Date(inv.invoiceDate) : null; const fallbackDate = inv.createdAt ? new Date(inv.createdAt) : null; const dateToUse = invDate || fallbackDate; if (!dateToUse) return false; if (dateToUse.getFullYear() !== parseInt(selectedYear)) return false; if (selectedMonth !== "all" && (dateToUse.getMonth() + 1) !== parseInt(selectedMonth)) 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; if (statusFilter === "bap_validated" && inv.bapValidated !== 1) return false; if (statusFilter === "bap_pending" && inv.bapValidated === 1) return false; if (statusFilter === "to_complete" && !isToComplete(inv)) return false; } return true; }); // Factures filtrées par période uniquement (sans filtre de statut) pour les compteurs const invoicesInPeriod = invoices?.filter((inv) => { if (selectedYear === "all") return true; const invDate = inv.invoiceDate ? new Date(inv.invoiceDate) : null; const fallbackDate = inv.createdAt ? new Date(inv.createdAt) : null; const dateToUse = invDate || fallbackDate; if (!dateToUse) return false; if (dateToUse.getFullYear() !== parseInt(selectedYear)) return false; if (selectedMonth !== "all" && (dateToUse.getMonth() + 1) !== parseInt(selectedMonth)) return false; return true; }) || []; const statusCounts = { all: invoicesInPeriod.length, exported: invoicesInPeriod.filter(inv => inv.exportStatus === "exported").length, not_exported: invoicesInPeriod.filter(inv => inv.exportStatus === "not_exported").length, export_error: invoicesInPeriod.filter(inv => inv.exportStatus === "export_error").length, bap_validated: invoicesInPeriod.filter(inv => inv.bapValidated === 1).length, bap_pending: invoicesInPeriod.filter(inv => inv.bapValidated !== 1).length, to_complete: invoicesInPeriod.filter(inv => isToComplete(inv)).length, }; // 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 (meetsBapQualityThreshold(score)) return {score}%; if (score >= 80) return {score}%; return {score}%; }; const isEligibleForExport = (invoice: any) => { // Une facture BAP est exportable si : // 1. Score >= 90 % (seuil BAP) // 2. Type d'achat rempli // 3. Service concerné rempli // 4. Ventilation comptable remplie return ( meetsBapQualityThreshold(invoice.qualityScore) && invoice.typeAchat && invoice.serviceConcerne && invoice.ventilationComptable ); }; // isEligibleForBAPValidation est déclarée plus haut (avant filteredInvoices) const getBAPValidationTooltip = (invoice: any): string => { const reasons: string[] = []; if (!meetsBapQualityThreshold(invoice.qualityScore)) reasons.push(`Score < ${BAP_MIN_QUALITY_SCORE}%`); 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 handleSort = (field: "invoiceDate" | "createdAt") => { if (sortField === field) { setSortDir(d => d === "asc" ? "desc" : "asc"); } else { setSortField(field); setSortDir("desc"); } }; const SortIcon = ({ field }: { field: "invoiceDate" | "createdAt" }) => { if (sortField !== field) return ; return sortDir === "asc" ? : ; }; 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.every(inv => selectedIds.includes(inv.id)); return (
{/* En-tête : titre + sous-titre */}

Factures BAP

Factures non-abonnement (Abonnement = NON)

{/* ===== CARTOUCHE FILTRES + ACTIONS BAP ===== */}
{/* Ligne 1 : Boutons d'action + Recherche */}
{/* Recherche à droite */}
setSearchQuery(e.target.value)} className="pl-10 bg-white border-emerald-200 focus:border-emerald-400 h-8 text-sm" />
{/* Ligne 2 : Filtres période (année + mois) */}
Période :
{(selectedYear !== "all" || selectedMonth !== "all") && ( )}
{/* Ligne 3 : Boutons statut + tri */}
Trier :
{/* ===== FIN CARTOUCHE BAP ===== */} {/* Table */} {isLoading ? (
Chargement...
) : sortedInvoices.length > 0 ? (
{/* Bouton global +/- */} Fournisseur N° Facture handleSort("invoiceDate")} > Date facture handleSort("createdAt")} > Date réception Montant Score Statut Actions {sortedInvoices.map((invoice) => { const isExpanded = expandedIds.has(invoice.id); return ( {/* Bouton +/- individuel */} handleSelectOne(invoice.id, checked as boolean)} /> {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)} €` : "-"}
{getQualityBadge(invoice.qualityScore)} {isEligibleForExport(invoice) && (
)}
{getExportStatusBadge(invoice.exportStatus || "not_exported")}
{invoice.bapValidated === 1 ? (
Validé
{allBapPdfUrls[invoice.id] ? ( ) : ( )}
) : ( )}
{/* Ligne 2 : détails dépliables (Service, Type achat, Ventilation) */} {isExpanded && ( {/* col +/- vide */} {/* col checkbox vide */} {/* colspan 6 pour les 3 champs */}
{/* Service */}
Service :
{/* Type achat */}
Type achat :
{/* Ventilation */}
Ventilation :
)}
); })}
) : (

Aucune facture trouvée

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

)}
{/* fin space-y-4 */} {/* 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}
); }