/** * ========================================================================= * PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0 * Point d'entrée React (Main) - Routage, Authentification, et État Global * ========================================================================= */ const { useState, useEffect, useCallback } = React; const SafeIcon = (FallbackText) => window[FallbackText] || (({className}) => {FallbackText}); const Loader = SafeIcon('Loader'); const AlertTriangle = SafeIcon('AlertTriangle'); const CheckCircle = SafeIcon('CheckCircle'); const Info = SafeIcon('Info'); const Lock = SafeIcon('Lock'); const Mail = SafeIcon('Mail'); const Building = SafeIcon('Building'); const KeyRound = SafeIcon('KeyRound'); const Shield = SafeIcon('Shield'); const X = SafeIcon('X'); const getPublicView = () => window.AccueilView || null; const getRiverainView = () => window.RiverainView || null; const getAdminView = () => window.AdminView || null; const getClientView = () => window.ClientView || null; const getPanneauEditor = () => window.PanneauEditorForm || null; const getEntityEditor = () => window.EntityEditorModal || null; const FallbackError = ({ name }) => (
Erreur d'affichage : Le composant "{name}" est introuvable.
Vérifiez que le fichier de la vue a été correctement chargé.
); const DelegateView = ({ token, showToast }) => { const [panneau, setPanneau] = useState(null); const [loading, setLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const [editingEntity, setEditingEntity] = useState(null); const PanneauEditor = getPanneauEditor(); const EntityEditor = getEntityEditor(); useEffect(() => { fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux/delegated_access&token=' + encodeURIComponent(token) + '&_t=' + Date.now()) .then(r => r.json()) .then(d => { if (d.status === 'success') { setPanneau(d.data); } else { setError(d.message); } setLoading(false); }) .catch(() => { setError("Erreur réseau"); setLoading(false); }); }, [token]); const handleSave = async () => { setIsSaving(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'panneaux/delegated_update', { method: 'POST', body: JSON.stringify({ token, ...panneau }) }); const d = await res.json(); if (d.status === 'success') { showToast("Informations enregistrées avec succès.", "success"); } else { showToast(d.message, 'error'); } } catch (e) { showToast("Erreur lors de la sauvegarde.", "error"); } setIsSaving(false); }; if (loading) { return (

Vérification de l'accès...

); } if (error) { return (

Accès refusé

{error}

); } return (

Saisie déléguée

Mise à jour des informations du panneau

Vous avez été invité à compléter les informations légales de ce panneau (intervenants, sous-traitants, PDF de l'arrêté). Vos modifications seront directement répercutées sur l'affichage public.
{PanneauEditor ? ( window.location.href = '?'} onPublish={handleSave} onEditEntity={(loc, data) => setEditingEntity({location: loc, data})} deleteEntity={(loc) => { if (!confirm("Supprimer cet élément ?")) return; let newInter = [...(panneau.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(panneau.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); } setPanneau({ ...panneau, intervenants: newInter, lots: newLots }); }} isSaving={isSaving} uiMode="delege" lockedFields={panneau.lockedFields || []} showToast={showToast} /> ) : }
{editingEntity && EntityEditor && ( { let newInter = [...(panneau.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(panneau.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()}); } } setPanneau({ ...panneau, intervenants: newInter, lots: newLots }); setEditingEntity(null); }} showToast={showToast} /> )}
); }; const AuthInput = ({ type, name, value, onChange, placeholder, icon }) => (
{icon}
); const AuthModal = ({ initialView = 'login', token = '', onClose, onLoginSuccess, showToast }) => { const [view, setView] = useState(initialView); const [loading, setLoading] = useState(false); const [formData, setFormData] = useState({ email: '', password: '', company: '', code: '', remember_duration: 0 }); const handleInputChange = (e) => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; const executeAuth = async (endpoint, dataObj, onSuccessCallback) => { setLoading(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + endpoint, { method: 'POST', body: JSON.stringify(dataObj) }); const d = await res.json(); if (d.status === 'success') { onSuccessCallback(d); } else { showToast(d.message, 'error'); } } catch(err) { showToast("Erreur réseau", 'error'); } setLoading(false); }; const handleLogin = (e) => { e.preventDefault(); const payload = { login: formData.email, password: formData.password, remember_duration: formData.remember_duration }; executeAuth('auth/login', payload, d => { if (d.data['2fa_required']) { setView('2fa'); showToast("Veuillez saisir votre code de sécurité.", "info"); } else { setFormData(prev => ({ ...prev, password: '' })); onLoginSuccess(d.data.role); } }); }; const handleResend2FA = async () => { setLoading(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/login', { method: 'POST', body: JSON.stringify({ login: formData.email, password: formData.password, remember_duration: formData.remember_duration }) }); const d = await res.json(); if (d.status === 'success' && d.data['2fa_required']) { showToast("Un nouveau code de sécurité vous a été envoyé.", "success"); } else { showToast(d.message || "Erreur lors du renvoi du code.", "error"); } } catch(err) { showToast("Erreur réseau", "error"); } setLoading(false); }; const handleVerify2FA = (e) => { e.preventDefault(); executeAuth('auth/verify_2fa', { code: formData.code }, d => { setFormData(prev => ({ ...prev, password: '', code: '' })); onLoginSuccess(d.data.role); }); }; const handleRegisterRequest = (e) => { e.preventDefault(); executeAuth('auth/register_request', { email: formData.email, company: formData.company }, () => { setView('register_sent'); }); }; const handleRegisterConfirm = (e) => { e.preventDefault(); executeAuth('auth/register_confirm', { token, password: formData.password }, d => { showToast("Compte créé avec succès !", "success"); setFormData(prev => ({ ...prev, password: '' })); onLoginSuccess(d.data.role); }); }; const handleForgotRequest = (e) => { e.preventDefault(); executeAuth('auth/forgot_password', { email: formData.email }, () => { setView('forgot_sent'); }); }; const handleResetConfirm = (e) => { e.preventDefault(); executeAuth('auth/reset_password', { token, password: formData.password }, () => { showToast("Mot de passe modifié avec succès.", "success"); setFormData(prev => ({ ...prev, password: '' })); setView('login'); }); }; return (
e.stopPropagation()}>

{view === 'login' ? 'Connexion' : view === 'register' ? 'Créer un compte' : view === '2fa' ? 'Sécurité 2FA' : view.includes('forgot') ? 'Mot de passe oublié' : 'Nouveau mot de passe'}

{view === 'login' && (
} /> } />
{formData.remember_duration > 0 && ( )}

Pas encore de compte ?

)} {view === 'register' && (
} /> } />

