/** * ========================================================================= * PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0 * Interface Administrateur (Super Admin) * Gestion globale, Logistique, Tarification et Maintenance Système * ========================================================================= */ const { useState, useEffect } = React; const { Home, Users, Building, Settings, Activity, ShieldAlert, KeyRound, Database, RefreshCw, Save, LogOut, Package, Eye, MessageSquare, AlertTriangle, CheckCircle, Trash2, Download, Terminal, Loader, Zap, HardDrive, Shield, AlertOctagon, Archive, Power, Edit, FileText, MapPin } = window; window.AdminView = ({ data, refreshData, showToast }) => { const [activeTab, setActiveTab] = useState('dashboard'); const [isSaving, setIsSaving] = useState(false); // Initialisation des données de paramètres const [settingsData, setSettingsData] = useState({ ...data.settings, ...data.prices, simp_opt_description: data.settings?.simp_opt_description ?? '1', simp_opt_image: data.settings?.simp_opt_image ?? '1', simp_opt_theme: data.settings?.simp_opt_theme ?? '1', simp_opt_link: data.settings?.simp_opt_link ?? '1', simp_opt_emergency: data.settings?.simp_opt_emergency ?? '1', simp_opt_schedule: data.settings?.simp_opt_schedule ?? '1' }); const [hasTva, setHasTva] = useState(true); const [showTvaWarning, setShowTvaWarning] = useState(false); const [diagnostics, setDiagnostics] = useState(null); const [logs, setLogs] = useState(''); const [backups, setBackups] = useState([]); const [isLoadingBackups, setIsLoadingBackups] = useState(false); const [showAllBackups, setShowAllBackups] = useState(false); const [sysSummary, setSysSummary] = useState({ loaded: false, diagStatus: 'emerald', backupStatus: 'emerald', logStatus: 'emerald', php: '', ram: '', db: '', disk: '', backupsCount: 0, logDesc: '', backupDesc: '' }); const [modalView, setModalView] = useState(null); const [pwdRequest, setPwdRequest] = useState(null); const [managingChantier, setManagingChantier] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [editingEntity, setEditingEntity] = useState(null); const [draggedItem, setDraggedItem] = useState(null); // Nouveaux états pour le flux Logistique const [shippingModal, setShippingModal] = useState(null); const [trackingData, setTrackingData] = useState({ number: '', link: '' }); useEffect(() => { setSettingsData({ ...data.settings, ...data.prices, simp_opt_description: data.settings?.simp_opt_description ?? '1', simp_opt_image: data.settings?.simp_opt_image ?? '1', simp_opt_theme: data.settings?.simp_opt_theme ?? '1', simp_opt_link: data.settings?.simp_opt_link ?? '1', simp_opt_emergency: data.settings?.simp_opt_emergency ?? '1', simp_opt_schedule: data.settings?.simp_opt_schedule ?? '1' }); setHasTva(data.settings?.billing_has_tva !== '0'); }, [data]); const fetchTelemetry = async () => { try { const cacheBuster = '&_t=' + Date.now(); const [diagRes, backRes, logRes] = await Promise.all([ fetch(window.ECO_CONFIG.apiBaseUrl + 'system/diagnostics' + cacheBuster, { cache: 'no-store' }).then(r => r.json()), fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups' + cacheBuster, { cache: 'no-store' }).then(r => r.json()), fetch(window.ECO_CONFIG.apiBaseUrl + 'system/logs' + cacheBuster, { cache: 'no-store' }).then(r => r.json()) ]); if (diagRes.status === 'success') { const dbDetail = diagRes.data.find(d => d.id === 'db')?.detail || '0 ms'; const diskDetail = diagRes.data.find(d => d.id === 'disk')?.detail || '0 GB'; const dbVal = parseFloat(dbDetail); const diskVal = parseFloat(diskDetail); let diagStatus = 'emerald'; if (dbVal > 500 || diskVal < 1) diagStatus = 'red'; else if (dbVal > 200 || diskVal < 5) diagStatus = 'amber'; const backupsData = backRes.data || []; const backupsCount = backupsData.length; let backupStatus = 'emerald'; let backupDesc = `${backupsCount} archive(s)`; if (backupsCount === 0) { backupStatus = 'red'; backupDesc = "Alerte : Aucune sauvegarde"; } else { const latestBackup = backupsData.reduce((max, b) => b.date > max.date ? b : max, backupsData[0]); const hoursSinceLast = (Date.now() - latestBackup.date) / (1000 * 60 * 60); if (hoursSinceLast > 48) { backupStatus = 'red'; backupDesc = `Critique : Retard de ${Math.floor(hoursSinceLast/24)} jours`; } else if (hoursSinceLast > 24) { backupStatus = 'amber'; backupDesc = `Attention : Retard de ${Math.floor(hoursSinceLast)}h`; } else { backupStatus = 'emerald'; backupDesc = `À jour (${backupsCount} archives)`; } } const logsContent = logRes.data?.logs || ''; let logStatus = 'emerald'; let logDesc = 'Journal vierge (Sain)'; const lowerLogs = logsContent.toLowerCase(); if (lowerLogs.includes('fatal error') || lowerLogs.includes('exception')) { logStatus = 'red'; logDesc = 'Erreurs critiques détectées !'; } else if (logsContent.length > 5) { logStatus = 'amber'; logDesc = 'Avertissements enregistrés'; } setSysSummary({ loaded: true, diagStatus, backupStatus, logStatus, php: diagRes.data.find(d => d.id === 'php')?.detail || 'OK', ram: diagRes.data.find(d => d.id === 'ram')?.detail || 'OK', db: dbDetail, disk: diskDetail, backupsCount, logDesc, backupDesc }); } } catch(e) {} }; useEffect(() => { fetchTelemetry(); }, []); const clients = data.clients || []; const chantiers = data.chantiers || []; const activeChantiers = chantiers.filter(c => c.status === 'Actif' && c.id !== 'demo-chantier'); const mrr = activeChantiers.filter(c => c.offerType === 'rental').reduce((sum, c) => sum + (c.currentRate || 0), 0); const interactions = data.interactions || []; const totalUnread = interactions.filter(m => m.target === 'Admin' && !m.resolved).length; const navItems = [ { id: 'dashboard', icon: , label: 'Vue globale' }, { id: 'clients', icon: , label: 'Base clients' }, { id: 'chantiers', icon: , label: 'Tous les panneaux', badge: activeChantiers.length }, { id: 'logistique', icon: , label: 'Logistique', badge: chantiers.filter(c => c.physicalPanels > 0 && c.shipping_status !== 'Livré').length }, { id: 'invoices', icon: , label: 'Facturation globale' }, { id: 'messages', icon: , label: 'Messagerie et support', badge: totalUnread }, { id: 'settings', icon: , label: 'Système et tarifs' }, ]; const requestAccess = async (uid) => { if (!confirm("Voulez-vous vraiment envoyer un e-mail au client pour lui demander l'accès à son compte ?")) return; setIsSaving(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'clients/request_access', { method: 'POST', body: JSON.stringify({ uid }) }); if ((await res.json()).status === 'success') showToast("E-mail de demande d'accès envoyé au client.", "success"); else showToast("Erreur lors de la demande", "error"); } finally { setIsSaving(false); } }; const impersonateClient = async (uid) => { setIsSaving(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/impersonate', { method: 'POST', body: JSON.stringify({ uid }) }); if ((await res.json()).status === 'success') window.location.href = '?'; else showToast("Erreur d'accès", "error"); } finally { setIsSaving(false); } }; const deleteClient = (uid, clientName) => { setPwdRequest({ 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) => { setIsSaving(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'clients/admin_delete', { method: 'POST', body: JSON.stringify({ uid, pwd }) }); if ((await res.json()).status === 'success') { showToast("Client supprimé avec succès."); refreshData(); } else showToast("Erreur de suppression (Mot de passe incorrect ?)", 'error'); } finally { setIsSaving(false); } } }); }; const toggleChantierStatus = async (id, currentStatus) => { const newStatus = currentStatus === 'Actif' ? 'Hors ligne' : 'Actif'; if (!confirm(`Passer ce panneau en statut : ${newStatus} ?`)) return; setIsSaving(true); try { await fetch(window.ECO_CONFIG.apiBaseUrl + 'chantiers/status', { method: 'POST', body: JSON.stringify({ id, status: newStatus }) }); refreshData(); } finally { setIsSaving(false); } }; // Fonction de mise à jour logistique acceptant le lien de tracking const updateShipping = async (id, status, tracking_number, tracking_link = '') => { setIsSaving(true); try { await fetch(window.ECO_CONFIG.apiBaseUrl + 'chantiers/shipping', { method: 'POST', body: JSON.stringify({ id, shipping_status: status, tracking_number, tracking_link }) }); showToast(`Statut logistique mis à jour : ${status}`); setShippingModal(null); refreshData(); } finally { setIsSaving(false); } }; const handleAutoFill = async () => { const num = prompt("Saisissez le SIRET (14 chiffres) ou SIREN (9 chiffres) de l'entreprise qui gère la plateforme (Vous) :"); if (!num) return; const cleanNum = num.replace(/\D/g, ''); if (cleanNum.length !== 9 && cleanNum.length !== 14) { return showToast("Le numéro doit contenir exactement 9 ou 14 chiffres.", "error"); } setIsSaving(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/sirene', { method: 'POST', body: JSON.stringify({ q: cleanNum }) }); const responseData = await res.json(); if (responseData.status !== 'success') throw new Error(responseData.message); const apiData = responseData.data; if (apiData.results && apiData.results.length > 0) { const company = apiData.results[0]; const expectedSiren = cleanNum.substring(0, 9); if (company.siren !== expectedSiren) { setIsSaving(false); return showToast("Numéro invalide : Aucune entreprise correspondante.", "error"); } const siege = company.siege || {}; let foundSiret = siege.siret || company.siren + "00010"; if (cleanNum.length === 14) { if (siege.siret === cleanNum) foundSiret = cleanNum; else if (company.matching_etablissements) { const exactEtab = company.matching_etablissements.find(e => e.siret === cleanNum); if (exactEtab) foundSiret = cleanNum; } } const addressParts = [siege.numero_voie, siege.indice_repetition, siege.type_voie, siege.libelle_voie, siege.code_postal, siege.libelle_commune]; const address = addressParts.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim() || "Adresse non renseignée"; const tvaKey = (12 + 3 * (parseInt(company.siren, 10) % 97)) % 97; const calculatedTva = `FR${tvaKey.toString().padStart(2, '0')}${company.siren}`; setSettingsData({ ...settingsData, billing_company: company.nom_complet, billing_address: address, billing_siret: foundSiret, billing_tva: calculatedTva }); setHasTva(true); showToast("Informations récupérées avec succès !", "success"); } else { showToast("Aucune entreprise trouvée.", "error"); } } catch (err) { console.error("Détail de l'erreur SIRENE :", err); showToast("Le service gouvernemental est momentanément indisponible.", "error"); } setIsSaving(false); }; const handleToggleTva = () => { if (hasTva) { setShowTvaWarning(true); } else { setHasTva(true); } }; const confirmTvaExemption = () => { setHasTva(false); setSettingsData({ ...settingsData, billing_tva: '' }); setShowTvaWarning(false); }; const handleSaveSettings = () => { setPwdRequest({ title: "Modification globale", desc: "Saisissez le mot de passe administrateur pour confirmer la modification des tarifs et des paramètres de sécurité.", onConfirm: async (pwd) => { setIsSaving(true); try { const payload = { ...settingsData, pwd, billing_has_tva: hasTva ? '1' : '0', price_rentalMo: settingsData.rentalMo, price_purchase: settingsData.purchase, price_boardFirst: settingsData.boardFirst, price_boardAdd: settingsData.boardAdd, price_noAds: settingsData.noAds }; const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'settings/update', { method: 'POST', body: JSON.stringify(payload) }); if ((await res.json()).status === 'success') { showToast("Paramètres globaux mis à jour."); refreshData(); } else showToast("Mot de passe invalide", 'error'); } finally { setIsSaving(false); } } }); }; // --- ACTIONS DE PANNEAU DÉMO --- const handleSaveChantier = async () => { setIsSaving(true); try { const payload = { id: 'demo-chantier', status: 'Actif', offerType: 'demo', currentRate: 0, physicalPanels: 0, details: managingChantier }; const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'chantiers', { method: 'POST', body: JSON.stringify(payload) }); const d = await res.json(); if(d.status === 'success') { showToast("Panneau de démonstration mis à jour !"); setIsModalOpen(false); refreshData(); } else showToast(d.message, 'error'); } catch(e) { showToast("Erreur de sauvegarde", "error"); } setIsSaving(false); }; const saveEditedEntity = () => { let newInter = [...(managingChantier.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(managingChantier.lots || [])); const loc = editingEntity.location; if (loc.type === 'intervenant') { if (loc.index !== undefined) newInter[loc.index] = editingEntity.data; else newInter.push({...editingEntity.data, id: crypto.randomUUID()}); } else if (loc.type === 'lot') { if (loc.index !== undefined) newLots[loc.index] = {...newLots[loc.index], ...editingEntity.data}; else newLots.push({...editingEntity.data, id: crypto.randomUUID(), entreprises: []}); } else if (loc.type === 'entreprise') { if (loc.index !== undefined) newLots[loc.lotIndex].entreprises[loc.index] = editingEntity.data; else newLots[loc.lotIndex].entreprises.push({...editingEntity.data, id: crypto.randomUUID()}); } setManagingChantier({ ...managingChantier, intervenants: newInter, lots: newLots }); setEditingEntity(null); }; const deleteEntity = (loc) => { if (!confirm("Supprimer cet élément ?")) return; let newInter = [...(managingChantier.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(managingChantier.lots || [])); if (loc.type === 'intervenant') newInter.splice(loc.index, 1); else if (loc.type === 'lot') newLots.splice(loc.index, 1); else newLots[loc.lotIndex].entreprises.splice(loc.index, 1); setManagingChantier({ ...managingChantier, intervenants: newInter, lots: newLots }); }; const handleDragStart = (e, location) => { setDraggedItem(location); e.dataTransfer.effectAllowed = 'move'; }; const handleDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }; const handleDrop = (e, dropLocation) => { e.preventDefault(); if (!draggedItem || draggedItem.type !== dropLocation.type) return; if (draggedItem.type === 'entreprise' && draggedItem.lotIndex !== dropLocation.lotIndex) return; let newInter = [...(managingChantier.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(managingChantier.lots || [])); if (draggedItem.type === 'intervenant') { const item = newInter.splice(draggedItem.index, 1)[0]; newInter.splice(dropLocation.index, 0, item); } else if (draggedItem.type === 'lot') { const item = newLots.splice(draggedItem.index, 1)[0]; newLots.splice(dropLocation.index, 0, item); } else if (draggedItem.type === 'entreprise') { const arr = newLots[dropLocation.lotIndex].entreprises; const item = arr.splice(draggedItem.index, 1)[0]; arr.splice(dropLocation.index, 0, item); } setManagingChantier({ ...managingChantier, intervenants: newInter, lots: newLots }); setDraggedItem(null); }; // --- ACTIONS DE MODALES SYSTÈME --- const fetchDiagnostics = async () => { setModalView('diagnostics'); setDiagnostics(null); const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/diagnostics&_t=' + Date.now(), { cache: 'no-store' }); const d = await res.json(); if(d.status === 'success') setDiagnostics(d.data); }; const fetchLogs = async () => { setModalView('logs'); setLogs('Chargement...'); const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/logs&_t=' + Date.now(), { cache: 'no-store' }); const d = await res.json(); setLogs(d.data?.logs || 'Aucun log trouvé.'); }; const clearLogs = async () => { await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/logs/clear', { method: 'POST' }); fetchLogs(); fetchTelemetry(); }; const loadBackups = async () => { setModalView('backups'); setShowAllBackups(false); setIsLoadingBackups(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups&_t=' + Date.now(), { cache: 'no-store' }); const d = await res.json(); if(d.status === 'success') setBackups(d.data); } catch(e) { showToast("Erreur lors du chargement des sauvegardes", "error"); } finally { setIsLoadingBackups(false); } }; const createBackup = async () => { setIsSaving(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups/create', { method: 'POST' }); if ((await res.json()).status === 'success') { showToast("Sauvegarde créée."); loadBackups(); fetchTelemetry(); } else showToast("Erreur de sauvegarde", "error"); } finally { setIsSaving(false); } }; const restoreBackup = (filename) => { setPwdRequest({ title: "Restauration système", desc: "ATTENTION CRITIQUE : La restauration écrasera la base de données actuelle. Saisissez le mot de passe administrateur :", onConfirm: async (pwd) => { setIsSaving(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups/restore', { method: 'POST', body: JSON.stringify({ filename, pwd }) }); if ((await res.json()).status === 'success') { alert("Restauration réussie. L'application va recharger."); window.location.reload(); } else showToast("Mot de passe incorrect ou erreur.", "error"); } finally { setIsSaving(false); } } }); }; const deleteBackup = (filename) => { setPwdRequest({ title: "Suppression d'archive", desc: "Saisissez le mot de passe administrateur pour supprimer définitivement cette sauvegarde :", onConfirm: async (pwd) => { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/backups/delete', { method: 'POST', body: JSON.stringify({ filename, pwd }) }); if ((await res.json()).status === 'success') { loadBackups(); fetchTelemetry(); } else { showToast("Mot de passe incorrect", "error"); } } }); }; const getColorClass = (status) => { if (!sysSummary.loaded) return 'text-slate-500 border-slate-200 bg-slate-50'; if (status === 'red') return 'text-red-700 border-red-200 bg-red-50 hover:border-red-400'; if (status === 'amber') return 'text-amber-700 border-amber-200 bg-amber-50 hover:border-amber-400'; return 'text-emerald-700 border-emerald-200 bg-emerald-50 hover:border-emerald-400'; }; // --- VUES DES ONGLETS (CARTES FLUIDES) --- const renderDashboard = () => { const recentMessages = interactions.filter(m => m.target === 'Admin').slice(0, 5); return (

Supervision

Vue d'ensemble de la plateforme SaaS.

} color="text-emerald-600" bg="bg-emerald-100" /> } color="text-blue-600" bg="bg-blue-100" onClick={() => setActiveTab('clients')} /> } color="text-indigo-600" bg="bg-indigo-100" onClick={() => setActiveTab('chantiers')} /> } color="text-amber-600" bg="bg-amber-100" />

