// ECO-PANNEAU.FR - _react/clients/_clients_settings_profile.jsx window.pano_ClientSettingsProfile = ({ myClientData, data, showToast, refreshData, openLocalDialog, closeCurrentLayer, activeDialog, setHasUnsavedChanges }) => { const { useState, useEffect } = React; // ZÉRO-DETTE : Utilisation de notre Hook abstrait ! const { isMounted, safeFetch } = window.pano_useSafeFetch(); const [isSaving, setIsSaving] = useState(false); const [userHasEdited, setUserHasEdited] = useState(false); const [emailChangeConfig, setEmailChangeConfig] = useState(null); const [tempFullName, setTempFullName] = useState(''); const adminSettings = data.settings || {}; const optCollab = adminSettings.opt_collab || 'active'; const optMessaging = adminSettings.opt_messaging || 'active'; const optNewsletter = adminSettings.opt_newsletter || 'active'; const getInitialOption = (adminOpt, savedVal) => { if (savedVal !== undefined) return savedVal; if (adminOpt === 'optional_on') return true; return false; }; // 1. - Initialisation des données (Mémorisées pour comparaison fluide) const initialProfileData = React.useMemo(() => ({ name: myClientData.name || '', full_name: myClientData.full_name || '', email: myClientData.email || '', phone: myClientData.phone || '', address: myClientData.address || '', siret: myClientData.siret || '', tva: myClientData.tva || '' }), [myClientData.name, myClientData.full_name, myClientData.email, myClientData.phone, myClientData.address, myClientData.siret, myClientData.tva]); const initialClientOptions = React.useMemo(() => { let parsedOpts = {}; try { if (myClientData.uiMode && myClientData.uiMode.startsWith('{')) { parsedOpts = JSON.parse(myClientData.uiMode); } } catch(e) {} return { collab: getInitialOption(optCollab, parsedOpts.collab), messaging: getInitialOption(optMessaging, parsedOpts.messaging), newsletter: getInitialOption(optNewsletter, parsedOpts.newsletter), has_tva: parsedOpts.has_tva !== false, // Vrai par défaut simp_opt_description: getInitialOption(adminSettings.simp_opt_description || 'active', parsedOpts.simp_opt_description), simp_opt_image: getInitialOption(adminSettings.simp_opt_image || 'active', parsedOpts.simp_opt_image), simp_opt_theme: getInitialOption(adminSettings.simp_opt_theme || 'active', parsedOpts.simp_opt_theme), simp_opt_link: getInitialOption(adminSettings.simp_opt_link || 'active', parsedOpts.simp_opt_link), simp_opt_emergency: getInitialOption(adminSettings.simp_opt_emergency || 'active', parsedOpts.simp_opt_emergency), simp_opt_schedule: getInitialOption(adminSettings.simp_opt_schedule || 'active', parsedOpts.simp_opt_schedule), simp_opt_hide_intervenants: getInitialOption(adminSettings.simp_opt_hide_intervenants || 'disabled', parsedOpts.simp_opt_hide_intervenants), simp_opt_hide_lots: getInitialOption(adminSettings.simp_opt_hide_lots || 'disabled', parsedOpts.simp_opt_hide_lots), simp_opt_hide_legal: getInitialOption(adminSettings.simp_opt_hide_legal || 'disabled', parsedOpts.simp_opt_hide_legal) }; }, [myClientData.uiMode, optCollab, optMessaging, optNewsletter, adminSettings]); const [profileData, setProfileData] = useState(initialProfileData); const [clientOptions, setClientOptions] = useState(initialClientOptions); // États de sécurité (Ex-onglet Sécurité) const [passwordData, setPasswordData] = useState({ current: '', new: '', confirm: '' }); const [twoFactorSetup, setTwoFactorSetup] = useState(null); const [twoFactorCode, setTwoFactorCode] = useState(''); const [twoFactorSecret, setTwoFactorSecret] = useState(''); const [twoFactorQr, setTwoFactorQr] = useState(''); const updateProfile = (key, val) => { setProfileData(prev => ({...prev, [key]: val})); setUserHasEdited(true); }; const updateOption = (key, val) => { setClientOptions(prev => ({...prev, [key]: val})); setUserHasEdited(true); }; const isFormModified = JSON.stringify(profileData) !== JSON.stringify(initialProfileData) || JSON.stringify(clientOptions) !== JSON.stringify(initialClientOptions); // 1.1 - Détection des modifications pour l'avertissement de sauvegarde useEffect(() => { if (setHasUnsavedChanges) { setHasUnsavedChanges(isFormModified); } }, [isFormModified, setHasUnsavedChanges]); // 1.2 - Synchronisation douce Zéro-Effacement useEffect(() => { if (!userHasEdited) { setProfileData(initialProfileData); setClientOptions(initialClientOptions); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialProfileData, initialClientOptions]); // 2. - Import des dépendances UI const { FileTextIcon, SaveIcon, LoaderIcon, SearchIcon, SettingsIcon, RefreshCwIcon, EditIcon, UserIcon, PhoneIcon, LockIcon, ShieldCheckIcon, ShieldAlertIcon, QrCodeIcon, MailIcon, CopyIcon, EyeIcon, HardDriveIcon, FolderIcon, CreditCardIcon } = window.pano_getIcons(); const { FormInput, FormTextarea, Modal, Button, Toggle, TextLogo, CardGrid, NativeQRCode, PasswordPromptModal } = window.pano_getComponents(); const hasCollaborations = (data.panneaux || []).some(p => (p.client_uid === myClientData.id && p.collaborators && p.collaborators.length > 0) || (p.client_uid !== myClientData.id) ); const hasOptionalFeatures = [optCollab, optMessaging, optNewsletter].some(opt => opt === 'optional_on' || opt === 'optional_off'); const simpKeys = ['simp_opt_description', 'simp_opt_image', 'simp_opt_theme', 'simp_opt_link', 'simp_opt_emergency', 'simp_opt_schedule', 'simp_opt_hide_intervenants', 'simp_opt_hide_lots', 'simp_opt_hide_legal']; const hasOptionalSimp = simpKeys.some(k => adminSettings[k] === 'optional_on' || adminSettings[k] === 'optional_off'); // 3. - Logique métier const handleProfileSave = async (e) => { e.preventDefault(); if (!profileData.name.trim()) { showToast("Le nom de la société est obligatoire.", "error"); return; } if (hasCollaborations && !profileData.full_name.trim()) { showToast("Vous ne pouvez pas effacer votre nom complet car vous participez à des projets collaboratifs.", "error"); return; } if (clientOptions.has_tva !== false && (!profileData.tva || !profileData.tva.trim())) { showToast("Le numéro de TVA intracommunautaire est obligatoire si vous y êtes assujetti.", "error"); return; } const payload = { ...profileData, uiMode: JSON.stringify(clientOptions) }; const d = await safeFetch('clients/profile/update', { body: payload, setLoading: setIsSaving, successMessage: "Profil et options mis à jour avec succès." }); if (!isMounted.current) return; if (d) { setUserHasEdited(false); if (setHasUnsavedChanges) setHasUnsavedChanges(false); refreshData(); } }; const handleTvaToggle = (checked) => { updateOption('has_tva', checked); if (checked && profileData.siret && profileData.siret.length >= 9 && !profileData.tva) { const siren = profileData.siret.substring(0, 9); const tvaKey = (12 + 3 * (parseInt(siren, 10) % 97)) % 97; updateProfile('tva', `FR${tvaKey.toString().padStart(2, '0')}${siren}`); } else if (!checked) { updateProfile('tva', ''); } }; const handleSiretLookup = async () => { if (!profileData.siret || profileData.siret.length < 9) { showToast("Veuillez saisir un SIREN (9 chiffres) ou SIRET (14 chiffres) valide.", "error"); return; } const d = await safeFetch('systeme/siret_lookup', { body: { siret: profileData.siret }, setLoading: setIsSaving, successMessage: "Informations récupérées avec succès !" }); if (!isMounted.current) return; if (d && d.data) { const fetchedAddress = d.data.address || d.data.adresse || d.data.geo_adresse || ''; const newProfile = { ...profileData, name: d.data.name || profileData.name, address: fetchedAddress || profileData.address, siret: d.data.siret || profileData.siret }; if (clientOptions.has_tva !== false && d.data.tva) { newProfile.tva = d.data.tva; } setProfileData(newProfile); setUserHasEdited(true); } }; const handleEmailChangeRequest = async (e) => { e.preventDefault(); if (!emailChangeConfig.newEmail.trim() || !emailChangeConfig.newEmail.includes('@')) { showToast("Veuillez saisir un e-mail valide.", "error"); return; } const d = await safeFetch('clients/profile/request_email', { body: { email: emailChangeConfig.newEmail }, setLoading: setIsSaving, successMessage: "Un code de vérification a été envoyé à cette nouvelle adresse." }); if (!isMounted.current) return; if (d) { setEmailChangeConfig({ ...emailChangeConfig, step: 'verify' }); } }; const handleEmailChangeVerify = async (e) => { e.preventDefault(); if (emailChangeConfig.code.length !== 6) return; const d = await safeFetch('clients/profile/update_email', { body: { code: emailChangeConfig.code }, setLoading: setIsSaving, successMessage: "Votre adresse e-mail a été mise à jour avec succès. Veuillez vous reconnecter." }); if (!isMounted.current) return; if (d) { setEmailChangeConfig(null); setTimeout(() => window.location.href = '?', 1500); } }; const handlePasswordChange = async (e) => { e.preventDefault(); if (passwordData.new !== passwordData.confirm) { showToast("Les nouveaux mots de passe ne correspondent pas.", "error"); return; } if (passwordData.new.length < 8) { showToast("Le mot de passe doit contenir au moins 8 caractères.", "error"); return; } const d = await safeFetch('clients/password/update', { body: { oldPassword: passwordData.current, password: passwordData.new }, setLoading: setIsSaving, successMessage: "Mot de passe mis à jour avec succès." }); if (!isMounted.current) return; if (d) setPasswordData({ current: '', new: '', confirm: '' }); }; const start2FASetup = async (type) => { if (type === 'email') { setTwoFactorSetup('email'); return; } const d = await safeFetch('clients/profile/setup_totp', { setLoading: setIsSaving }); if (!isMounted.current) return; if (d) { setTwoFactorSecret(d.data.secret); setTwoFactorQr(d.data.otpauth); setTwoFactorSetup('authenticator'); } }; const confirm2FASetup = async (e) => { e.preventDefault(); if (twoFactorSetup === 'authenticator' && twoFactorCode.length !== 6) return; const method = twoFactorSetup === 'authenticator' ? 'totp' : 'email'; const d = await safeFetch('clients/profile/update_2fa', { body: { method: method }, setLoading: setIsSaving, successMessage: "Authentification à double facteur activée !" }); if (!isMounted.current) return; if (d) { setTwoFactorSetup(null); setTwoFactorCode(''); refreshData(); } }; const disable2FA = () => openLocalDialog('disable_2fa_pwd'); const handleCancel2FA = () => { setTwoFactorSetup(null); setTwoFactorCode(''); setTwoFactorSecret(''); setTwoFactorQr(''); }; const renderOption = (id, label, desc, adminStatus, clientValue, toggleKey) => { if (adminStatus === 'active' || adminStatus === 'disabled') return null; return (
{label} {desc}
updateOption(toggleKey, v)} />
); }; // --- CALCUL DU STOCKAGE --- const baseQuotaMb = parseInt(adminSettings.quota_global_mb || 5000, 10); const extraQuotaGb = parseInt(myClientData.storage_extra_gb || 0, 10); const extraPriceMo = parseFloat(myClientData.storage_price_mo || 0); const totalQuotaMb = baseQuotaMb + (extraQuotaGb * 1024); const usedStorageMb = parseFloat(myClientData.storage_used_mb || 0); let storagePercent = 0; if (totalQuotaMb > 0) { storagePercent = Math.min(100, Math.max(0, (usedStorageMb / totalQuotaMb) * 100)); } let progressColorClass = 'bg-emerald-500'; if (storagePercent > 90) { progressColorClass = 'bg-red-600'; } else if (storagePercent > 80) { progressColorClass = 'bg-orange-500'; } return (
{/* BLOC 1 : IDENTITÉ ET CONTACT */}

