/** * ========================================================================= * PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0 * Point d'entrée de l'Application React (_main.jsx) * Gère le routage, l'authentification, les toasts et le flux de données * ========================================================================= */ const { useState, useEffect, useRef } = React; const { ShieldAlert, CheckCircle, AlertTriangle, Loader, LogIn, Mail, KeyRound, X, ArrowLeft } = window; // ========================================================================= // 1. COMPOSANT DÉLÉGUÉ (ACCÈS ARCHITECTE / SOUS-TRAITANT) // ========================================================================= const DelegateView = ({ token, showToast }) => { const [chantier, setChantier] = useState(null); const [loading, setLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [editingEntity, setEditingEntity] = useState(null); const [draggedItem, setDraggedItem] = useState(null); useEffect(() => { window.CURRENT_DELEGATE_TOKEN = token; fetch(window.ECO_CONFIG.apiBaseUrl + 'chantiers/delegated_access&token=' + encodeURIComponent(token)) .then(res => res.json()) .then(d => { if (d.status === 'success') setChantier(d.data); else showToast(d.message || "Lien invalide ou expiré.", "error"); }) .catch(() => showToast("Erreur de connexion", "error")) .finally(() => setLoading(false)); return () => { window.CURRENT_DELEGATE_TOKEN = null; }; }, [token]); const handleSave = () => { setIsSaving(true); fetch(window.ECO_CONFIG.apiBaseUrl + 'chantiers/delegated_update', { method: 'POST', body: JSON.stringify({ token, name: chantier.name, location: chantier.location, themeColor: chantier.themeColor, intervenants: chantier.intervenants, lots: chantier.lots, pdfId: chantier.pdfId }) }).then(res => res.json()).then(d => { if (d.status === 'success') { showToast("Modifications enregistrées avec succès.", "success"); setTimeout(() => window.location.href = '?', 2000); } else { showToast(d.message || "Erreur de sauvegarde", "error"); } }).finally(() => setIsSaving(false)); }; const saveEditedEntity = () => { let newInter = [...(chantier.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(chantier.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()}); } setChantier({ ...chantier, intervenants: newInter, lots: newLots }); setEditingEntity(null); }; const deleteEntity = (loc) => { if (!confirm("Supprimer cet élément ?")) return; let newInter = [...(chantier.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(chantier.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); setChantier({ ...chantier, 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 = [...(chantier.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(chantier.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); } setChantier({ ...chantier, intervenants: newInter, lots: newLots }); setDraggedItem(null); }; if (loading) return
; if (!chantier) return
Accès non autorisé ou expiré.
; return (

Accès délégué

Édition restreinte du panneau légal

window.location.href = '?'} onSaveDraft={handleSave} onEditEntity={(loc, data) => setEditingEntity({location: loc, data})} draggedItem={draggedItem} handleDragStart={handleDragStart} handleDragOver={handleDragOver} handleDrop={handleDrop} deleteEntity={deleteEntity} isSaving={isSaving} uiMode="delege" />
{editingEntity && }
); }; // ========================================================================= // 2. ÉCRAN D'AUTHENTIFICATION UNIFIÉ // ========================================================================= const AuthScreen = ({ onLoginSuccess, showToast, urlParams, initialView = 'login', onBack }) => { const [view, setView] = useState(initialView); const [loading, setLoading] = useState(false); const [loginData, setLoginData] = useState({ email: '', password: '', code2fa: '' }); const [regData, setRegData] = useState({ email: '', company: '' }); useEffect(() => { const processUrlParams = async () => { const grantToken = urlParams.get('grant_access'); if (grantToken) { try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'clients/grant_access', { method: 'POST', body: JSON.stringify({ token: grantToken }) }); if ((await res.json()).status === 'success') showToast("Accès temporaire accordé au support.", "success"); else showToast("Lien d'autorisation invalide.", "error"); } catch(e) { showToast("Erreur réseau.", "error"); } window.history.replaceState({}, '', '?'); } const regToken = urlParams.get('reg_token'); if (regToken) setView('register_confirm'); const resetToken = urlParams.get('reset_token'); if (resetToken) setView('reset_password'); }; processUrlParams(); }, []); const handleLogin = async (e) => { e.preventDefault(); setLoading(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/login', { method: 'POST', body: JSON.stringify({ login: loginData.email, password: loginData.password }) }); const data = await res.json(); if (data.status === 'success') { if (data.data['2fa_required']) { setView('2fa'); showToast(data.data.method === 'email' ? "Un code a été envoyé à votre adresse e-mail." : "Ouvrez votre application d'authentification.", "info"); } else { onLoginSuccess(data.data.role); } } else { showToast(data.message || "Identifiants incorrects", "error"); } } catch (err) { showToast("Erreur de connexion", "error"); } setLoading(false); }; const handle2FA = async (e) => { e.preventDefault(); setLoading(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/verify_2fa', { method: 'POST', body: JSON.stringify({ code: loginData.code2fa }) }); const data = await res.json(); if (data.status === 'success') onLoginSuccess(data.data.role); else showToast(data.message || "Code invalide", "error"); } catch (err) { showToast("Erreur de validation", "error"); } setLoading(false); }; const handleRegisterRequest = async (e) => { e.preventDefault(); setLoading(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/register_request', { method: 'POST', body: JSON.stringify(regData) }); const data = await res.json(); if (data.status === 'success') { setView('register_pending'); showToast("Veuillez vérifier vos e-mails.", "success"); } else if (data.status === 'exists') { setView('login'); showToast("Ce compte existe déjà. Veuillez vous connecter.", "info"); } else showToast(data.message || "Erreur d'inscription", "error"); } catch (err) { showToast("Erreur réseau", "error"); } setLoading(false); }; const handleRegisterConfirm = async (e) => { e.preventDefault(); setLoading(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/register_confirm', { method: 'POST', body: JSON.stringify({ token: urlParams.get('reg_token'), password: loginData.password }) }); const data = await res.json(); if (data.status === 'success') { showToast("Compte créé avec succès !", "success"); window.history.replaceState({}, '', '?'); onLoginSuccess('client'); } else showToast(data.message || "Lien invalide", "error"); } catch (err) { showToast("Erreur", "error"); } setLoading(false); }; const handleResetPassword = async (e) => { e.preventDefault(); setLoading(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/reset_password', { method: 'POST', body: JSON.stringify({ token: urlParams.get('reset_token'), password: loginData.password }) }); const data = await res.json(); if (data.status === 'success') { showToast("Mot de passe modifié !", "success"); window.history.replaceState({}, '', '?'); setView('login'); } else showToast(data.message || "Lien expiré", "error"); } catch (err) { showToast("Erreur", "error"); } setLoading(false); }; const handleForgotPasswordRequest = async (e) => { e.preventDefault(); setLoading(true); try { await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/forgot_password', { method: 'POST', body: JSON.stringify({ email: loginData.email }) }); showToast("Si ce compte existe, un e-mail a été envoyé.", "info"); setView('login'); } catch (err) { showToast("Erreur", "error"); } setLoading(false); }; return (
{view === 'login' && (

Connexion

setLoginData({...loginData, email: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition" />
setLoginData({...loginData, password: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition" />

Vous n'avez pas de compte ?

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

Sécurité requise

Veuillez saisir le code de vérification à 6 chiffres pour valider votre identité.

setLoginData({...loginData, code2fa: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-4 text-center text-2xl font-mono tracking-widest focus:border-emerald-500 outline-none transition" />
)} {view === 'register' && (

Nouveau compte

setRegData({...regData, company: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition" />
setRegData({...regData, email: e.target.value})} className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition" />
)} {view === 'register_pending' && (

Vérifiez vos e-mails

Un lien d'activation a été envoyé à l'adresse indiquée. Cliquez dessus pour configurer votre mot de passe.

)} {view === 'register_confirm' && (

Sécurisez votre compte

Créez un mot de passe robuste (Min 8 caractères, Maj, Min, Chiffre).

setLoginData({...loginData, password: e.target.value})} placeholder="Votre mot de passe..." className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition" />
)} {view === 'forgot' && (

Mot de passe oublié

Entrez votre e-mail pour recevoir un lien de réinitialisation.

setLoginData({...loginData, email: e.target.value})} placeholder="E-mail du compte" className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition" />
)} {view === 'reset_password' && (

Nouveau mot de passe

setLoginData({...loginData, password: e.target.value})} placeholder="Nouveau mot de passe fort" className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition" />
)}
Plateforme sécurisée © {new Date().getFullYear()} eco-panneau.fr
); }; // ========================================================================= // 3. RACINE DE L'APPLICATION (APP ROUTER) // ========================================================================= const App = () => { const [globalData, setGlobalData] = useState({ chantiers: [], interactions: [], clients: [], invoices: [], settings: {}, prices: {}, stats: {} }); const [loading, setLoading] = useState(true); const [userRole, setUserRole] = useState('public'); const [toasts, setToasts] = useState([]); // Détection des URL spéciales const urlParams = new URLSearchParams(window.location.search); const scanId = urlParams.get('scan'); const delegateToken = urlParams.get('delegate'); const threadToken = urlParams.get('thread'); const hasAuthParams = urlParams.has('reg_token') || urlParams.has('reset_token') || urlParams.has('grant_access'); // État pour afficher la vitrine ou l'écran d'auth const [authMode, setAuthMode] = useState(hasAuthParams ? 'login' : null); const showToast = (msg, type = 'success') => { const id = Date.now(); setToasts(prev => [...prev, { id, msg, type }]); setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 4000); }; const hasFetched = useRef(false); const fetchData = async () => { try { const vid = window.getVisitorId ? window.getVisitorId() : ''; // Anti-cache strict pour garantir que les données "Rafraîchir" sont bien nouvelles let url = window.ECO_CONFIG.apiBaseUrl + `sync&visitor_id=${vid}&_t=${Date.now()}`; if (scanId) url += `&scan=${scanId}`; if (threadToken) url += `&thread=${encodeURIComponent(threadToken)}`; const res = await fetch(url); if (res.status === 401) { setUserRole('public'); return; } if (res.status === 503) { showToast("Le site est en maintenance.", "error"); return; } const data = await res.json(); if (data.status === 'success') { setGlobalData(data.data); if (data.data.clients && data.data.clients.length > 0) { const roleRes = await fetch(window.ECO_CONFIG.apiBaseUrl + 'auth/me'); if (roleRes.ok) { const roleData = await roleRes.json(); if (roleData.status === 'success') setUserRole(roleData.data.role); } } } } catch (err) { console.error(err); } finally { setLoading(false); } }; useEffect(() => { if (hasFetched.current) return; hasFetched.current = true; fetchData(); const handleSessionExpired = () => { showToast("Votre session a expiré.", "error"); setUserRole('public'); setAuthMode('login'); }; window.addEventListener('session_expired', handleSessionExpired); return () => window.removeEventListener('session_expired', handleSessionExpired); }, []); // --------------------------------------------------------------------- // ROUTAGE DES VUES // --------------------------------------------------------------------- if (loading) return (

Chargement...

); // 1. Vue Riverain standard (Scan physique) if (scanId) { const c = globalData.chantiers?.find(x => x.id === scanId); return ( ); } // 2. Vue Riverain publique (Lien de réponse Support sans ID de panneau) if (threadToken && !scanId) { const publicChantier = { id: 'CONTACT_PUBLIC', name: 'Service client', location: 'Plateforme eco-panneau.fr', themeColor: '#0f172a', hasNoAds: true }; return ( ); } // 3. Délégation Architecte if (delegateToken) { return ( ); } // 4. Espaces connectés if (userRole === 'admin') { return ( ); } if (userRole === 'client') { return ( ); } // Écran public : modale de connexion if (authMode) { return ( { setUserRole(role); setLoading(true); hasFetched.current = false; fetchData(); }} showToast={showToast} urlParams={urlParams} onBack={() => setAuthMode(null)} /> ); } // Écran public : Vitrine Web (Landing Page) return ( setAuthMode(mode)} data={globalData} showToast={showToast} /> ); }; // ========================================================================= // 4. SYSTÈME DE TOASTS GLOBAUX // ========================================================================= const ToastContainer = ({ toasts }) => (
{toasts.map(t => (
{t.type === 'error' ? : } {t.msg}
))}
); // ========================================================================= // 5. INJECTION REACT DANS LE DOM (React 18 API) // ========================================================================= const root = ReactDOM.createRoot(document.getElementById('root')); root.render( ); /* EOF ===== [_main.jsx] =============== */