// ECO-PANNEAU.FR - _react/admin/_admin_dashboard.jsx window.pano_AdminDashboardTab = ({ data, refreshData, adminOpts, updateAdminOpts, toggleDashboardPin, setManagingPanneau, setValidationErrors, setPreviewPanneau }) => { const { useState, useEffect } = React; // ZÉRO-DETTE : Utilisation de notre Hook abstrait ! const { isMounted, safeFetch } = window.pano_useSafeFetch(); // CORRECTION ZÉRO-DETTE PERFORMANCE : Extraction centralisée des fonctions du routeur const urlModal = window.pano_useUrlModal ? window.pano_useUrlModal() : {}; const { openModal, openDialog, closeCurrentLayer, openChat, activeModal, activeDialog, targetId, dialogId } = urlModal; const [isSavingOrder, setIsSavingOrder] = useState(false); const { ActivityIcon, UsersIcon, BuildingIcon, EyeIcon, MessageSquareIcon, MessageCircleIcon, TerminalIcon, ArchiveIcon, FileDigitIcon, AlertTriangleIcon, PackageIcon, PinIcon, ChevronUpIcon, ChevronDownIcon, SlidersHorizontalIcon, FileTextIcon, MailIcon, EditIcon, QrCodeIcon, KeyRoundIcon, ShieldAlertIcon, ShieldIcon } = window.pano_getIcons(); const { StatCard, Button, IconBadge, NotificationBadge, Modal, ConfirmModal, CardGrid, DataCard, TextLogo, StatusBadge, AdminPanelCard, AdminLogisticsCard, AdminMessageCard, AdminClientCard } = window.pano_getComponents(); const [confirmDialog, setConfirmDialog] = useState(null); // CORRECTION : Retrait du bloc "maintenance" de l'ordre par défaut const defaultOrder = ['pinned', 'actions', 'stats', 'chats']; const currentOrder = adminOpts?.dashboard_order || defaultOrder; const currentActionsMode = adminOpts?.actions_mode || 'mini'; const [tempOrder, setTempOrder] = useState(currentOrder); const [tempActionsMode, setTempActionsMode] = useState(currentActionsMode); useEffect(() => { setTempOrder(currentOrder); setTempActionsMode(currentActionsMode); }, [JSON.stringify(currentOrder), currentActionsMode]); const blockNames = { pinned: "Favoris (Épinglés)", actions: "À traiter en priorité", stats: "Vue d'ensemble de la plateforme", chats: "Conversations récentes" }; const moveBlock = (index, dir) => { const newOrder = [...tempOrder]; if (dir === 'up' && index > 0) { [newOrder[index-1], newOrder[index]] = [newOrder[index], newOrder[index-1]]; } else if (dir === 'down' && index < newOrder.length - 1) { [newOrder[index+1], newOrder[index]] = [newOrder[index], newOrder[index+1]]; } setTempOrder(newOrder); }; const saveOrder = async (e) => { setIsSavingOrder(true); if (updateAdminOpts) { await updateAdminOpts({ ...adminOpts, dashboard_order: tempOrder, actions_mode: tempActionsMode }); } if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit setIsSavingOrder(false); if (closeCurrentLayer) closeCurrentLayer(e); }; const clients = data.clients || []; const panneaux = data.panneaux || []; const interactions = data.interactions || []; const regularPanneaux = panneaux.filter(p => p.id !== 'demo-panneau' && p.status !== 'Brouillon'); const activePanneaux = panneaux.filter(p => p.status === 'Actif' && p.id !== 'demo-panneau'); const draftPanneaux = panneaux.filter(p => p.status === 'Brouillon' && p.id !== 'demo-panneau'); const mrr = activePanneaux.filter(p => p.offerType === 'rental').reduce((sum, p) => sum + (p.currentRate || 0), 0); const threads = window.pano_buildAdminChatThreads ? window.pano_buildAdminChatThreads(interactions, panneaux, clients) : []; const checkPanelViolations = (p) => { const blacklist = (data.settings?.blacklist || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean); const greylist = (data.settings?.greylist || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean); let blackCount = 0; let greyCount = 0; const textToSearch = [p.name, p.location, p.description, p.maitreOuvrage].filter(Boolean).join(' ').toLowerCase(); const detected = { black: [], grey: [] }; const containsWord = (text, word) => { const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`\\b${escapedWord}\\b`, 'iu'); return regex.test(text); }; blacklist.forEach(word => { if(containsWord(textToSearch, word)) { blackCount++; detected.black.push(word); } }); greylist.forEach(word => { if(containsWord(textToSearch, word)) { greyCount++; detected.grey.push(word); } }); return { isViolation: blackCount > 0 || greyCount > 2, detected }; }; // CORRECTION : Prise en compte de la violation sémantique pour la remonter en "Action requise" sur le Dashboard const pendingPanels = regularPanneaux.filter(p => { const { isViolation } = checkPanelViolations(p); return p.status === 'Attente validation' || (!p.admin_seen && p.status === 'Actif') || isViolation; }); const limitShippingForce = parseInt(data.settings?.limit_shipping_force || 90, 10); const pendingLogistics = regularPanneaux.filter(p => { if (p.physicalPanels <= 0) return false; const currentStatus = p.shipping_status || 'En attente de validation'; if (['En attente de validation', 'Maquette refusée', 'En attente de commande au fournisseur', "En attente d'impression"].includes(currentStatus)) return true; if (currentStatus === 'Expédié' && p.updated_at) { const updateTime = new Date(p.updated_at.replace(' ', 'T')).getTime(); const daysSince = (Date.now() - updateTime) / (1000 * 3600 * 24); return daysSince > limitShippingForce; } return false; }); const attentionThreads = threads.filter(t => t.unread > 0 || t.id === 'MODERATION_QUEUE'); const totalActionsRequises = pendingPanels.length + pendingLogistics.length + attentionThreads.length; const dashboardThreads = [...threads].filter(t => t.id !== 'MODERATION_QUEUE').slice(0, 5); const unreadMessagesCount = interactions.filter(m => m.isAlert === 2 || (!m.resolved && m.authorType !== 'Admin' && m.authorType !== 'Systeme' && m.authorType !== 'System') ).length; const unreadMap = {}; threads.forEach(t => { if (t.unread > 0 && t.type === 'support') { const cuid = t.id.replace('SUPPORT_', ''); if (!unreadMap[cuid]) unreadMap[cuid] = { support: 0 }; unreadMap[cuid].support += t.unread; } }); const renderPinnedBlock = () => { const pinnedPanelsIds = adminOpts?.panels || []; const pinnedDeliveriesIds = adminOpts?.deliveries || []; const pinnedInvoicesIds = adminOpts?.invoices || []; const pinnedClientsIds = adminOpts?.clients || []; const pinnedThreadsIds = adminOpts?.threads || []; const pinnedPanels = (data.panneaux || []).filter(p => pinnedPanelsIds.includes(p.id)); const pinnedDeliveries = (data.panneaux || []).filter(p => pinnedDeliveriesIds.includes(p.id)); const pinnedInvoices = (data.invoices || []).filter(i => pinnedInvoicesIds.includes(i.invoice_ref || i.invoiceNumber)); const pinnedClients = (data.clients || []).filter(c => pinnedClientsIds.includes(c.id)); const pinnedChats = threads.filter(t => pinnedThreadsIds.includes(t.id)); const totalPinned = pinnedPanels.length + pinnedDeliveries.length + pinnedInvoices.length + pinnedClients.length + pinnedChats.length; if (totalPinned === 0) return null; return (