Déjà inscrit ?

)} {view === 'register_sent' && (
Un lien magique a été envoyé à {formData.email}. Cliquez dessus pour activer votre compte.
)} {view === 'register_confirm' && (

Dernière étape : choisissez un mot de passe sécurisé pour protéger votre compte.

} /> )} {view === '2fa' && (

Un code de sécurité vous a été envoyé par e-mail ou est disponible sur votre application d'authentification.

} />
)} {view === 'forgot' && (

Saisissez l'adresse e-mail associée à votre compte pour recevoir un lien de réinitialisation.

} /> )} {view === 'forgot_sent' && (
Si un compte existe pour cet e-mail, un lien de réinitialisation vous a été envoyé.
)} {view === 'reset' && (

Saisissez votre nouveau mot de passe (8 caractères minimum avec majuscule et chiffre).

} /> )}
); }; // ========================================================================= // 3. COMPOSANT RACINE (MAIN APP) - ORCHESTRATION GLOBALE // ========================================================================= const App = () => { const [session, setSession] = useState({ loaded: false, role: 'public', user_id: null, is_impersonating: false }); const [data, setData] = useState({ panneaux: [], clients: [], interactions: [], invoices: [], settings: {}, prices: {}, stats: {} }); const [loading, setLoading] = useState(true); const [toast, setToast] = useState(null); const [authConfig, setAuthConfig] = useState(null); const [globalLoading, setGlobalLoading] = useState(false); const [actionResult, setActionResult] = useState(null); const [globalPanic, setGlobalPanic] = useState(null); const showToast = useCallback((msg, type = 'info') => { const id = Date.now(); setToast({ msg, type, id }); setTimeout(() => setToast(current => current?.id === id ? null : current), 4000); }, []); useEffect(() => { const handleOptimisticMsg = (e) => { const newMsg = e.detail; setData(prev => { const merged = [...(prev.interactions || []), newMsg]; // CORRECTION : Contournement du bug Date Safari avec le wrapper String merged.sort((a, b) => new Date(String(a.created_at || '').replace(' ', 'T')).getTime() - new Date(String(b.created_at || '').replace(' ', 'T')).getTime()); return { ...prev, interactions: merged }; }); }; window.addEventListener('optimistic_message', handleOptimisticMsg); return () => window.removeEventListener('optimistic_message', handleOptimisticMsg); }, []); useEffect(() => { const originalFetch = window.fetch; window.fetch = async function() { const urlStr = arguments[0] instanceof Request ? arguments[0].url : arguments[0]; const isSilent = typeof urlStr === 'string' && urlStr.includes('silent=1'); if (!isSilent) { window.activeApiCalls = (window.activeApiCalls || 0) + 1; window.dispatchEvent(new CustomEvent('api_call_start')); } try { const response = await originalFetch.apply(this, arguments); if (response.status === 423) { try { const errorData = await response.clone().json(); if (errorData.status === 'panic') { setGlobalPanic({ message: errorData.message, mode: errorData.data?.panic_mode || 'seal' }); } } catch(e) { setGlobalPanic({ message: "Alerte de sécurité. Verrouillage du site en cours.", mode: 'shield' }); } } if (response.status === 401) { const url = new URL(arguments[0], window.location.origin); if (url.searchParams.get('api') !== 'auth/me' && url.searchParams.get('api') !== 'auth/login') { window.dispatchEvent(new CustomEvent('session_expired')); } } return response; } finally { if (!isSilent) { window.activeApiCalls = Math.max(0, window.activeApiCalls - 1); if (window.activeApiCalls === 0) window.dispatchEvent(new CustomEvent('api_call_end')); } } }; let timeout; const handleStart = () => { if (window.activeApiCalls === 1) { // CORRECTION : Le délai du loader global passe de 1000 à 3000 ms timeout = setTimeout(() => setGlobalLoading(true), 3000); } }; const handleEnd = () => { if (window.activeApiCalls === 0) { clearTimeout(timeout); setGlobalLoading(false); } }; window.addEventListener('api_call_start', handleStart); window.addEventListener('api_call_end', handleEnd); return () => { window.removeEventListener('api_call_start', handleStart); window.removeEventListener('api_call_end', handleEnd); clearTimeout(timeout); window.fetch = originalFetch; }; }, []); const fetchData = async (silent = false) => { try { const vid = window.getVisitorId(); const urlParams = new URLSearchParams(window.location.search); const scanId = urlParams.get('scan') || ''; const threadToken = urlParams.get('thread') || ''; const res = await fetch(window.ECO_CONFIG.apiBaseUrl + `sync&visitor_id=${vid}&scan=${scanId}&thread=${encodeURIComponent(threadToken)}${silent ? '&silent=1' : ''}&_t=${Date.now()}`); const d = await res.json(); if (d.status === 'success') { setData(prevData => { const newInteractions = d.data.interactions || []; const prevInteractions = prevData.interactions || []; const realPrev = prevInteractions.filter(m => !String(m.id).startsWith('temp_')); const tempMsgs = prevInteractions.filter(m => String(m.id).startsWith('temp_')); let merged = newInteractions.length > realPrev.length ? newInteractions : [...newInteractions, ...tempMsgs]; // CORRECTION : Contournement du bug Date Safari avec le wrapper String merged.sort((a, b) => new Date(String(a.created_at || '').replace(' ', 'T')).getTime() - new Date(String(b.created_at || '').replace(' ', 'T')).getTime()); return { ...d.data, interactions: merged }; }); } } catch (e) { // Silence } finally { if (!silent) setLoading(false); } }; useEffect(() => { if (!session.loaded) return; const urlParams = new URLSearchParams(window.location.search); let updated = false; if (urlParams.has('reg_token')) { setAuthConfig({ view: 'register_confirm', token: urlParams.get('reg_token') }); urlParams.delete('reg_token'); updated = true; } else if (urlParams.has('reset_token')) { setAuthConfig({ view: 'reset', token: urlParams.get('reset_token') }); urlParams.delete('reset_token'); updated = true; } else if (urlParams.has('del_token')) { const token = urlParams.get('del_token'); urlParams.delete('del_token'); updated = true; fetch(window.ECO_CONFIG.apiBaseUrl + 'clients/confirm_delete', { method: 'POST', body: JSON.stringify({ token }) }) .then(r => r.json()) .then(d => { if (d.status === 'success') { setActionResult({ title: "Compte supprimé", message: "Votre compte et toutes vos données ont été définitivement supprimés. Au revoir !", type: "success" }); } else { setActionResult({ title: "Erreur", message: d.message || "Lien invalide ou expiré.", type: "error" }); } }); } else if (urlParams.has('grant_access')) { const token = urlParams.get('grant_access'); urlParams.delete('grant_access'); updated = true; if (session.role === 'admin' && !session.is_impersonating) { setActionResult({ title: "Action non autorisée", message: "En tant qu'administrateur, vous ne pouvez pas vous auto-attribuer l'accès à un compte client de cette manière.", type: "error" }); } else { fetch(window.ECO_CONFIG.apiBaseUrl + 'clients/grant_access', { method: 'POST', body: JSON.stringify({ token }) }) .then(r => r.json()) .then(d => { if (d.status === 'success') { setActionResult({ title: "Accès autorisé", message: "L'équipe technique a désormais un accès temporaire (24h) à votre compte pour vous assister.", type: "success" }); } else { setActionResult({ title: "Lien invalide", message: "Ce lien d'autorisation est invalide ou a expiré.", type: "error" }); } }); } } if (updated) { window.history.replaceState({}, '', '?' + urlParams.toString()); } }, [session.loaded, session.role, session.is_impersonating, showToast]); useEffect(() => { const initSession = async () => { try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/me'); const d = await res.json(); if (d.status === 'success') { setSession({ loaded: true, role: d.data.role, user_id: d.data.user_id, is_impersonating: d.data.is_impersonating }); } else { setSession({ loaded: true, role: 'public', user_id: null, is_impersonating: false }); } } catch (e) { setSession({ loaded: true, role: 'public', user_id: null, is_impersonating: false }); } }; initSession(); const handleSessionExpired = () => { setSession({ loaded: true, role: 'public', user_id: null, is_impersonating: false }); showToast("Votre session a expiré. Veuillez vous reconnecter.", "info"); }; window.addEventListener('session_expired', handleSessionExpired); return () => window.removeEventListener('session_expired', handleSessionExpired); }, [showToast]); useEffect(() => { if (session.loaded) { fetchData(); } }, [session.loaded, session.role]); useEffect(() => { if (!session.loaded) return; let pollingInterval; let currentDelay = 1000; let lastActivityTime = Date.now(); let isTabVisible = !document.hidden; const performChatSync = async () => { if (!isTabVisible) return; try { const urlParams = new URLSearchParams(window.location.search); const scanId = urlParams.get('scan') || ''; const threadToken = urlParams.get('thread') || ''; const res = await fetch(window.ECO_CONFIG.apiBaseUrl + `interactions/sync&scan=${scanId}&thread=${encodeURIComponent(threadToken)}&silent=1&_t=${Date.now()}`); const d = await res.json(); if (d.status === 'success') { setData(prevData => { const prevInteractions = prevData.interactions || []; const newInteractions = d.data.interactions || []; const realPrev = prevInteractions.filter(m => !String(m.id).startsWith('temp_')); let changed = realPrev.length !== newInteractions.length; if (!changed && newInteractions.length > 0 && realPrev.length > 0) { const lastPrev = realPrev[realPrev.length - 1]; const lastNew = newInteractions[newInteractions.length - 1]; if (lastPrev.id !== lastNew.id || lastPrev.resolved !== lastNew.resolved || lastPrev.isAlert !== lastNew.isAlert) { changed = true; } const prevUnread = realPrev.filter(m => !m.resolved).length; const newUnread = newInteractions.filter(m => !m.resolved).length; if (prevUnread !== newUnread) { changed = true; } } if (changed) { lastActivityTime = Date.now(); scheduleNextPoll(1000); const tempMsgs = prevInteractions.filter(m => String(m.id).startsWith('temp_')); let merged = newInteractions.length > realPrev.length ? newInteractions : [...newInteractions, ...tempMsgs]; // CORRECTION : Contournement bug Date Safari merged.sort((a, b) => new Date(String(a.created_at || '').replace(' ', 'T')).getTime() - new Date(String(b.created_at || '').replace(' ', 'T')).getTime()); return { ...prevData, interactions: merged }; } return prevData; }); } } catch(e) {} const idleTime = Date.now() - lastActivityTime; if (idleTime > 120000) { currentDelay = 10000; } else if (idleTime > 30000) { currentDelay = 3000; } else { currentDelay = 1000; } scheduleNextPoll(currentDelay); }; const scheduleNextPoll = (delay) => { clearTimeout(pollingInterval); if (isTabVisible) { pollingInterval = setTimeout(performChatSync, delay); } }; const handleVisibilityChange = () => { isTabVisible = !document.hidden; if (isTabVisible) { lastActivityTime = Date.now(); currentDelay = 1000; performChatSync(); } else { clearTimeout(pollingInterval); } }; const handleUserInteraction = () => { if (currentDelay > 1000) { lastActivityTime = Date.now(); currentDelay = 1000; scheduleNextPoll(1000); } else { lastActivityTime = Date.now(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('click', handleUserInteraction); document.addEventListener('keydown', handleUserInteraction); scheduleNextPoll(currentDelay); return () => { clearTimeout(pollingInterval); document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('click', handleUserInteraction); document.removeEventListener('keydown', handleUserInteraction); }; }, [session.loaded]); useEffect(() => { if (!session.loaded || session.role === 'public') return; let globalPollingInterval; let currentDelay = 15000; let lastActivityTime = Date.now(); let isTabVisible = !document.hidden; const performGlobalSync = async () => { if (!isTabVisible) return; await fetchData(true); const idleTime = Date.now() - lastActivityTime; if (idleTime > 120000) { currentDelay = 60000; } else if (idleTime > 30000) { currentDelay = 30000; } else { currentDelay = 15000; } scheduleNextPoll(currentDelay); }; const scheduleNextPoll = (delay) => { clearTimeout(globalPollingInterval); if (isTabVisible) { globalPollingInterval = setTimeout(performGlobalSync, delay); } }; const handleVisibilityChange = () => { isTabVisible = !document.hidden; if (isTabVisible) { lastActivityTime = Date.now(); currentDelay = 15000; performGlobalSync(); } else { clearTimeout(globalPollingInterval); } }; const handleUserInteraction = () => { if (currentDelay > 15000) { lastActivityTime = Date.now(); currentDelay = 15000; scheduleNextPoll(15000); } else { lastActivityTime = Date.now(); } }; document.addEventListener('visibilitychange', handleVisibilityChange); document.addEventListener('click', handleUserInteraction); document.addEventListener('keydown', handleUserInteraction); scheduleNextPoll(currentDelay); return () => { clearTimeout(globalPollingInterval); document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('click', handleUserInteraction); document.removeEventListener('keydown', handleUserInteraction); }; }, [session.loaded, session.role]); // Chargement initial if (!session.loaded || loading) { return (