Identité et contact

Le nom de la société est obligatoire. Cette information sera utilisée pour la facturation et apparaîtra lors de vos invitations collaboratives sur .
updateProfile('name', e.target.value)} required disabled={isSaving} className="min-w-0 w-full" />
updateProfile('full_name', e.target.value)} required={hasCollaborations} disabled={isSaving || hasCollaborations} className="min-w-0 w-full" hint={!hasCollaborations ? "Obligatoire uniquement pour inviter ou rejoindre des collaborateurs." : "Cliquez pour modifier."} /> {hasCollaborations && !isSaving && (
{ e.preventDefault(); e.stopPropagation(); setTempFullName(profileData.full_name); openLocalDialog('locked_fullname_info'); }} title="Modifier le nom" /> )}

Une vérification de sécurité sera exigée pour valider votre nouvelle adresse e-mail.

updateProfile('phone', e.target.value)} disabled={isSaving} className="min-w-0 w-full" icon={} />
{/* BLOC 2 : INFORMATIONS DE FACTURATION */}

Informations de facturation

{ const cleanSiret = e.target.value.replace(/\D/g, '').substring(0, 14); updateProfile('siret', cleanSiret); }} placeholder="14 chiffres" className="flex-1 min-w-0 border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition font-bold disabled:bg-slate-50 disabled:opacity-50" maxLength={128} disabled={isSaving} />
!isSaving && handleTvaToggle(clientOptions.has_tva !== false ? false : true)} className={`flex items-center justify-between gap-3 bg-slate-50 border border-slate-100 p-4 rounded-xl shadow-sm min-w-0 ${isSaving ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:border-emerald-300 transition'}`}> Soumis à la TVA {Toggle &&
}
{clientOptions.has_tva !== false && FormInput && ( updateProfile('tva', e.target.value)} className="uppercase font-bold min-w-0 w-full" /> )}
updateProfile('address', e.target.value)} rows={3} className="min-w-0 w-full" />
{/* BLOC : ESPACE DE STOCKAGE (NOUVEAU) */}