Favoris Administrateur

{pinnedPanels.map(p => { const { isViolation, detected } = checkPanelViolations(p); return ( 0} isViolation={isViolation} detected={detected} refreshData={refreshData} setManagingPanneau={setManagingPanneau} setValidationErrors={setValidationErrors} openModal={openModal} openDialog={openDialog} closeCurrentLayer={closeCurrentLayer} openChat={openChat} activeModal={activeModal} activeDialog={activeDialog} targetId={targetId} dialogId={dialogId} /> ); })} {pinnedDeliveries.map(c => ( ))} {AdminClientCard && pinnedClients.map(c => ( ))} {pinnedInvoices.map(inv => { const ref = inv.invoice_ref || inv.invoiceNumber; return ( urlModal.changeTab('factures')} className="cursor-pointer hover:border-amber-300 transition group p-4 min-w-0">

{inv.amount} €

{inv.type}

); })} {pinnedChats.map(t => ( openChat(id, false)} /> ))}
); }; const renderActionsBlock = () => { if (totalActionsRequises === 0) return null; const pendingPanelsExtra = pendingPanels.length > 4 ? pendingPanels.length - 4 : 0; const pendingLogisticsExtra = pendingLogistics.length > 4 ? pendingLogistics.length - 4 : 0; const attentionThreadsExtra = attentionThreads.length > 4 ? attentionThreads.length - 4 : 0; if (currentActionsMode === 'full') { return (

À traiter en priorité

{pendingPanels.length > 0 && (

Panneaux à valider

{pendingPanels.slice(0, 4).map(p => { const { isViolation, detected } = checkPanelViolations(p); return ( 0} isViolation={isViolation} detected={detected} refreshData={refreshData} setManagingPanneau={setManagingPanneau} setValidationErrors={setValidationErrors} openModal={openModal} openDialog={openDialog} closeCurrentLayer={closeCurrentLayer} openChat={openChat} activeModal={activeModal} activeDialog={activeDialog} targetId={targetId} dialogId={dialogId} /> ); })} {pendingPanelsExtra > 0 && ( urlModal.changeTab('panneaux')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">

{`+ ${pendingPanelsExtra} ${pendingPanelsExtra > 1 ? 'autres panneaux' : 'autre panneau'}`}

)}
)} {pendingLogistics.length > 0 && (

Logistique

{pendingLogistics.slice(0, 4).map(p => ( ))} {pendingLogisticsExtra > 0 && ( urlModal.changeTab('logistique')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">

{`+ ${pendingLogisticsExtra} ${pendingLogisticsExtra > 1 ? 'autres commandes' : 'autre commande'}`}

)}
)} {attentionThreads.length > 0 && (

Messages non lus

{attentionThreads.slice(0, 4).map(t => ( openChat(id, false)} /> ))} {attentionThreadsExtra > 0 && ( urlModal.changeTab('messages')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">

{`+ ${attentionThreadsExtra} ${attentionThreadsExtra > 1 ? 'autres conversations' : 'autre conversation'}`}

)}
)}
); } return (

À traiter en priorité

{pendingPanels.slice(0, 4).map(p => ( urlModal.changeTab('panneaux')} className="cursor-pointer group hover:scale-[1.02] transition-transform min-w-0">

{p.name || 'Projet sans nom'}

{p.status === 'Attente validation' ? 'Validation requise' : 'Contrôle requis'}
))} {pendingPanelsExtra > 0 && ( urlModal.changeTab('panneaux')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">

{`+ ${pendingPanelsExtra} ${pendingPanelsExtra > 1 ? 'autres panneaux' : 'autre panneau'}`}

)} {pendingLogistics.slice(0, 4).map(p => { const currentStatus = p.shipping_status || 'En attente de validation'; const isRefused = currentStatus === 'Maquette refusée'; return ( urlModal.changeTab('logistique')} className="cursor-pointer group hover:scale-[1.02] transition-transform min-w-0">

{p.name}

Logistique: {currentStatus === 'Expédié' ? `Non réceptionné (> ${limitShippingForce}j)` : currentStatus}

); })} {pendingLogisticsExtra > 0 && ( urlModal.changeTab('logistique')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">

{`+ ${pendingLogisticsExtra} ${pendingLogisticsExtra > 1 ? 'autres commandes' : 'autre commande'}`}

)} {attentionThreads.slice(0, 4).map(t => ( { if (e) { e.preventDefault(); e.stopPropagation(); } openChat(t.id, false); }} className="cursor-pointer group hover:scale-[1.02] transition-transform min-w-0">

{t.id === 'MODERATION_QUEUE' ? 'Modération requise' : 'Nouveau message'}

{t.title}

))} {attentionThreadsExtra > 0 && ( urlModal.changeTab('messages')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">

{`+ ${attentionThreadsExtra} ${attentionThreadsExtra > 1 ? 'autres conversations' : 'autre conversation'}`}

)}
); }; const renderStatsBlock = () => (

Vue d'ensemble de la plateforme

} variant="success" /> } variant="info" onClick={() => urlModal.changeTab('clients')} /> } variant="indigo" onClick={() => urlModal.changeTab('panneaux')} /> } variant="secondary" /> } variant="warning" />
); const renderChatsBlock = () => (

Conversations récentes

urlModal.changeTab('messages')} className="text-xs font-bold text-emerald-600 hover:underline cursor-pointer shrink-0 ml-2">Voir tout
{dashboardThreads.map(t => ( openChat(id, false)} /> ))} {dashboardThreads.length === 0 && (

Aucune interaction récente.

)}
); const blocksMap = { pinned: renderPinnedBlock(), actions: renderActionsBlock(), stats: renderStatsBlock(), chats: renderChatsBlock() }; // 7. - Rendu final return ( <>

Supervision

Vue d'ensemble de la plateforme .

{currentOrder.map(blockKey => { const blockContent = blocksMap[blockKey]; if (!blockContent) return null; if (blockKey === 'chats') { return
{blockContent}
; } return ( {blockContent} ); })}
{/* MODALE DE RÉORGANISATION */} {activeDialog === 'reorder_dashboard' && Modal && ( ( <> )} >

Utilisez les flèches pour réorganiser les grandes sections de votre tableau de bord selon vos priorités.

{tempOrder.map((blockKey, idx) => (
{blockNames[blockKey]}
))}

Format de la section "À traiter en priorité"

)} ); }; /* EOF ========== [_react/admin/_admin_dashboard.jsx] */