// ECO-PANNEAU.FR - _react/admin/_admin_panneaux.jsx
// 1. - COMPOSANT CARTE ISOLÉ ET RÉUTILISABLE ADMINISTRATEUR
window.pano_AdminPanelCard = ({
panel: p, clients, isPinned, toggleDashboardPin, unreadSupport, needsControl, hasBeenSuspended, isViolation, detected,
setPreviewPanneau, refreshData, setManagingPanneau, setValidationErrors,
openModal, openLocalModal, openDialog, openLocalDialog, closeCurrentLayer, openChat, activeDialog, dialogId, data
}) => {
const { useState } = React;
const { isMounted, safeFetch } = window.pano_useSafeFetch();
const [isSaving, setIsSaving] = useState(false);
const [suspendReason, setSuspendReason] = useState("");
const [adminPassword, setAdminPassword] = useState("");
const [deletePassword, setDeletePassword] = useState("");
const [deleteAesKey, setDeleteAesKey] = useState("");
const [deleteReason, setDeleteReason] = useState("");
const { DataCard, IconBadge, StatusBadge, Button, NotificationBadge, Modal, FormInput } = window.pano_getComponents();
const { BuildingIcon, PinIcon, QrCodeIcon, EyeIcon, AlertTriangleIcon, CheckCircleIcon, MessageSquareIcon, UserIcon, LockIcon, Trash2Icon, PowerIcon } = window.pano_getIcons();
const handleOpenModal = (n, id, s) => {
if (openModal) openModal(n, id, s);
else if (openLocalModal) openLocalModal(n, id, s);
};
const handleOpenDialog = (n, id, s) => {
if (openDialog) openDialog(n, id, s);
else if (openLocalDialog) openLocalDialog(n, id, s);
};
const handleCloseLayer = () => {
if (closeCurrentLayer) closeCurrentLayer();
};
const handleOpenChat = (id, s) => {
if (openChat) openChat(id, s);
else {
const prev = window.history.state || { panoStack: [], level: 0, currentTab: 'dashboard' };
const newStack = (prev.panoStack || []).filter(l => l.type !== 'chat');
newStack.push({ type: 'chat', name: 'chat', targetId: id });
window.history.pushState({ ...prev, panoStack: newStack, level: newStack.length }, '', window.location.href);
window.dispatchEvent(new Event('pano_stack_sync'));
}
};
const owner = clients.find(c => c.id === (p.client_uid || p.clientName));
const supportThreadId = 'SUPPORT_' + (p.client_uid || p.clientName);
const hasDraft = p.draft_data && Object.keys(p.draft_data).length > 0;
const adminSettings = data?.settings || {};
const limitStrikes = parseInt(adminSettings.limit_strikes_suspend || 5, 10);
const suspendCount = p.suspend_count || 0;
const canHardDelete = suspendCount >= limitStrikes;
let cardVariant = 'default';
if (isViolation) cardVariant = 'danger';
else if (p.status === 'Attente validation') cardVariant = 'warning';
else if (p.status === 'Suspendu') cardVariant = 'warning';
const handleValidatePanel = async () => {
const d = await safeFetch('panneaux/mark_seen', {
body: { id: p.id },
setLoading: setIsSaving,
successMessage: "Panneau validé et activé."
});
if (!isMounted.current) return;
if (d && refreshData) refreshData();
};
const handleSuspendSubmit = async (e) => {
e.preventDefault();
if (!suspendReason.trim() || !adminPassword) return;
const d1 = await safeFetch('panneaux/status', {
body: { id: p.id, status: 'Suspendu', password: adminPassword, reason: suspendReason },
setLoading: setIsSaving
});
if (!isMounted.current) return;
if (d1) {
const message = `Le panneau "${p.name}" a été suspendu ou rejeté car il nécessite une correction.\n\nMotif :\n${suspendReason}\n\nVeuillez apporter les corrections nécessaires pour pouvoir le réactiver (statut "Actif").\n\nVous trouverez en pièce jointe de l'e-mail qui vient de vous être envoyé, un rappel de nos Conditions Générales de Vente (CGV).`;
await safeFetch('interactions', {
body: { panneauId: supportThreadId, detail: message, author: 'Admin', targetEmail: 'Client', type: 'message' }
});
if (!isMounted.current) return;
if (refreshData) refreshData();
handleCloseLayer();
setTimeout(() => {
if (isMounted.current) {
setSuspendReason("");
setAdminPassword("");
}
}, 300);
}
};
const handleDeleteSubmit = async (e) => {
e.preventDefault();
if (!deletePassword || !deleteAesKey || !deleteReason.trim()) return;
const needsDOE = p.status === 'Actif' || p.status === 'Suppression programmée' || p.status === 'Suspendu';
if (needsDOE) {
const d = await safeFetch('archives/doe', {
body: { id: p.id, admin_delete_reason: deleteReason, password: deletePassword, aes_key_confirm: deleteAesKey },
setLoading: setIsSaving,
successMessage: "Panneau clôturé (D.O.E généré) et client notifié."
});
if (!isMounted.current) return;
if (d) {
if (refreshData) refreshData();
handleCloseLayer();
setTimeout(() => {
if (isMounted.current) {
setDeletePassword(""); setDeleteAesKey(""); setDeleteReason("");
}
}, 300);
}
} else {
const d = await safeFetch('panneaux/delete', {
body: { id: p.id, force_immediate: true, password: deletePassword, aes_key_confirm: deleteAesKey, admin_delete_reason: deleteReason },
setLoading: setIsSaving,
successMessage: "Panneau supprimé définitivement et client notifié."
});
if (!isMounted.current) return;
if (d) {
if (refreshData) refreshData();
handleCloseLayer();
setTimeout(() => {
if (isMounted.current) {
setDeletePassword(""); setDeleteAesKey(""); setDeleteReason("");
}
}, 300);
}
}
};
return (
<>
{ e.stopPropagation(); toggleDashboardPin && toggleDashboardPin('panels', p.id, e); }}
className={`absolute top-2 right-2 z-20 p-1 cursor-pointer transition-all duration-200 ${isPinned ? 'text-purple-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"}
>
{p.name || 'Projet sans nom'}
{p.location &&
{p.location}
}
{owner ? owner.name : p.clientName}
{p.status === 'Actif' && hasDraft && (
Modifié
)}
{p.status !== 'Actif' && }
{needsControl ? (
) : (
{/* Modales Encapsulées */}
{activeDialog === 'suspend' && dialogId === p.id && Modal && (
(
<>
Annuler
{isSaving ? "Traitement..." : "Suspendre le panneau"}
>
)}
>
)}
{activeDialog === 'delete_panel' && dialogId === p.id && Modal && (
(
<>
Annuler
Clôturer / Détruire
>
)}
>
)}
{activeDialog === 'moderation' && dialogId === p.id && Modal && (
(
{ handleCloseLayer(); setTimeout(() => { handleOpenChat(supportThreadId, false); }, 100); }} className="w-full py-3 justify-center text-sm shadow-sm">Contacter le propriétaire
{ handleCloseLayer(); setTimeout(() => handleOpenDialog('suspend', p.id, false), 100); }} className="w-full py-3 justify-center text-sm shadow-md">Suspendre le panneau (Avertissement N°{suspendCount + 1})
Ignorer
)}
>
Mots sensibles détectés :
{detected?.black?.length > 0 &&
Liste noire : {detected.black.join(', ')}
}
{detected?.grey?.length > 0 &&
Liste grise : {detected.grey.join(', ')}
}
)}
>
);
};
// 2. - ONGLET PRINCIPAL DE GESTION PANNEAUX ADMIN
window.pano_AdminPanelsTab = ({ data, refreshData, setManagingPanneau, setValidationErrors, setPreviewPanneau, adminOpts, toggleDashboardPin }) => {
const { useMemo } = React;
const urlModal = window.pano_useUrlModal ? window.pano_useUrlModal() : {};
const { openModal, openDialog, closeCurrentLayer, openChat, activeDialog, dialogId } = urlModal;
const { Button, IconBadge, CardGrid, SearchBar, EmptySearch, PaginationFooter, AdminPanelCard } = window.pano_getComponents();
const { BuildingIcon, AlertTriangleIcon, PinIcon } = window.pano_getIcons();
// CORRECTION PERF 1 : Pré-calcul des listes de violations pour éviter le parsing à chaque panneau
const violationLists = useMemo(() => {
const black = (data.settings?.blacklist || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
const grey = (data.settings?.greylist || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
return { black, grey };
}, [data.settings?.blacklist, data.settings?.greylist]);
const checkPanelViolations = (p, lists) => {
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);
};
lists.black.forEach(word => { if(containsWord(textToSearch, word)) { blackCount++; detected.black.push(word); } });
lists.grey.forEach(word => { if(containsWord(textToSearch, word)) { greyCount++; detected.grey.push(word); } });
return { isViolation: blackCount > 0 || greyCount > 2, detected };
};
const panneaux = data.panneaux || [];
const clients = data.clients || [];
// CORRECTION PERF 2 : Création d'un dictionnaire O(1) pour la recherche des propriétaires
const clientsMap = useMemo(() => {
const map = {};
clients.forEach(c => { map[c.id] = c; });
return map;
}, [clients]);
const regularPanneaux = panneaux.filter(p => p.id !== 'demo-panneau' && p.status !== 'Brouillon');
const threads = useMemo(() => {
return window.pano_buildAdminChatThreads ? window.pano_buildAdminChatThreads(data.interactions || [], panneaux, clients) : [];
}, [data.interactions, panneaux, clients]);
const unreadMap = useMemo(() => {
const map = {};
threads.forEach(t => {
if (t.unread > 0 && t.type === 'support') {
const cuid = t.id.replace('SUPPORT_', '');
if (!map[cuid]) map[cuid] = { support: 0 };
map[cuid].support += t.unread;
}
});
return map;
}, [threads]);
const {
searchQuery, setSearchQuery,
visibleCount, setVisibleCount,
filteredData: filteredPanneaux
} = window.pano_useSearchAndPagination(regularPanneaux, (p, q) => {
const n = window.pano_normalizeString;
const owner = clientsMap[p.client_uid || p.clientName]; // O(1) Lookup instantané
const ownerSearchStr = n(owner ? [owner.name, owner.full_name, owner.fullName, owner.email].filter(Boolean).join(' ') : '');
return n(p.name).includes(q) ||
n(p.clientName).includes(q) ||
n(p.id).includes(q) ||
n(p.location).includes(q) ||
n(p.status).includes(q) ||
ownerSearchStr.includes(q);
});
const pinnedIds = adminOpts?.panels || [];
const attentionPanels = [];
const pinnedPanels = [];
const otherPanels = [];
filteredPanneaux.forEach(p => {
const { isViolation, detected } = checkPanelViolations(p, violationLists); // Utilisation de la liste pré-calculée
const needsControl = p.status === 'Attente validation' || (!p.admin_seen && p.status === 'Actif');
const needsAttention = needsControl || isViolation;
const isPinned = pinnedIds.includes(p.id);
const cardProps = {
panel: p, clients, isPinned, toggleDashboardPin,
unreadSupport: unreadMap[p.clientName]?.support || 0,
needsControl, hasBeenSuspended: (p.suspend_count || 0) > 0,
isViolation, detected, setPreviewPanneau, refreshData,
setManagingPanneau, setValidationErrors,
openModal, openDialog, closeCurrentLayer, openChat, activeDialog, dialogId,
data
};
if (needsAttention) attentionPanels.push(cardProps);
else if (isPinned) pinnedPanels.push(cardProps);
else otherPanels.push(cardProps);
});
const displayedOtherPanels = otherPanels.slice(0, visibleCount);
return (
Panneaux (Admin)
Modération et gestion de la base de données.
{regularPanneaux.length > 0 && SearchBar && (
)}
{regularPanneaux.length === 0 ? (
Aucun panneau dans la base
) : filteredPanneaux.length === 0 ? (
EmptySearch &&
) : (
<>
{attentionPanels.length > 0 && (
Action requise
{attentionPanels.map(props => )}
)}
{pinnedPanels.length > 0 && (
Favoris (Épinglés)
{pinnedPanels.map(props => )}
)}
{otherPanels.length > 0 && (
Autres panneaux
{otherPanels.length}
{displayedOtherPanels.map(props => )}
{PaginationFooter && (
)}
)}
>
)}
);
};
/* EOF ========== [_react/admin/_admin_panneaux.jsx] */