Espace de stockage global

Consommation {usedStorageMb.toFixed(2)} Mo / {totalQuotaMb} Mo
{storagePercent > 90 ? (

Vous êtes presque à court d'espace ! Supprimez des fichiers ou modifiez votre forfait pour éviter le blocage de vos téléchargements.

) : storagePercent > 80 ? (

Votre espace de stockage se remplit. Pensez à faire du tri.

) : null} {extraQuotaGb > 0 && (
Forfait additionnel actif : +{extraQuotaGb} Go pour {extraPriceMo} € HT / mois.
)}
{/* BLOC 3 : FONCTIONNALITÉS DU COMPTE */} {hasOptionalFeatures && (

Fonctionnalités du compte

Gérez les options additionnelles de votre espace et de vos panneaux.

{renderOption('opt_collab', 'Travail en collaboration', 'Inviter des membres sur vos panneaux.', optCollab, clientOptions.collab, 'collab')} {renderOption('opt_messaging', 'Messagerie des riverains', 'Recevoir et répondre aux messages du voisinage.', optMessaging, clientOptions.messaging, 'messaging')} {renderOption('opt_newsletter', 'Info riverains', 'Informer les riverains abonnés des événements.', optNewsletter, clientOptions.newsletter, 'newsletter')}
)} {/* BLOC 4 : SIMPLIFICATION DE L'ÉDITEUR */} {hasOptionalSimp && (

Simplification de l'éditeur

Personnalisez les champs et onglets affichés lors de la création ou de la modification de vos panneaux.

{['simp_opt_description', 'simp_opt_image', 'simp_opt_theme', 'simp_opt_link', 'simp_opt_emergency', 'simp_opt_schedule'].some(k => adminSettings[k] === 'optional_on' || adminSettings[k] === 'optional_off') && (

Champs de l'éditeur

{renderOption('simp_opt_description', 'Description des travaux', 'Afficher le champ de description détaillée.', adminSettings.simp_opt_description || 'active', clientOptions.simp_opt_description, 'simp_opt_description')} {renderOption('simp_opt_image', 'Vue du projet (Image)', 'Permettre l\'upload d\'une image d\'aperçu.', adminSettings.simp_opt_image || 'active', clientOptions.simp_opt_image, 'simp_opt_image')} {renderOption('simp_opt_theme', 'Couleur du thème', 'Permettre la personnalisation des couleurs.', adminSettings.simp_opt_theme || 'active', clientOptions.simp_opt_theme, 'simp_opt_theme')} {renderOption('simp_opt_link', 'Lien vers le promoteur', 'Afficher le champ de lien web additionnel.', adminSettings.simp_opt_link || 'active', clientOptions.simp_opt_link, 'simp_opt_link')} {renderOption('simp_opt_emergency', 'Téléphone d\'urgence', 'Afficher le champ de contact d\'urgence.', adminSettings.simp_opt_emergency || 'active', clientOptions.simp_opt_emergency, 'simp_opt_emergency')} {renderOption('simp_opt_schedule', 'Horaires de nuisances', 'Afficher les horaires de travaux bruyants.', adminSettings.simp_opt_schedule || 'active', clientOptions.simp_opt_schedule, 'simp_opt_schedule')}
)} {['simp_opt_hide_intervenants', 'simp_opt_hide_lots', 'simp_opt_hide_legal'].some(k => adminSettings[k] === 'optional_on' || adminSettings[k] === 'optional_off') && (

Masquage d'onglets

{renderOption('simp_opt_hide_intervenants', 'Masquer l\'onglet Intervenants', 'Désactiver la gestion des intervenants.', adminSettings.simp_opt_hide_intervenants || 'disabled', clientOptions.simp_opt_hide_intervenants, 'simp_opt_hide_intervenants')} {renderOption('simp_opt_hide_lots', 'Masquer l\'onglet Lots de travaux', 'Désactiver la gestion des lots.', adminSettings.simp_opt_hide_lots || 'disabled', clientOptions.simp_opt_hide_lots, 'simp_opt_hide_lots')} {renderOption('simp_opt_hide_legal', 'Masquer l\'affichage de l\'Arrêté (Public)', 'Cacher le document légal au public.', adminSettings.simp_opt_hide_legal || 'disabled', clientOptions.simp_opt_hide_legal, 'simp_opt_hide_legal')}
)}
)} {/* BOUTON DE SAUVEGARDE PROFIL */}
{/* BLOC 5 : MODIFIER LE MOT DE PASSE */}

