// ECO-PANNEAU.FR - __react/socle/_socle_editeur.jsx const { useState, useEffect, useMemo, useRef } = React; window.pano_PanneauEditorForm = ({ panneau, setPanneau, managingPanneau, originalPanneau, onCancel, onSaveDraft, onPublish, onDiscardDraft, onPreview, onEditEntity, draggedItem, handleDragStart, handleDragOver, handleDrop, deleteEntity, isSaving, validationErrors = [], lockedFields = [], data, myClientData, currentUserRole }) => { // 1. - SÉCURITÉ ANTI-FUITE DE MÉMOIRE : Utilisation du Hook Global Zéro-Dette const { isMounted, safeFetch } = window.pano_useSafeFetch(); const [activeTab, setActiveTab] = useState('infos'); const [uploadProgress, setUploadProgress] = useState(null); const [confirmConfig, setConfirmConfig] = useState(null); // Nouveaux états pour l'intercepteur de facturation const [checkoutPrompt, setCheckoutPrompt] = useState(false); const [isLocalSaving, setIsLocalSaving] = useState(false); // ÉTATS DE VERROUILLAGE COLLABORATIF (PESSIMISTIC LOCKING) const lockStatusRef = useRef('acquiring'); const [lockState, setLockState] = useState('acquiring'); // acquiring, acquired, denied, idle const [lockInfo, setLockInfo] = useState(''); const lastActivityRef = useRef(Date.now()); const icons = useMemo(() => window.pano_getIcons(), []); const { SaveIcon, CheckCircleIcon, EyeIcon, LoaderIcon, AlertTriangleIcon, XIcon, ShieldIcon, EditIcon, FileTextIcon, BuildingIcon, UsersIcon, SettingsIcon, RefreshCwIcon, ShoppingCartIcon, LockIcon, ClockIcon } = icons; const comps = useMemo(() => window.pano_getComponents(), []); const { Modal, Button, StatusBadge, ConfirmModal, PanneauEditorInfos, PanneauEditorIntervenants, PanneauEditorLots, PanneauEditorAutres } = comps; const safeRole = currentUserRole || (myClientData ? 'client' : 'admin'); const ownerId = String(panneau?.client_uid || panneau?.clientName || '').trim(); const myId = String(myClientData?.id || '').trim(); const isOwner = ownerId === myId && myId !== ''; const isMe = (uid) => String(uid) === myId || String(uid) === String(myClientData?.email_hash || ''); const myCollab = !isOwner && myClientData ? panneau?.collaborators?.find(c => isMe(c.uid)) : null; const isDemo = panneau?.id === 'demo-panneau' || panneau?.offerType === 'demo'; const canEdit = safeRole === 'admin' || safeRole === 'delegate' || isOwner || myCollab?.rights?.can_edit || isDemo; const isDelegatedUser = safeRole === 'delegate'; const isFieldLocked = (fieldId) => lockedFields.includes(fieldId); const isDraft = panneau?.status === 'Brouillon' || panneau?.status === 'Réservé'; // --- LOGIQUE DE VERROUILLAGE COLLABORATIF (PESSIMISTIC LOCKING) --- // Suivi de l'activité utilisateur useEffect(() => { const updateActivity = () => { lastActivityRef.current = Date.now(); }; window.addEventListener('mousemove', updateActivity); window.addEventListener('keydown', updateActivity); window.addEventListener('click', updateActivity); window.addEventListener('scroll', updateActivity, true); return () => { window.removeEventListener('mousemove', updateActivity); window.removeEventListener('keydown', updateActivity); window.removeEventListener('click', updateActivity); window.removeEventListener('scroll', updateActivity, true); }; }, []); // Acquisition du verrou et Heartbeat const changeLockState = (s) => { lockStatusRef.current = s; setLockState(s); }; useEffect(() => { if (!canEdit || isDemo || panneau.id === 'demo-panneau' || panneau.status === 'Réservé') { changeLockState('acquired'); return; } let isSubscribed = true; let heartbeatInterval; const checkLock = async () => { if (lockStatusRef.current === 'idle' || lockStatusRef.current === 'denied') return; const idleLimit = parseInt(data?.settings?.editor_lock_timeout || 15, 10) * 60 * 1000; if (Date.now() - lastActivityRef.current > idleLimit) { changeLockState('idle'); await safeFetch('panneaux/unlock', { body: { id: panneau.id }, silent: true }); return; } const d = await safeFetch('panneaux/lock', { body: { id: panneau.id }, silent: true }); if (!isSubscribed) return; if (d && d.status === 'success') { if (lockStatusRef.current !== 'acquired') changeLockState('acquired'); } else if (d && d.status === 'locked') { changeLockState('denied'); setLockInfo(d.data?.owner_name || 'Un collaborateur'); } else { // Fallback (ex: pas de connexion), on assume l'accès pour ne pas bloquer if (lockStatusRef.current !== 'acquired') changeLockState('acquired'); } }; checkLock(); heartbeatInterval = setInterval(checkLock, 30000); // Heartbeat toutes les 30 secondes return () => { isSubscribed = false; clearInterval(heartbeatInterval); if (lockStatusRef.current === 'acquired') { // Requête asynchrone robuste au démontage pour libérer instantanément try { fetch((window.pano_CONFIG?.apiBaseUrl || '?api=') + 'panneaux/unlock&silent=1', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-silent': '1' }, body: JSON.stringify({ id: panneau.id }), keepalive: true }); } catch(e) {} } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [panneau.id, canEdit, isDemo]); const handleRegainControl = async () => { setIsLocalSaving(true); const d = await safeFetch('panneaux/lock/regain', { body: { id: panneau.id } }); setIsLocalSaving(false); if (!isMounted.current) return; if (d && d.status === 'success') { if (d.data && d.data.panneau) { if (window.pano_showToast) window.pano_showToast("Le panneau a été modifié pendant votre absence. Données actualisées.", "info"); setManagingPanneau(d.data.panneau); } else { if (window.pano_showToast) window.pano_showToast("Vous pouvez reprendre là où vous en étiez.", "success"); } lastActivityRef.current = Date.now(); changeLockState('acquired'); } else if (d && d.status === 'locked') { changeLockState('denied'); setLockInfo(d.data?.owner_name || 'Un collaborateur'); } }; // --- FIN LOGIQUE DE VERROUILLAGE --- // 1.1 - Calculateur de préférences de simplification (Zéro-Trust UI) const uiPrefs = useMemo(() => { if (safeRole === 'admin') { return { simp_opt_description: true, simp_opt_image: true, simp_opt_theme: true, simp_opt_link: true, simp_opt_emergency: true, simp_opt_schedule: true, simp_opt_hide_intervenants: false, simp_opt_hide_lots: false, simp_opt_hide_legal: false }; } const adminSettings = data?.settings || {}; let clientOpts = {}; try { if (myClientData?.uiMode) clientOpts = JSON.parse(myClientData.uiMode); } catch(e) {} const resolvePref = (key, defaultAdminVal) => { const adminVal = adminSettings[key] || defaultAdminVal; if (adminVal === 'active') return true; if (adminVal === 'disabled') return false; if (clientOpts[key] !== undefined) return clientOpts[key]; return adminVal === 'optional_on'; }; return { simp_opt_description: resolvePref('simp_opt_description', 'active'), simp_opt_image: resolvePref('simp_opt_image', 'active'), simp_opt_theme: resolvePref('simp_opt_theme', 'active'), simp_opt_link: resolvePref('simp_opt_link', 'active'), simp_opt_emergency: resolvePref('simp_opt_emergency', 'active'), simp_opt_schedule: resolvePref('simp_opt_schedule', 'active'), simp_opt_hide_intervenants: resolvePref('simp_opt_hide_intervenants', 'disabled'), simp_opt_hide_lots: resolvePref('simp_opt_hide_lots', 'disabled'), simp_opt_hide_legal: resolvePref('simp_opt_hide_legal', 'disabled') }; }, [safeRole, data?.settings, myClientData?.uiMode]); // Filtre les erreurs de validation si la section légale est masquée const filteredValidationErrors = useMemo(() => { if (!validationErrors) return []; if (uiPrefs.simp_opt_hide_legal) { return validationErrors.filter(err => !err.toLowerCase().includes("arrêté légal") && !err.toLowerCase().includes("pdf")); } return validationErrors; }, [validationErrors, uiPrefs.simp_opt_hide_legal]); const hasUnsavedChanges = useMemo(() => { if (!originalPanneau || !originalPanneau.draft_data) return false; if (Object.keys(originalPanneau.draft_data).length === 0) return false; const cleanObj = (obj) => { const copy = { ...obj }; delete copy.draft_data; delete copy.updated_at; delete copy.created_at; delete copy.last_rental_alert; delete copy.admin_seen; return copy; }; const live = cleanObj(originalPanneau); const draft = cleanObj({ ...originalPanneau, ...originalPanneau.draft_data }); return JSON.stringify(live) !== JSON.stringify(draft); }, [originalPanneau]); const isModified = useMemo(() => { return JSON.stringify(managingPanneau) !== JSON.stringify(originalPanneau); }, [managingPanneau, originalPanneau]); const displayPanneau = useMemo(() => { return panneau?.draft_data && Object.keys(panneau.draft_data).length > 0 ? { ...panneau, ...panneau.draft_data } : panneau; }, [panneau]); const handleSetPanneau = (action) => { if (!canEdit) return; setPanneau((prevManaging) => { const baseObj = prevManaging || originalPanneau; if (!baseObj) return null; const currentDisplay = baseObj.draft_data && Object.keys(baseObj.draft_data).length > 0 ? { ...baseObj, ...baseObj.draft_data } : baseObj; const nextDisplay = typeof action === 'function' ? action(currentDisplay) : action; if (isDraft) { const cleanNext = { ...nextDisplay }; delete cleanNext.draft_data; return cleanNext; } else { const draftUpdates = { ...nextDisplay }; delete draftUpdates.draft_data; return { ...baseObj, draft_data: draftUpdates }; } }); }; const discardDraft = () => { if (!canEdit) return; setConfirmConfig({ title: "Rétablir la version en ligne", message: "Voulez-vous vraiment annuler vos modifications ? Toutes les saisies non publiées seront perdues et la version en ligne sera restaurée dans l'éditeur.", confirmText: "Oui, rétablir", type: 'error', isDestructive: true, onConfirm: () => { setConfirmConfig(null); if (onDiscardDraft) onDiscardDraft(); }, onCancel: () => { setConfirmConfig(null); } }); }; // 1.2 - INTERCEPTEUR DE FACTURATION FLUIDE (Zéro-Friction UX) const proceedToCheckout = () => { const urlModal = window.pano_useUrlModal ? window.pano_useUrlModal() : null; if (urlModal && urlModal.replaceCurrentLayer) { urlModal.replaceCurrentLayer('dialog', 'select_offer', panneau.id, false); } }; const handleCheckoutRequest = () => { if (isModified || hasUnsavedChanges) { setCheckoutPrompt(true); } else { proceedToCheckout(); } }; const handleSaveDraftAndCheckout = async () => { setIsLocalSaving(true); // --- JUST-IN-TIME FETCH (JIT) POUR ÉRADIQUER LE CONFLIT D'ÉDITION --- let freshOriginal = null; if (panneau.id !== 'demo-panneau') { try { const dSync = await safeFetch('sync', { method: 'GET', silent: true }); if (dSync && dSync.data && dSync.data.panneaux) { freshOriginal = dSync.data.panneaux.find(x => x.id === panneau.id); } } catch(e) {} } const originalToUse = freshOriginal || originalPanneau; // --- ANTI-CONFLIT D'ÉDITION --- const detailsWithProtected = { ...panneau }; if (originalToUse) { Object.assign(detailsWithProtected, { updated_at: originalToUse.updated_at, shipping_status: originalToUse.shipping_status, tracking_number: originalToUse.tracking_number, tracking_link: originalToUse.tracking_link, admin_seen: originalToUse.admin_seen }); } const payload = { id: panneau.id, status: panneau.status, offerType: panneau.offerType, currentRate: panneau.currentRate, physicalPanels: panneau.physicalPanels, details: detailsWithProtected }; const d = await safeFetch('panneaux', { body: payload, successMessage: "Brouillon enregistré avec succès." }); if (!isMounted.current) return; setIsLocalSaving(false); if (d) { setCheckoutPrompt(false); proceedToCheckout(); } }; const renderActions = (requestClose) => { return (
{canEdit && (hasUnsavedChanges || isModified) && !isDraft && ( )} {canEdit && ( )} {canEdit && safeRole !== 'delegate' && ( )}
); }; let navItems = [ { id: 'infos', label: 'Projet et autorisations', icon: } ]; if (!uiPrefs.simp_opt_hide_intervenants) { navItems.push({ id: 'intervenants', label: 'Intervenants', icon: }); } if (!uiPrefs.simp_opt_hide_lots) { navItems.push({ id: 'lots', label: 'Lots de travaux', icon: }); } navItems.push({ id: 'autres', label: 'Affichage et annexes', icon: }); return ( <> Assurez-vous de l'exactitude des informations légales.} maxWidth="max-w-5xl" onClose={(e) => { if (panneau && panneau.status === 'Réservé') { safeFetch('panneaux/delete', { body: { id: panneau.id, force_immediate: true }, silent: true }); } if (onCancel) onCancel(e); }} preventClose={isSaving || isLocalSaving} requireConfirm={isModified} tabs={navItems} activeTab={activeTab} onTabChange={setActiveTab} actions={renderActions} > {filteredValidationErrors.length > 0 && (

{AlertTriangleIcon && } Impossible de publier. {filteredValidationErrors.length > 1 ? 'Éléments bloquants :' : 'Élément bloquant :'}

    {filteredValidationErrors.map((err, idx) =>
  • {err}
  • )}
)}
{StatusBadge && } {!canEdit && ( {EyeIcon && } Lecture seule )} {(hasUnsavedChanges || isModified) && ( {EditIcon && } Modifié )} {isDelegatedUser && ( {ShieldIcon && } Saisie Déléguée )}
{uploadProgress && (
{LoaderIcon && } {uploadProgress.label}
)}
{PanneauEditorInfos && }
{PanneauEditorIntervenants && }
{PanneauEditorLots && }
{PanneauEditorAutres && }
{/* OVERLAY DEAD SCREEN (PESSIMISTIC LOCKING) */} {(lockState === 'denied' || lockState === 'idle') && (
{lockState === 'denied' ? ( <> {LockIcon && }

Panneau en cours d'édition

Ce panneau est actuellement verrouillé car {lockInfo} est en train de le modifier. Veuillez patienter.

) : ( <> {ClockIcon && }

Session expirée

Suite à une inactivité prolongée, le panneau a été déverrouillé pour permettre à votre équipe d'y accéder.

)}
)} {/* MODALE INTERCEPTEUR DE FACTURATION */} {checkoutPrompt && Modal && ( setCheckoutPrompt(false)} preventClose={isLocalSaving} actions={(close) => (
)} >

Vous avez modifié des informations sur ce panneau. Souhaitez-vous enregistrer ces modifications en tant que brouillon avant de passer à la commande de réassort ?

)} {confirmConfig && ConfirmModal && } ); }; /* EOF ========== [__react/socle/_socle_editeur.jsx] */