Derniers messages

{recentMessages.map((m, i) => (
setActiveTab('messages')}>

{m.author}

{new Date(m.created_at).toLocaleDateString('fr-FR')}

{m.detail}

{m.chantierId === 'CONTACT_PUBLIC' ? 'ORIGINE : Formulaire d\'accueil' : 'ORIGINE : Espace client (Support)'}

))} {recentMessages.length === 0 &&

Aucune interaction récente.

}

Serveur et maintenance

); }; const renderClients = () => (

Base clients

Gestion des comptes et prise de contrôle.

{clients.map(c => { const isImpersonable = c.admin_access_until && new Date(c.admin_access_until) > new Date(); const isSuspended = c.paymentStatus === 'suspended'; return (

{c.name}

{c.email}

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

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

Aucun client enregistré.

)}
); const renderChantiers = () => { const demoChantier = chantiers.find(c => c.id === 'demo-chantier') || { id: 'demo-chantier', status: 'Actif', offerType: 'demo', name: 'Panneau de démonstration', location: 'Paris', themeColor: '#059669', hasNoAds: false }; const regularChantiers = chantiers.filter(c => c.id !== 'demo-chantier'); return (

Tous les panneaux

Supervision des affichages légaux.

Panneau de démonstration

Gérez le contenu du faux panneau affiché sur la page d'accueil publique.

{regularChantiers.map(c => { const client = clients.find(cl => cl.id === c.clientName); return (

{c.name}

{c.location}

Client : {client ? client.name : c.clientName}

Abonnement

{c.offerType === 'rental' ? 'Loc.' : 'Achat'} ({c.currentRate}€)

Statut

{c.status}

); })} {regularChantiers.length === 0 && (

Aucun panneau client enregistré.

)}
); }; const renderLogistique = () => { const logiChantiers = chantiers.filter(c => c.physicalPanels > 0 && c.id !== 'demo-chantier'); return (

Logistique

Gestion des expéditions de panneaux physiques A1.

{logiChantiers.map(c => { const client = clients.find(cl => cl.id === c.clientName); const status = c.shipping_status || 'Validation en cours'; const isValidation = status === 'Validation en cours'; const isPrinting = status === "Attente d'impression"; const isShipped = status === 'Expédié'; const isDelivered = status === 'Livré'; return (
{/* Identité */}
{isDelivered ? : }

{c.name}

Client : {client?.name || 'Inconnu'}

{client?.address || 'Adresse non renseignée'}

{/* Quantité */}

Quantité

{c.physicalPanels}
{/* Statut et Boutons d'Action Logistique */}

Statut

{status}
{isValidation && ( )} {isPrinting && ( )} {isShipped && (

En cours d'acheminement

{c.tracking_number}

)} {isDelivered && (

Réception confirmée

)}
{/* Fichier A1 */}
{c.a1PdfId ? ( PDF A1 ) : (
Manquant
)}
); })} {logiChantiers.length === 0 && (

Aucune commande

Les commandes de panneaux physiques apparaîtront ici.

)}
); }; const renderInvoices = () => (

Facturation globale

Historique des paiements de tous les clients.

Export CSV
{data.invoices?.map((inv, i) => { const client = clients.find(c => c.id === inv.clientName); return (

{inv.chantierName}

Client : {client ? client.name : 'Inconnu'}

Date

{new Date(inv.created_at).toLocaleDateString('fr-FR')}

Montant

{inv.amount} €

{inv.invoiceNumber} PDF
); })} {(!data.invoices || data.invoices.length === 0) && (

Aucune facture émise.

)}
); const renderMessages = () => { const threadsMap = {}; interactions.forEach(m => { let threadId = null; let title = ''; let targetEmail = ''; if (m.chantierId === 'CONTACT_PUBLIC') { targetEmail = (m.target === 'Admin') ? m.author : m.target; if (!targetEmail) return; threadId = `CONTACT_PUBLIC_${targetEmail}`; title = `Contact public : ${targetEmail}`; } else if (m.chantierId.startsWith('SUPPORT_')) { threadId = m.chantierId; const cUid = m.chantierId.split('_')[1]; const cName = clients.find(c => c.id === cUid)?.name || 'Client inconnu'; title = `Support : ${cName}`; targetEmail = 'Client'; } if (threadId) { if (!threadsMap[threadId]) threadsMap[threadId] = { id: threadId, title, targetEmail, messages: [], unread: 0 }; threadsMap[threadId].messages.push(m); if (m.target === 'Admin' && !m.resolved) threadsMap[threadId].unread++; } }); const threads = Object.values(threadsMap).sort((a,b) => new Date(b.messages[0].created_at) - new Date(a.messages[0].created_at)); const selectedThreadId = new URLSearchParams(window.location.search).get('chat_id') || (threads[0]?.id); const setChatId = (id) => { const u = new URL(window.location); u.searchParams.set('chat_id', id); window.history.replaceState({}, '', u); refreshData(); }; const selectedThread = threads.find(t => t.id === selectedThreadId); const handleSendAdmin = async (text) => { if (!selectedThread) return; const payload = { chantierId: selectedThread.id.startsWith('CONTACT_PUBLIC_') ? 'CONTACT_PUBLIC' : selectedThread.id, detail: text, author: 'Admin', targetEmail: selectedThread.targetEmail }; await fetch(window.ECO_CONFIG.apiBaseUrl + 'interactions', { method: 'POST', body: JSON.stringify(payload) }); refreshData(); }; return (

Messagerie

{threads.map(t => ( ))} {threads.length === 0 &&

Aucune conversation.

}
{selectedThread ? ( <>

{selectedThread.title}

) : (

Sélectionnez une conversation

)}
); }; const renderSettings = () => (

Paramètres système

Tarification globale et mentions légales.

Tarification (HT)

setSettingsData({...settingsData, rentalMo: v})} /> setSettingsData({...settingsData, purchase: v})} /> setSettingsData({...settingsData, boardFirst: v})} /> setSettingsData({...settingsData, boardAdd: v})} /> setSettingsData({...settingsData, noAds: v})} />