Modifier le mot de passe

setPasswordData({...passwordData, current: e.target.value})} required className="min-w-0 w-full" /> setPasswordData({...passwordData, new: e.target.value})} required className="min-w-0 w-full" /> setPasswordData({...passwordData, confirm: e.target.value})} required className="min-w-0 w-full" />
{/* BLOC 6 : AUTHENTIFICATION À DOUBLE FACTEUR */}

Authentification à double facteur

Sécurisez l'accès à vos panneaux et à votre coffre-fort légal sur .

{myClientData.twoFactorEnabled && ( Activé )}
{myClientData.twoFactorEnabled ? (

Votre compte est protégé par la double authentification. Vous devez saisir un code unique à chaque connexion depuis un nouvel appareil.

) : (
{!twoFactorSetup ? (
!isSaving && start2FASetup('authenticator')}>

Application (recommandé)

Utilisez Google Authenticator ou Authy pour générer des codes hors-ligne.

!isSaving && start2FASetup('email')}>

Par e-mail

Recevez un code de sécurité par e-mail à chaque connexion.

) : (

Configuration {twoFactorSetup === 'email' ? 'par e-mail' : 'via application'}

{twoFactorSetup === 'authenticator' && (

Scannez ce QR Code avec votre application d'authentification :

{NativeQRCode ? : }

Ou saisissez la clé manuellement :

)} {twoFactorSetup === 'email' && (

Vous allez recevoir un code de vérification à l'adresse e-mail de votre compte pour chaque connexion.

)} {twoFactorSetup === 'authenticator' && (
setTwoFactorCode(e.target.value.replace(/[^0-9]/g, '').slice(0,6))} placeholder="000000" className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-blue-500 outline-none transition font-black text-center text-xl sm:text-2xl tracking-[0.2em] sm:tracking-widest disabled:opacity-50 min-w-0" />
)}
)}
)}
{/* MODALES EXTERNES */} {emailChangeConfig && Modal && ( setEmailChangeConfig(null)} preventClose={isSaving} actions={(close) => emailChangeConfig.step === 'request' ? ( <> ) : ( ) } > {emailChangeConfig.step === 'request' ? (

Saisissez votre nouvelle adresse e-mail. Un code de vérification y sera envoyé pour confirmer qu'elle vous appartient bien.

setEmailChangeConfig({...emailChangeConfig, newEmail: e.target.value})} placeholder="Nouvelle adresse e-mail" required className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition font-bold min-w-0" autoFocus />
) : (

Saisissez le code à 6 chiffres envoyé à {emailChangeConfig.newEmail}.

setEmailChangeConfig({...emailChangeConfig, code: e.target.value.replace(/\D/g, '').slice(0, 6)})} placeholder="000000" required className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition text-center text-2xl tracking-[0.2em] font-black min-w-0" autoFocus />
{RefreshCwIcon && } Renvoyer
setEmailChangeConfig(null)} className={`text-xs font-bold text-slate-500 hover:text-slate-800 transition cursor-pointer shrink-0 ${isSaving ? 'opacity-50 pointer-events-none' : ''}`}> Annuler
)}
)} {activeDialog === 'disable_2fa_pwd' && PasswordPromptModal && ( { const d = await safeFetch('clients/profile/update_2fa', { body: { method: 'none', password: password }, setLoading: setIsSaving, successMessage: "Authentification à double facteur désactivée." }); if (!isMounted.current) return; if (d) { closeCurrentLayer(); refreshData(); } }} onCancel={closeCurrentLayer} isSaving={isSaving} /> )} {activeDialog === 'locked_fullname_info' && Modal && ( ( <> )} >
{ e.preventDefault(); if (tempFullName.trim()) { updateProfile('full_name', tempFullName); closeCurrentLayer(); } }} className="space-y-4 pb-2">

Vous pouvez modifier votre nom, mais vous ne pouvez pas l'effacer complètement car vous participez à des projets collaboratifs.

setTempFullName(e.target.value)} required autoFocus />
)}
); }; /* EOF ========== [_react/clients/_clients_settings_profile.jsx] */