// ECO-PANNEAU.FR - _react/admin/_admin_clients.jsx // 1. - COMPOSANT CARTE ISOLÉ ET RÉUTILISABLE ADMINISTRATEUR (CLIENTS) window.pano_AdminClientCard = ({ client: c, data, isPinned, toggleDashboardPin, unreadSupport, refreshData, // PROPS DE ROUTAGE INJECTÉES PAR LE PARENT openDialog, closeCurrentLayer, openChat, activeDialog, dialogId }) => { const { useState } = React; // SÉCURITÉ ANTI-FUITE DE MÉMOIRE : Utilisation du Hook Global Zéro-Dette const { isMounted, safeFetch } = window.pano_useSafeFetch(); // CORRECTION ZÉRO-DETTE : Fallbacks de navigation robustes const handleOpenDialog = (n, id, s) => { if (openDialog) openDialog(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')); } }; // États encapsulés const [isSaving, setIsSaving] = useState(false); const [adminPassword, setAdminPassword] = useState(""); const [aesKeyConfirm, setAesKeyConfirm] = useState(""); const [confirmConfig, setConfirmConfig] = useState(null); const [creditAmount, setCreditAmount] = useState(""); const [creditReason, setCreditReason] = useState("Geste commercial / Annulation de dette"); const [discount, setDiscount] = useState(c.discount || 0); const [discountDuration, setDiscountDuration] = useState(c.discount_duration || '1'); const { UsersIcon, KeyRoundIcon, ShieldAlertIcon, ShieldIcon, ZapIcon, MessageSquareIcon, Trash2Icon, EditIcon, MailIcon, CheckCircleIcon, LoaderIcon, LockIcon, PinIcon } = window.pano_getIcons(); const { Modal, ConfirmModal, Button, IconBadge, NotificationBadge, DataCard, FormInput } = window.pano_getComponents(); // Calculs de statistiques par client isolés const clientPanels = (data?.panneaux || []).filter(p => p.clientName === c.id || p.client_uid === c.id); const activePanels = clientPanels.filter(p => p.status === 'Actif').length; const draftPanels = clientPanels.filter(p => p.status === 'Brouillon').length; let revenue12m = 0; const twelveMonthsAgoMs = new Date().setMonth(new Date().getMonth() - 12); (data?.invoices || []).forEach(inv => { if (inv.clientName === c.id) { const invTime = new Date(String(inv.created_at || '').replace(' ', 'T')).getTime(); if (invTime >= twelveMonthsAgoMs) { revenue12m += parseFloat(inv.amount || 0); } } }); const isImpersonable = c.admin_access_until && new Date(String(c.admin_access_until || '').replace(' ', 'T')) > new Date(); const isSuspended = c.paymentStatus === 'suspended'; const hasDebt = c.wallet_balance < 0; let cardVariant = 'default'; if (isSuspended || hasDebt) cardVariant = 'danger'; else if (unreadSupport > 0) cardVariant = 'warning'; // Actions métier localisées (SÉCURISÉES) const sendAccessRequest = async (mode) => { const d = await safeFetch('clients/request_access', { body: { uid: c.id, mode }, setLoading: setIsSaving, successMessage: "Demande d'accès envoyée avec succès." }); if (!isMounted.current) return; if (d) { handleCloseLayer(); if (refreshData) refreshData(); } }; const revokeAccess = async () => { setConfirmConfig({ title: "Révocation d'accès", message: "Êtes-vous sûr de vouloir révoquer l'autorisation d'accès du support technique pour ce client ?", confirmText: "Oui, révoquer", isDestructive: true, onConfirm: async () => { handleCloseLayer(); const d = await safeFetch('clients/revoke_access', { body: { uid: c.id }, setLoading: setIsSaving, successMessage: "Accès révoqué." }); if (!isMounted.current) return; if (d && refreshData) refreshData(); } }); handleOpenDialog('confirm_client_action', c.id, false); }; const impersonateClient = async () => { const d = await safeFetch('auth/impersonate', { body: { uid: c.id }, setLoading: setIsSaving }); if (!isMounted.current) return; if (d) window.location.href = '?'; }; const handleDeleteClientSubmit = async (e) => { e.preventDefault(); if (!adminPassword || !aesKeyConfirm) return; const d = await safeFetch('clients/admin_delete', { body: { uid: c.id, pwd: adminPassword, aes_key_confirm: aesKeyConfirm }, setLoading: setIsSaving, successMessage: "Client supprimé avec succès." }); if (!isMounted.current) return; if (d) { handleCloseLayer(); if (refreshData) refreshData(); setTimeout(() => { if (isMounted.current) { setAdminPassword(''); setAesKeyConfirm(''); } }, 300); } }; const handleAddCredit = async (e) => { e.preventDefault(); const amt = parseFloat(creditAmount); if (amt <= 0) { if (window.pano_showToast) window.pano_showToast("Montant invalide", "error"); return; } const d = await safeFetch('invoices/credit', { body: { client_uid: c.id, amount: amt, reason: creditReason }, setLoading: setIsSaving, successMessage: "Crédit ajouté avec succès !" }); if (!isMounted.current) return; if (d) { handleCloseLayer(); if (refreshData) refreshData(); } }; const handleEditClientSubmit = async (e) => { e.preventDefault(); if (!adminPassword) return; const d = await safeFetch('clients/update', { body: { uid: c.id, discount: parseInt(discount) || 0, discountDuration: discountDuration, pwd: adminPassword }, setLoading: setIsSaving, successMessage: "Remise mise à jour avec succès." }); if (!isMounted.current) return; if (d) { handleCloseLayer(); if (refreshData) refreshData(); setTimeout(() => { if (isMounted.current) setAdminPassword(''); }, 300); } }; return ( <>
{ e.stopPropagation(); toggleDashboardPin && toggleDashboardPin('clients', c.id, e); }} className={`absolute top-2 right-2 z-20 p-1 cursor-pointer transition-all duration-200 ${isPinned ? 'text-emerald-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"} >

{c.name}

{c.full_name &&

{c.full_name}

}

{c.email}

ID : {c.id.substring(0,8)}

{c.wallet_balance !== 0 && ( 0 ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}> {c.wallet_balance > 0 ? `Solde: +${c.wallet_balance.toFixed(2)}€` : `Dette: ${c.wallet_balance.toFixed(2)}€`} )}

Revenu (12m)

{revenue12m.toFixed(2)} €

Actifs

{activePanels}

Brouillons

{draftPanels}

{(isSuspended || c.discount > 0) && (
{isSuspended && ( Impayé (Suspendu) )} {c.discount > 0 && Remise {c.discount}%}
)}
{isImpersonable ? (
) : ( )}
{/* Modales Encapsulées Zéro-Dette */} {activeDialog === 'access_request' && dialogId === c.id && Modal && ( (
)} >
Comment souhaitez-vous envoyer la demande d'autorisation d'accès temporaire (24h) à ce client ?
)} {activeDialog === 'credit' && dialogId === c.id && Modal && ( ( <> )} >
* Champs obligatoires
Ajout d'un crédit ou Annulation de dette
Ce montant sera ajouté au solde du client. S'il a une dette (solde négatif ou statut impayé), cela permettra de l'annuler en tout ou partie.
setCreditAmount(e.target.value)} required className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm outline-none focus:border-emerald-500 font-bold min-w-0" placeholder="Ex: 50,00" />
setCreditReason(e.target.value)} required className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm outline-none focus:border-emerald-500 font-bold min-w-0" />
)} {activeDialog === 'edit_client' && dialogId === c.id && Modal && ( ( <> )} >
* Champs obligatoires
setDiscount(e.target.value)} className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm outline-none focus:border-emerald-500 font-bold min-w-0" />
} type="password" value={adminPassword} onChange={e => setAdminPassword(e.target.value)} placeholder="Mot de passe admin" required className="min-w-0 w-full" />
)} {activeDialog === 'delete_client' && dialogId === c.id && Modal && ( ( <> )} >

Vous êtes sur le point de détruire définitivement le compte de {c.name} et toutes ses données associées. Cette action est irréversible.

Sécurité requise

setAdminPassword(e.target.value)} placeholder="Mot de passe admin" required error className="min-w-0 w-full" /> setAesKeyConfirm(e.target.value)} placeholder="Clé AES_KEY_CONFIRM" required error className="min-w-0 w-full" />
)} {activeDialog === 'confirm_client_action' && dialogId === c.id && confirmConfig && ConfirmModal && ( { setConfirmConfig(null); handleCloseLayer(); }} /> )} ); }; // 2. - ONGLET PRINCIPAL DE GESTION CLIENTS ADMIN window.pano_AdminClientsTab = ({ data, refreshData, 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(); // CORRECTION ZÉRO-DETTE PERFORMANCE : Abonnement unique au routeur const urlModal = window.pano_useUrlModal ? window.pano_useUrlModal() : {}; const { openDialog, closeCurrentLayer, openChat, activeDialog, dialogId } = urlModal; const { SearchBar, EmptySearch, PaginationFooter, CardGrid, AdminClientCard } = window.pano_getComponents(); const { UsersIcon, AlertTriangleIcon, PinIcon, LoaderIcon, DownloadIcon } = window.pano_getIcons(); const [localClients, setLocalClients] = useState(data.clients || []); const [hasMoreClients, setHasMoreClients] = useState((data.clients || []).length >= 50); const [isLoadingMore, setIsLoadingMore] = useState(false); useEffect(() => { setLocalClients(data.clients || []); setHasMoreClients((data.clients || []).length >= 50); }, [data.clients]); const { searchQuery, setSearchQuery, visibleCount, setVisibleCount, filteredData: filteredClients } = window.pano_useSearchAndPagination(localClients, (c, q) => { const n = window.pano_normalizeString; const displayStatus = c.paymentStatus === 'suspended' ? 'impayé suspendu' : 'actif'; return n(c.name).includes(q) || n(c.full_name).includes(q) || n(c.email).includes(q) || n(c.id).includes(q) || n(displayStatus).includes(q); }); const unreadSupportMap = useMemo(() => { const map = {}; const threads = window.pano_buildAdminChatThreads ? window.pano_buildAdminChatThreads(data.interactions || [], data.panneaux || [], localClients) : []; threads.forEach(t => { if (t.unread > 0 && t.type === 'support') { const cuid = t.id.replace('SUPPORT_', ''); map[cuid] = (map[cuid] || 0) + t.unread; } }); return map; }, [data.interactions, data.panneaux, localClients]); const handleLoadMoreClients = async () => { const d = await safeFetch(`sync/more&type=clients&offset=${localClients.length}&limit=50`, { method: 'GET', silent: true, setLoading: setIsLoadingMore }); if (!isMounted.current) return; // SÉCURITÉ if (d && d.status === 'success') { const newClients = d.data.clients || []; setLocalClients(prev => { const combined = [...prev, ...newClients]; return Array.from(new Map(combined.map(item => [item.id, item])).values()); }); setHasMoreClients(newClients.length === 50); } }; const pinnedIds = adminOpts?.clients || []; const attentionClients = []; const pinnedClients = []; const otherClients = []; filteredClients.forEach(c => { const isSuspended = c.paymentStatus === 'suspended'; const hasDebt = c.wallet_balance < 0; const unreadSupport = unreadSupportMap[c.id] || 0; const isPinned = pinnedIds.includes(c.id); // Distribution des props du routeur const cardProps = { client: c, data, isPinned, toggleDashboardPin, unreadSupport, refreshData, openDialog, closeCurrentLayer, openChat, activeDialog, dialogId }; if (isSuspended || hasDebt || unreadSupport > 0) attentionClients.push(cardProps); else if (isPinned) pinnedClients.push(cardProps); else otherClients.push(cardProps); }); const displayedOtherClients = otherClients.slice(0, visibleCount); return (

Base clients

Gestion des comptes et prise de contrôle.

{localClients.length > 0 && SearchBar && (
)}
{localClients.length === 0 ? (

Aucun client enregistré.

) : filteredClients.length === 0 ? ( EmptySearch && ) : ( <> {/* 8.1 - Priorités */} {attentionClients.length > 0 && (

Action requise

{attentionClients.map(props => )}
)} {/* 8.2 - Favoris */} {pinnedClients.length > 0 && (

Favoris (Épinglés)

{pinnedClients.map(props => )}
)} {/* 8.3 - Autres */} {otherClients.length > 0 && (

{attentionClients.length > 0 ? 'Autres clients' : 'Tous les clients'} {otherClients.length}

{displayedOtherClients.map(props => )} {PaginationFooter && ( )} {hasMoreClients && visibleCount >= otherClients.length && (
)}
)} )}
); }; /* EOF ========== [_react/admin/_admin_clients.jsx] */