Mentions légales (CGV et RGPD)

Émetteur (facturation)

setSettingsData({...settingsData, billing_company: e.target.value})} className="w-full border border-slate-200 rounded-lg p-2 text-sm outline-none focus:border-slate-400 bg-white" />
setSettingsData({...settingsData, billing_siret: e.target.value})} className="w-full border border-slate-200 rounded-lg p-2 text-sm outline-none focus:border-slate-400 bg-white" />
setSettingsData({...settingsData, billing_address: e.target.value})} className="w-full border border-slate-200 rounded-lg p-2 text-sm outline-none focus:border-slate-400 bg-white" />
{hasTva && (
setSettingsData({...settingsData, billing_tva: e.target.value})} placeholder="FR..." className="w-full border border-slate-200 rounded-lg p-2 text-sm outline-none focus:border-slate-400 uppercase bg-white" />
)}

Options du mode simplifié

Sélectionnez les fonctionnalités accessibles aux clients utilisant l'interface "Simplifiée".

{[ { key: 'simp_opt_description', label: 'Description des travaux' }, { key: 'simp_opt_image', label: 'Image du panneau' }, { key: 'simp_opt_theme', label: 'Couleur du thème' }, { key: 'simp_opt_link', label: 'Lien promoteur' }, { key: 'simp_opt_emergency', label: 'N° d\'urgence' }, { key: 'simp_opt_schedule', label: 'Horaires des nuisances' } ].map(opt => ( ))}
); // --- RENDU GLOBAL --- return ( { await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/logout'); window.location.href='?'; }}> {activeTab === 'dashboard' && renderDashboard()} {activeTab === 'clients' && renderClients()} {activeTab === 'chantiers' && renderChantiers()} {activeTab === 'logistique' && renderLogistique()} {activeTab === 'invoices' && renderInvoices()} {activeTab === 'messages' && renderMessages()} {activeTab === 'settings' && renderSettings()} {/* Modale d'expédition logistique (création ou mise à jour) */} {shippingModal && ( setShippingModal(null)} preventClose={isSaving}>