Chargement sécurisé...

); } const urlParams = new URLSearchParams(window.location.search); const isScanOrThread = urlParams.has('scan') || urlParams.has('thread'); const delegateToken = urlParams.get('delegate'); let ViewToRender = null; const isRiverain = session.role === 'public' && isScanOrThread; if (session.role === 'admin') { const AdminViewComp = getAdminView(); ViewToRender = AdminViewComp ? : ; } else if (session.role === 'client') { const ClientViewComp = getClientView(); ViewToRender = ClientViewComp ? : ; } else { if (isRiverain) { const RiverainViewComp = getRiverainView(); const scanId = urlParams.get('scan'); let panneauData = data.panneaux?.find(c => c.id === scanId); if (!scanId && urlParams.has('thread')) { panneauData = { id: 'CONTACT_PUBLIC', name: 'Service client', location: 'Assistance en ligne', themeColor: '#0f172a', hasNoAds: true }; } const startTab = urlParams.has('thread') ? 'contact' : 'home'; ViewToRender = RiverainViewComp ? : ; } else if (delegateToken) { window.CURRENT_DELEGATE_TOKEN = delegateToken; ViewToRender = ; } else { const PublicViewComp = getPublicView(); ViewToRender = PublicViewComp ? setAuthConfig({ view })} showToast={showToast} /> : ; } } const showSiteBanner = !isRiverain && data.settings?.site_banner_active === '1' && data.settings?.site_banner_msg; return (
{showSiteBanner && (
/g, '>').replace(/"/g, '"').replace(/'/g, ''') }} className="admin-html" />
)}
{ViewToRender}
{actionResult && (
{actionResult.type === 'success' ? : }

{actionResult.title}

{actionResult.message}

)} {authConfig && ( setAuthConfig(null)} showToast={showToast} onLoginSuccess={(role) => { setAuthConfig(null); setSession(prev => ({ ...prev, role })); fetchData(); }} /> )} {globalLoading && (
Un instant...
)} {toast && (
{toast.type === 'error' ? : toast.type === 'success' ? : }
{toast.msg}
)} {globalPanic && (

{globalPanic.mode === 'shield' ? 'Bouclier anti-DDoS actif' : 'Action bloquée par sécurité'}

{globalPanic.message}

{globalPanic.mode === 'seal' ? ( ) : ( )}
)}
); }; const rootElement = document.getElementById('root'); if (rootElement) { const root = window.ReactDOM.createRoot(rootElement); root.render(); } /* EOF ===== [_main.jsx] =============== */