// ECO-PANNEAU.FR - _react/admin/_admin_factures.jsx window.pano_AdminInvoicesTab = ({ data, refreshData, showToast, setActiveTab, openLocalModal, openLocalDialog, closeCurrentLayer, activeModal, activeDialog, targetId, adminOpts, toggleDashboardPin }) => { const { useState, useMemo, useEffect } = React; // SÉCURITÉ ANTI-FUITE DE MÉMOIRE : Utilisation du Hook Global Zéro-Dette const { isMounted, safeFetch } = window.pano_useSafeFetch(); // 1. - Routage Zéro-Dette et modales const { activeModal: urlModal, activeDialog: urlDialog, openDialog, closeCurrentLayer: urlCloseLayer } = window.pano_useUrlModal(); const routerActiveDialog = activeDialog || urlDialog; const routerCloseLayer = closeCurrentLayer || urlCloseLayer; const routerOpenDialog = openLocalDialog || openDialog; // 2. - États Locaux et Pagination Serveur const [localInvoices, setLocalInvoices] = useState(data.invoices || []); const [hasMoreInvoices, setHasMoreInvoices] = useState((data.invoices || []).length >= 50); const [isLoadingMore, setIsLoadingMore] = useState(false); const [downloadedInvoices, setDownloadedInvoices] = useState(() => { try { return JSON.parse(localStorage.getItem('pano_admin_downloaded_invoices') || '[]'); } catch(e) { if (window.pano_logFallback) window.pano_logFallback("Erreur de parsing du localStorage (pano_admin_downloaded_invoices)."); return []; } }); const [isSaving, setIsSaving] = useState(false); const [refundTarget, setRefundTarget] = useState(null); const [previewConfig, setPreviewConfig] = useState(null); const [archiveConfig, setArchiveConfig] = useState(null); // États de filtrage const [tempClient, setTempClient] = useState(''); const [tempKeyword, setTempKeyword] = useState(''); const [appliedClient, setAppliedClient] = useState(''); useEffect(() => { setLocalInvoices(data.invoices || []); setHasMoreInvoices((data.invoices || []).length >= 50); }, [data.invoices]); // Synchronisation multi-onglets pour la pastille "Non lu" useEffect(() => { const handleUpdate = () => { try { setDownloadedInvoices(JSON.parse(localStorage.getItem('pano_admin_downloaded_invoices') || '[]')); } catch(e) {} }; window.addEventListener('admin_invoice_downloaded', handleUpdate); return () => window.removeEventListener('admin_invoice_downloaded', handleUpdate); }, []); // 3. - Icônes et Composants const { DownloadIcon, RefreshCwIcon, FileTextIcon, LoaderIcon, CheckCircleIcon, ArchiveIcon, PinIcon, EyeIcon, InfoIcon, AlertTriangleIcon, UserIcon } = window.pano_getIcons(); const { Modal, Button, CardGrid, DataCard, FormInput, EmptySearch, UniversalViewer, ArchiveGeneratorModal, SearchBar, PaginationFooter } = window.pano_getComponents(); const clients = data.clients || []; // 4. - Filtrage Dynamique et Pagination Client const invoicesForSearch = useMemo(() => { if (!appliedClient) return localInvoices; const selectedClientObj = clients.find(c => c.id === appliedClient); return localInvoices.filter(inv => { if (inv.clientName === appliedClient) return true; if (selectedClientObj && typeof inv.clientName === 'string') { if (inv.clientName.includes(selectedClientObj.name)) return true; } return false; }); }, [localInvoices, appliedClient, clients]); const { searchQuery, setSearchQuery, visibleCount, setVisibleCount, filteredData: filteredInvoices } = window.pano_useSearchAndPagination(invoicesForSearch, (inv, q) => { const n = window.pano_normalizeString; const client = clients.find(c => c.id === inv.clientName); const clientNameStr = n(client?.name || inv.clientName); const refStr = n(inv.invoice_ref || inv.invoiceNumber); const typeStr = n(inv.type); const amountStr = n(inv.amount); const dateStr = window.pano_formatDate ? window.pano_formatDate(inv.created_at) : new Date(String(inv.created_at || '').replace(' ', 'T')).toLocaleDateString('fr-FR'); const panNameStr = n(inv.panneauName); return refStr.includes(q) || clientNameStr.includes(q) || typeStr.includes(q) || amountStr.includes(q) || panNameStr.includes(q) || n(dateStr).includes(q); }); // 5. - Actions métier const handleLoadMoreInvoices = async () => { const d = await safeFetch(`sync/more&type=invoices&offset=${localInvoices.length}&limit=50`, { method: 'GET', silent: true, setLoading: setIsLoadingMore }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit si l'onglet a été fermé if (d && d.status === 'success') { const newInvoices = d.data.invoices || []; setLocalInvoices(prev => { const combined = [...prev, ...newInvoices]; // Dédoublonnage par sécurité return Array.from(new Map(combined.map(item => [item.id, item])).values()); }); setHasMoreInvoices(newInvoices.length === 50); } }; const handleDownload = (invRef) => { if (!downloadedInvoices.includes(invRef)) { const newArr = [...downloadedInvoices, invRef]; setDownloadedInvoices(newArr); localStorage.setItem('pano_admin_downloaded_invoices', JSON.stringify(newArr)); window.dispatchEvent(new CustomEvent('admin_invoice_downloaded')); } // Sécurité SPA : On ouvre un nouvel onglet pour éviter de tuer React si le serveur renvoie une erreur JSON window.open(`${window.pano_CONFIG.apiBaseUrl}invoices/download&ref=${invRef}`, '_blank'); }; const handleRefund = async (e) => { e.preventDefault(); const amount = parseFloat(e.target.amount.value); const reason = e.target.reason.value; const mode = e.target.mode.value; if (amount <= 0 || amount > refundTarget.maxRefundable) return showToast("Montant invalide", "error"); const invRef = refundTarget.invoice.invoice_ref || refundTarget.invoice.invoiceNumber; const d = await safeFetch('invoices/refund', { body: { invoice_ref: invRef, amount, mode, reason }, setLoading: setIsSaving, successMessage: "Remboursement ou avoir traité avec succès !" }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit post-requête if (d) { routerCloseLayer(); refreshData(); } }; const targetInvoice = routerActiveDialog === 'invoice_details' && routerDialogId ? localInvoices.find(i => (i.invoice_ref || i.invoiceNumber) === routerDialogId) : null; // CLASSIFICATION : URGENCE > FAVORIS > RESTE const pinnedIds = adminOpts?.invoices || []; const attentionInvoices = []; const pinnedInvoices = []; const otherInvoices = []; filteredInvoices.forEach(inv => { const invRef = inv.invoice_ref || inv.invoiceNumber; const isUnread = !downloadedInvoices.includes(invRef); const isPinned = pinnedIds.includes(invRef); const isArchive = String(inv.id).startsWith('arc_'); const needsAttention = isUnread && !isArchive; if (needsAttention) { attentionInvoices.push(inv); } else if (isPinned) { pinnedInvoices.push(inv); } else { otherInvoices.push(inv); } }); const displayedOtherInvoices = otherInvoices.slice(0, visibleCount); // 7. - Rendu des cartes (Factures) const renderInvoiceCard = (inv, keyPrefix) => { const client = clients.find(c => c.id === inv.clientName); const invRef = inv.invoice_ref || inv.invoiceNumber; const isArchive = String(inv.id).startsWith('arc_'); const isUnread = !downloadedInvoices.includes(invRef) && !isArchive; const isPinned = pinnedIds.includes(invRef); const isAvoir = inv.type.includes('Avoir') || parseFloat(inv.amount) < 0; let maxRefundable = 0; if (!isAvoir && !isArchive) { const relatedAvoirs = localInvoices.filter(r => (r.type.includes('Avoir') || parseFloat(r.amount) < 0) && (r.type.includes(invRef)) ); const totalRefunded = relatedAvoirs.reduce((sum, r) => sum + Math.abs(parseFloat(r.amount)), 0); maxRefundable = parseFloat((parseFloat(inv.amount) - totalRefunded).toFixed(2)); } return (
{ e.stopPropagation(); toggleDashboardPin && toggleDashboardPin('invoices', invRef, e); }} className={`absolute top-2 right-2 z-20 p-1 cursor-pointer transition-all duration-200 ${isPinned ? 'text-amber-500 opacity-100 hover:scale-110' : 'text-slate-300 opacity-0 group-hover:opacity-100 hover:text-slate-500 hover:scale-110'}`} style={{ transform: 'rotate(30deg)' }} title={isPinned ? "Désépingler" : "Épingler au tableau de bord"} >
{isUnread && Nouvelle} {isArchive && Archive légale}
Date {window.pano_formatDate ? window.pano_formatDate(inv.created_at) : new Date(String(inv.created_at || '').replace(' ', 'T')).toLocaleDateString('fr-FR')}
Montant {inv.amount} €
{/* BLOC CENTRAL : Le nom du Panneau est mis en avant */}
{inv.panneauName && ( <> Projet concerné {inv.panneauName} )} {(client?.name || inv.clientName) && (
{client?.name || inv.clientName}
)} {inv.type.replace(/\s*\(PI:[^)]+\)/, '')} (Réf. {invRef})
{/* ACTIONS : Masquées tant que la facture n'est pas téléchargée */}
{!isUnread && ( <> {!isArchive &&
); }; // 8. - Rendu UI return (

Factures et avoirs

Retrouvez l'historique de tous les paiements et abonnements.

{invoicesForSearch.length > 0 && SearchBar && (
)} {/* CORRECTION SÉCURITÉ UI : Masquage conditionnel des boutons d'export */} {localInvoices.length > 0 && (
)}
{localInvoices.length === 0 ? (
{FileTextIcon && }

Aucune facture émise ni archivée.

) : filteredInvoices.length === 0 ? ( EmptySearch && ) : ( <> {/* A. PRIORITÉ (FACTURES NON TÉLÉCHARGÉES) */} {attentionInvoices.length > 0 && (

Factures non téléchargées

{attentionInvoices.map((inv, i) => renderInvoiceCard(inv, `att_${i}`))}
)} {/* B. FAVORIS */} {pinnedInvoices.length > 0 && (

Favoris (Épinglés)

{pinnedInvoices.map((inv, i) => renderInvoiceCard(inv, `pin_${i}`))}
)} {/* C. LE RESTE */} {otherInvoices.length > 0 && (

Historique complet

{displayedOtherInvoices.slice(0, visibleCount).map((inv, i) => renderInvoiceCard(inv, `oth_${i}`))} {PaginationFooter && ( )} {hasMoreInvoices && visibleCount >= otherInvoices.length && (
)}
)} )} {/* Modales contextuelles */} {routerActiveDialog === 'archive_config' && archiveConfig && ArchiveGeneratorModal && ( )} {routerActiveDialog === 'refund' && refundTarget && Modal && ( ( <> )} >
* Champs obligatoires
Règle stricte : Aucun remboursement n'est jamais accepté sauf en cas d'erreur de facturation.
Facture initiale : {refundTarget.invoice.amount} € ({refundTarget.clientName})
{parseFloat(refundTarget.invoice.amount) > refundTarget.maxRefundable && ( Attention : Des avoirs partiels ont déjà été émis pour cette facture.
Reste remboursable : {refundTarget.maxRefundable} €
)}

Générez une facture d'avoir (partielle ou totale) pour annuler comptablement cette transaction.

{FormInput && }
{FormInput && }
)} {routerActiveDialog === 'invoice_details' && targetInvoice && Modal && ( ( )} >

Date d'émission

{window.pano_formatDate ? window.pano_formatDate(targetInvoice.created_at) : targetInvoice.created_at}

Montant TTC

{targetInvoice.amount} €

{targetInvoice.panneauName && (

Projet concerné

{targetInvoice.panneauName}

)}

Objet / Description

{targetInvoice.type.replace(/\s*\(PI:[^)]+\)/, '')}

En cas de question sur cette facturation, vous pouvez contacter le support en ouvrant une conversation.

)} {previewConfig && UniversalViewer && ( setPreviewConfig(null)} /> )}
); }; /* EOF ========== [_react/admin/_admin_factures.jsx] */