// 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 (