/**
* =========================================================================
* 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
Vous n'avez pas de compte ?
)}
{view === '2fa' && (
Sécurité requise
Veuillez saisir le code de vérification à 6 chiffres pour valider votre identité.
)}
{view === 'register' && (
Nouveau compte
)}
{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).
)}
{view === 'forgot' && (
Mot de passe oublié
Entrez votre e-mail pour recevoir un lien de réinitialisation.
)}
{view === 'reset_password' && (
Nouveau mot de passe
)}
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 (
);
// 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] =============== */