// 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 (
{inv.type}
{`+ ${pendingPanelsExtra} ${pendingPanelsExtra > 1 ? 'autres panneaux' : 'autre panneau'}`}
{`+ ${pendingLogisticsExtra} ${pendingLogisticsExtra > 1 ? 'autres commandes' : 'autre commande'}`}
{`+ ${attentionThreadsExtra} ${attentionThreadsExtra > 1 ? 'autres conversations' : 'autre conversation'}`}
{p.name || 'Projet sans nom'}
{`+ ${pendingPanelsExtra} ${pendingPanelsExtra > 1 ? 'autres panneaux' : 'autre panneau'}`}
{p.name}
Logistique: {currentStatus === 'Expédié' ? `Non réceptionné (> ${limitShippingForce}j)` : currentStatus}
{`+ ${pendingLogisticsExtra} ${pendingLogisticsExtra > 1 ? 'autres commandes' : 'autre commande'}`}
{t.id === 'MODERATION_QUEUE' ? 'Modération requise' : 'Nouveau message'}
{t.title}
{`+ ${attentionThreadsExtra} ${attentionThreadsExtra > 1 ? 'autres conversations' : 'autre conversation'}`}
Aucune interaction récente.
)}Vue d'ensemble de la plateforme
Utilisez les flèches pour réorganiser les grandes sections de votre tableau de bord selon vos priorités.