// 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 (
Retrouvez l'historique de tous les paiements et abonnements.
Aucune facture émise ni archivée.
Date d'émission
{window.pano_formatDate ? window.pano_formatDate(targetInvoice.created_at) : targetInvoice.created_at}
Montant TTC
{targetInvoice.amount} €
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.