{shippingModal.shipping_status === 'Expédié' ? "Corrigez le numéro ou le lien de suivi. Le client recevra un e-mail avec les nouvelles informations." : "Saisissez les informations de suivi pour notifier le client. Le lien de suivi est obligatoire pour valider l'expédition."}

setTrackingData({...trackingData, number: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-blue-500 outline-none transition" placeholder="Ex : 6A123456789" />
setTrackingData({...trackingData, link: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-blue-500 outline-none transition" placeholder="Ex : https://www.laposte.fr/outils/suivre-vos-envois?code=..." />
)} {showTvaWarning && ( setShowTvaWarning(false)}>

Exonération de TVA

En désactivant ce champ, vous indiquez que votre propre société (la plateforme eco-panneau) n'est pas assujettie à la TVA (ex : auto-entrepreneur).

Conséquence immédiate :

  • Toutes vos futures factures seront émises sans TVA (Prix TTC = Prix HT).
  • La mention légale "TVA non applicable, art. 293 B du CGI" figurera sur les factures clients.
  • Les montants envoyés à Stripe ne seront plus majorés de 20%.
)} {pwdRequest && ( setPwdRequest(null)}>
{pwdRequest.desc}
{ e.preventDefault(); const pwd = e.target.pwd.value; if(pwd) { pwdRequest.onConfirm(pwd); setPwdRequest(null); } }}>
)} {/* Autres Modales Système... */} {modalView === 'diagnostics' && ( setModalView(null)}> {!diagnostics ?

Analyse complète en cours...

: (
{diagnostics.map(d => (

{d.label}

{d.status}

{d.detail}
))}
)}
)} {modalView === 'logs' && ( setModalView(null)}>
{logs}
)} {modalView === 'backups' && ( setModalView(null)} preventClose={isLoadingBackups}>
{isLoadingBackups ? (

Chargement en cours...

) : ( <> {backups.slice(0, showAllBackups ? backups.length : 5).map(b => (

{b.name}

{new Date(b.date).toLocaleString('fr-FR')} • {b.size}

))} {backups.length === 0 &&

Aucune sauvegarde trouvée.

} {!showAllBackups && backups.length > 5 && ( )} )}
)} {isModalOpen && modalView === 'config_demo' && (
{ if(e.target === e.currentTarget) document.getElementById('editor-cancel-btn')?.click(); }}>
e.stopPropagation()}>

Édition du panneau démo

Panneau public vitrine.

setIsModalOpen(false)} onSaveActive={handleSaveChantier} onEditEntity={(loc, data) => setEditingEntity({location: loc, data})} draggedItem={draggedItem} handleDragStart={handleDragStart} handleDragOver={handleDragOver} handleDrop={handleDrop} deleteEntity={deleteEntity} isSaving={isSaving} uiMode="professionnel" />
{editingEntity && }
)}
); }; /* EOF ===== [_administrateur.jsx] =============== */