// ECO-PANNEAU.FR - _react/admin/_admin_settings.jsx window.pano_AdminSettingsTab = ({ data, refreshData, openLocalModal, openLocalDialog, closeCurrentLayer, activeModal, activeDialog, targetId }) => { const { useState, useEffect } = React; // ZÉRO-DETTE : Utilisation de notre Hook abstrait ! const { isMounted, safeFetch } = window.pano_useSafeFetch(); // 2. - Routage et modales const { activeDialog: hookActiveDialog, openDialog: hookOpenDialog, closeCurrentLayer: hookCloseLayer } = window.pano_useUrlModal ? window.pano_useUrlModal() : {}; const routerActiveDialog = activeDialog || hookActiveDialog; const routerCloseLayer = closeCurrentLayer || hookCloseLayer; const routerOpenDialog = openLocalDialog || hookOpenDialog; // 3. - États const [isSaving, setIsSaving] = useState(false); const [settings, setSettings] = useState(data.settings || {}); const [prices, setPrices] = useState(data.prices || {}); const [pwdRequestData, setPwdRequestData] = useState(null); const [confirmConfig, setConfirmConfig] = useState(null); // États des notifications Web Push const [pushStatus, setPushStatus] = useState('checking'); const [pushSubscription, setPushSubscription] = useState(null); // 4. - Composants et Icônes const { CreditCardIcon, FileTextIcon, EyeIcon, ZapIcon, SaveIcon, LoaderIcon, SearchIcon, HardDriveIcon, RefreshCwIcon, BellIcon, SmartphoneIcon, ShieldAlertIcon } = window.pano_getIcons(); const { PriceInput, Modal, Toggle, FormInput, FormTextarea, PasswordPromptModal, ConfirmModal, TextLogo, Button, DataCard } = window.pano_getComponents(); // 5. - Initialisation des notifications Push useEffect(() => { const checkPushStatus = async () => { if (!('serviceWorker' in navigator) || !('PushManager' in window)) { if (isMounted.current) setPushStatus('unsupported'); return; } if (Notification.permission === 'denied') { if (isMounted.current) setPushStatus('denied'); return; } try { const registration = await navigator.serviceWorker.ready; if (!isMounted.current) return; const sub = await registration.pushManager.getSubscription(); if (!isMounted.current) return; if (sub) { setPushSubscription(sub); setPushStatus('subscribed'); } else { setPushStatus('unsubscribed'); } } catch (e) { if (isMounted.current) setPushStatus('unsupported'); } }; checkPushStatus(); }, [isMounted]); // 6. - Méthodes métier Push const subscribeToPush = async () => { setIsSaving(true); try { const permission = await Notification.requestPermission(); if (!isMounted.current) return; if (permission !== 'granted') { setPushStatus('denied'); setIsSaving(false); return; } const registration = await navigator.serviceWorker.ready; if (!isMounted.current) return; const vapidPublicKey = window.pano_CONFIG?.vapidPublicKey || 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'; const urlBase64ToUint8Array = (base64String) => { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; }; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) }); if (!isMounted.current) return; if (subscription) { const subData = JSON.parse(JSON.stringify(subscription)); await safeFetch('system/push_subscribe', { body: { endpoint: subData.endpoint, keys: subData.keys }, successMessage: "Notifications activées sur ce navigateur." }); if (!isMounted.current) return; setPushSubscription(subscription); setPushStatus('subscribed'); } } catch (e) { if (!isMounted.current) return; if (window.pano_showToast) window.pano_showToast("Erreur lors de l'activation des notifications.", "error"); } if (isMounted.current) setIsSaving(false); }; const unsubscribeFromPush = async () => { setIsSaving(true); try { if (pushSubscription) { const endpoint = pushSubscription.endpoint; await pushSubscription.unsubscribe(); if (!isMounted.current) return; setPushSubscription(null); setPushStatus('unsubscribed'); await safeFetch('system/push_unsubscribe', { body: { endpoint }, successMessage: "Notifications désactivées sur ce navigateur." }); if (!isMounted.current) return; } } catch (e) { if (!isMounted.current) return; if (window.pano_showToast) window.pano_showToast("Erreur lors de la désactivation.", "error"); } if (isMounted.current) setIsSaving(false); }; const revokeAllPushLinks = (e) => { setConfirmConfig({ title: "Rompre tous les liens de notification", message: "Êtes-vous sûr de vouloir révoquer les notifications push sur TOUS vos appareils (téléphones, ordinateurs, tablettes) ? Vous devrez les réactiver manuellement sur chaque appareil souhaité.", confirmText: "Oui, tout révoquer", isDestructive: true, onConfirm: async (evt) => { const d = await safeFetch('system/push_revoke_all', { setLoading: setIsSaving, successMessage: "Toutes les connexions de notifications ont été rompues." }); if (!isMounted.current) return; if (d) { if (pushSubscription) { try { await pushSubscription.unsubscribe(); } catch(err) {} if (isMounted.current) { setPushSubscription(null); setPushStatus('unsubscribed'); } } } routerCloseLayer(evt); }, onCancel: routerCloseLayer }); routerOpenDialog('confirm', null, e); }; // 7. - Méthodes métier Paramètres const updateSetting = (key, val) => setSettings({ ...settings, [key]: val }); const updatePrice = (key, val) => setPrices({ ...prices, [key]: val }); const handleTvaToggle = (checked) => { const hasTva = checked ? '1' : '0'; const newSettings = { ...settings, billing_has_tva: hasTva }; if (hasTva == 1 && settings.billing_siret && settings.billing_siret.length >= 9 && !settings.billing_tva) { const siren = settings.billing_siret.substring(0, 9); const tvaKey = (12 + 3 * (parseInt(siren, 10) % 97)) % 97; newSettings.billing_tva = `FR${tvaKey.toString().padStart(2, '0')}${siren}`; } setSettings(newSettings); }; const handleSiretLookup = async () => { if (!settings.billing_siret || settings.billing_siret.length < 9) { if (window.pano_showToast) window.pano_showToast("Veuillez saisir un SIREN (9) ou SIRET (14) valide.", "error"); return; } const d = await safeFetch('systeme/siret_lookup', { body: { siret: settings.billing_siret }, setLoading: setIsSaving, successMessage: "Informations récupérées avec succès." }); if (!isMounted.current) return; if (d && d.data) { const newSettings = { ...settings, billing_company: d.data.name || settings.billing_company, billing_address: d.data.address || settings.billing_address, billing_siret: d.data.siret || settings.billing_siret }; if (settings.billing_has_tva != 0 && d.data.tva) { newSettings.billing_tva = d.data.tva; } setSettings(newSettings); } else if (d && window.pano_showToast) { window.pano_showToast("Entreprise introuvable.", "error"); } }; const handleSave = async (e, forceDisableOrders = false) => { const isBillingComplete = !!( settings.billing_company?.trim() && settings.billing_address?.trim() && settings.billing_siret?.trim() && (settings.billing_has_tva == 0 || settings.billing_tva?.trim()) ); if (!isBillingComplete && settings.allow_new_purchases != 0 && forceDisableOrders !== true) { setConfirmConfig({ title: "Informations de facturation incomplètes", message: "Il manque des informations de facturation obligatoires (Raison sociale, SIRET, Adresse, ou TVA intracommunautaire). Si vous enregistrez maintenant, l'autorisation des commandes sera automatiquement désactivée sur la plateforme. Voulez-vous continuer ?", confirmText: "Désactiver les commandes et sauver", cancelText: "Annuler pour compléter", type: "warning", isDestructive: false, onConfirm: (evt) => { const newSet = { ...settings, allow_new_purchases: '0' }; setSettings(newSet); routerCloseLayer(evt); setTimeout(() => { if (isMounted.current) executePreSave(newSet, evt); }, 100); }, onCancel: routerCloseLayer }); routerOpenDialog('confirm', null, e); return; } await executePreSave(settings, e); }; const executePreSave = async (currentSettings, evt) => { const payload = {}; let isProtectedChanged = false; const protectedFields = [ 'maintenance', 'allow_new_purchases', 'blacklist', 'greylist', 'sec_ip_limit', 'mail_limit_count', 'mail_limit_window', 'sec_global_limit', 'sec_lock_min', 'sec_lock_max', 'notif_delay_standard', 'notif_delay_support', 'cron_mail_limit', 'cron_support_ratio', 'cron_batch_size', 'quota_step_gb' ]; for (const key in currentSettings) { const oldVal = String(data.settings?.[key] ?? '').replace(/\r\n/g, '\n'); const newVal = String(currentSettings[key] ?? '').replace(/\r\n/g, '\n'); if (oldVal !== newVal) { payload[key] = currentSettings[key]; if (protectedFields.includes(key)) { isProtectedChanged = true; } } } const priceKeys = ['rentalMo', 'purchase', 'hostingYr', 'boardFirst', 'boardAdd', 'noAds', 'storage']; priceKeys.forEach(k => { const oldPrice = parseFloat(data.prices?.[k]) || 0; const newPrice = parseFloat(prices[k]) || 0; if (oldPrice !== newPrice) { payload['price_' + k] = prices[k]; } }); if (Object.keys(payload).length === 0) { if (window.pano_showToast) window.pano_showToast("Aucune modification à sauvegarder.", "info"); return; } if (isProtectedChanged) { setPwdRequestData({ title: "Sécurité Zéro-Trust", desc: "Cette modification impacte le cœur du système et nécessite votre clé de sécurité globale (AES_KEY_CONFIRM) :", onConfirm: async (pwd, submitEvt) => { const d = await executeSave({ ...payload, pwd }); if (!isMounted.current) return; if (d) { routerCloseLayer(submitEvt); } } }); routerOpenDialog('pwd_request', null, evt); return; } await executeSave(payload); }; const executeSave = async (finalPayload) => { const d = await safeFetch('settings/update', { body: finalPayload, setLoading: setIsSaving }); if (!isMounted.current) return; if (d && d.status === 'success') { if (refreshData) refreshData(); if (d.data && d.data.forced_downgrade) { setConfirmConfig({ title: "Sécurité d'exploitation", message: "Vos paramètres ont été enregistrés. Toutefois, votre profil de facturation étant incomplet, le système a automatiquement verrouillé la 'Prise de commandes' pour éviter toute anomalie comptable ou juridique.", confirmText: "J'ai compris", cancelText: "Fermer", type: "warning", isDestructive: false, onConfirm: (evt) => routerCloseLayer(evt), onCancel: routerCloseLayer }); routerOpenDialog('confirm', null); } else { if (window.pano_showToast) window.pano_showToast("Paramètres sauvegardés avec succès !", "success"); } } return d; }; let hasChanges = false; for (const key in settings) { const oldVal = String(data.settings?.[key] ?? '').replace(/\r\n/g, '\n'); const newVal = String(settings[key] ?? '').replace(/\r\n/g, '\n'); if (oldVal !== newVal) { hasChanges = true; break; } } if (!hasChanges) { const priceKeys = ['rentalMo', 'purchase', 'hostingYr', 'boardFirst', 'boardAdd', 'noAds', 'storage']; for (const k of priceKeys) { const oldPrice = parseFloat(data.prices?.[k]) || 0; const newPrice = parseFloat(prices[k]) || 0; if (oldPrice !== newPrice) { hasChanges = true; break; } } } const priceFields = [ { key: 'rentalMo', label: 'Abonnement mensuel' }, { key: 'purchase', label: 'Achat unique' }, { key: 'hostingYr', label: 'Hébergement annuel' }, { key: 'boardFirst', label: 'Impression A1 (1er)' }, { key: 'boardAdd', label: 'Impression A1 (suiv.)' }, { key: 'noAds', label: 'Option sans pub' } ]; return ( <>
Configuration globale de
Affichez des messages d'information globaux aux utilisateurs connectés ou aux riverains.
Navigateur actuel
{pushStatus === 'checking' && "Vérification..."} {pushStatus === 'unsupported' && "Non supporté par ce navigateur"} {pushStatus === 'denied' && "Bloqué par le navigateur"} {pushStatus === 'unsubscribed' && "Désactivé"} {pushStatus === 'subscribed' && Actif sur cet appareil}
Urgence / Sécurité
Révoquer tous les appareils liés