/** * ========================================================================= * PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0 * Interface Administrateur - Onglet Base Clients * ========================================================================= */ window.AdminClientsTab = ({ data, refreshData, showToast, setActiveTab, openLocalModal, openLocalDialog, closeCurrentLayer, activeModal, activeDialog, targetId }) => { const { useState } = React; // FACTORISATION : Utilisation du Hook global de gestion des modales par URL const { activeDialog: urlDialog, openDialog, closeCurrentLayer: urlCloseLayer } = window.useUrlModal(); // Priorité aux props passées par le parent, sinon fallback sur le hook const routerActiveDialog = activeDialog || urlDialog; const routerCloseLayer = closeCurrentLayer || urlCloseLayer; const routerOpenDialog = openLocalDialog || openDialog; const [isSaving, setIsSaving] = useState(false); // Cibles des modales const [accessTarget, setAccessTarget] = useState(null); const [creditTarget, setCreditTarget] = useState(null); const [editClientTarget, setEditClientTarget] = useState(null); const [pwdRequestData, setPwdRequestData] = useState(null); const [confirmConfig, setConfirmConfig] = useState(null); // FACTORISATION : Récupération sécurisée et propre des icônes const { UsersIcon, KeyRoundIcon, ShieldAlertIcon, ShieldIcon, ZapIcon, MessageSquareIcon, Trash2Icon, EditIcon, MailIcon, CheckCircleIcon, LoaderIcon } = window.getIcons(); const Modal = window.Modal || (() => null); const ConfirmModal = window.ConfirmModal || (() => null); const PasswordPromptModal = window.PasswordPromptModal || (() => null); const Button = window.Button || (({children}) => ); const IconBadge = window.IconBadge || (() => null); const clients = data.clients || []; const sendAccessRequest = async (uid, mode) => { const d = await window.apiFetch('clients/request_access', { body: { uid, mode }, setLoading: setIsSaving, successMessage: "Demande d'accès envoyée avec succès." }); if (d) { routerCloseLayer(); refreshData(); } }; const revokeAccess = async (uid) => { setConfirmConfig({ title: "Révocation d'accès", message: "Êtes-vous sûr de vouloir révoquer l'autorisation d'accès technique pour ce client ?", confirmText: "Oui, révoquer", isDestructive: true, onConfirm: async () => { routerCloseLayer(); const d = await window.apiFetch('clients/revoke_access', { body: { uid }, setLoading: setIsSaving, successMessage: "Accès révoqué." }); if (d) refreshData(); } }); routerOpenDialog('confirm'); }; const impersonateClient = async (uid) => { const d = await window.apiFetch('auth/impersonate', { body: { uid }, setLoading: setIsSaving }); if (d) window.location.href = '?'; }; const deleteClient = (uid, clientName) => { setPwdRequestData({ title: `Suppression du compte : ${clientName}`, desc: (ATTENTION : Saisissez le mot de passe administrateur pour détruire définitivement le compte de {clientName} et toutes ses données associées. Cette action est irréversible.), onConfirm: async (pwd) => { const d = await window.apiFetch('clients/admin_delete', { body: { uid, pwd }, setLoading: setIsSaving, successMessage: "Client supprimé avec succès." }); if (d) { routerCloseLayer(); refreshData(); } } }); routerOpenDialog('pwd_request'); }; const handleAddCredit = async (e) => { e.preventDefault(); const amount = parseFloat(e.target.amount.value); const reason = e.target.reason.value; if (amount <= 0) return showToast("Montant invalide", "error"); const d = await window.apiFetch('invoices/credit', { body: { client_uid: creditTarget.client_uid, amount, reason }, setLoading: setIsSaving, successMessage: "Crédit ajouté avec succès !" }); if (d) { routerCloseLayer(); refreshData(); } }; const handleEditClientSubmit = async (e) => { e.preventDefault(); const discount = parseInt(e.target.discount.value) || 0; const discountDuration = e.target.discountDuration.value; const paymentStatus = e.target.paymentStatus.value; const d = await window.apiFetch('clients/update', { body: { uid: editClientTarget.id, discount, discountDuration, paymentStatus }, setLoading: setIsSaving, successMessage: "Client mis à jour avec succès." }); if (d) { routerCloseLayer(); refreshData(); } }; return (

Base clients

Gestion des comptes et prise de contrôle.

{clients.map(c => { const isImpersonable = c.admin_access_until && new Date(String(c.admin_access_until || '').replace(' ', 'T')) > new Date(); const isSuspended = c.paymentStatus === 'suspended'; // Calcul des statistiques du client const twelveMonthsAgo = new Date(); twelveMonthsAgo.setMonth(twelveMonthsAgo.getMonth() - 12); const clientInvoices = (data.invoices || []).filter(inv => inv.clientName === c.id); // On laisse passer les avoirs pour qu'ils soient déduits comptablement const revenue12m = clientInvoices .filter(inv => { const date = new Date(String(inv.created_at || '').replace(' ', 'T')); return date >= twelveMonthsAgo; }) .reduce((sum, inv) => sum + parseFloat(inv.amount || 0), 0); const clientPanels = (data.panneaux || []).filter(p => p.clientName === c.id); const activePanelsCount = clientPanels.filter(p => p.status === 'Actif').length; const draftPanelsCount = clientPanels.filter(p => p.status === 'Brouillon').length; return (

{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)}€`} )}
{/* Statistiques Client */}

Revenu (12m)

{revenue12m.toFixed(2)} €

Actifs

{activePanelsCount}

Brouillons

{draftPanelsCount}

{isSuspended ? 'Impayé (Suspendu)' : 'Actif'} {c.discount > 0 && Remise {c.discount}%}
{isImpersonable ? (
) : ( )}
); })} {clients.length === 0 && (

Aucun client enregistré.

)}
{/* MODALES LOCALES DÉLÉGUÉES (LAYER 2) */} {routerActiveDialog === 'access_request' && accessTarget && Modal && (
Comment souhaitez-vous envoyer la demande d'autorisation d'accès temporaire (24h) à ce client ?
)} {routerActiveDialog === 'credit' && creditTarget && Modal && (
* Champs obligatoires
Ajout d'un crédit / 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.
)} {routerActiveDialog === 'edit_client' && editClientTarget && Modal && (
* Champs obligatoires

Vous pouvez forcer manuellement le statut pour contourner un blocage Stripe.

)} {routerActiveDialog === 'pwd_request' && pwdRequestData && PasswordPromptModal && ( )} {routerActiveDialog === 'confirm' && confirmConfig && ConfirmModal && }
); }; /* EOF ========== [_react/_admin_clients.jsx] */