/** * ========================================================================= * PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0 * Composants : Éditeur de Panneaux et Modales de Création * ========================================================================= */ const { useState } = React; const { GripVertical, Edit, Trash2, Loader, Image, Modal, Plus, MapPin, FileDigit, Check, Palette, ExternalLink, Phone, Clock, Users, Eye, Smartphone, HardHat, Download, X, Settings, ArrowUp, ArrowDown, Zap, AlertTriangle } = window; const EntityCard = ({ data, type, index, lotIndex, onEdit, onDelete, onDragStart, onDragOver, onDrop, onDragEnter, onDragLeave, onMoveUp, onMoveDown }) => { const isLot = type === 'lot'; // Protection contre les URL blob mortes const logoSrc = data.logoId ? `?api=file/download&type=image&id=${data.logoId}` : (data.logoUrl?.startsWith('blob:') ? null : data.logoUrl); const [isDragOver, setIsDragOver] = useState(false); return (
onDragStart(e, {type, index, lotIndex})} onDragOver={(e) => { e.preventDefault(); onDragOver(e); }} onDragEnter={(e) => { e.preventDefault(); setIsDragOver(true); if(onDragEnter) onDragEnter(e); }} onDragLeave={(e) => { e.preventDefault(); setIsDragOver(false); if(onDragLeave) onDragLeave(e); }} onDrop={(e) => { e.preventDefault(); setIsDragOver(false); onDrop(e, {type, index, lotIndex}); }} className={`bg-white border rounded-xl p-3 flex flex-col gap-2 shadow-sm relative group transition ${isDragOver ? 'border-emerald-500 bg-emerald-50/30 ring-2 ring-emerald-500/20' : 'hover:border-slate-300'} ${isLot ? 'border-slate-300 mb-3' : 'border-slate-100'}`} >
{logoSrc && logo}

{data.name || 'Sans nom'}

{!isLot &&

{data.role || 'Rôle non défini'}

} {/* CORRECTION 5A : Affichage de l'adresse postale sur la carte */} {data.address &&

{data.address}

}
{/* Boutons Haut/Bas toujours visibles pour le mobile */}
{isLot && (
{data.entreprises?.map((ent, eIdx) => ( ))}
)}
); }; const EntityEditorModal = ({ editingEntity, setEditingEntity, saveEditedEntity, showToast }) => { const isLot = editingEntity.location.type === 'lot'; const [uploadingLogo, setUploadingLogo] = useState(false); const [isFetchingSirene, setIsFetchingSirene] = useState(false); const updateField = (field, val) => setEditingEntity({ ...editingEntity, data: { ...editingEntity.data, [field]: val } }); const handleLogoUpload = async (e) => { const file = e.target.files[0]; if(!file) return; setUploadingLogo(true); try { const id = await window.uploadFile(file, 'image'); updateField('logoId', id); updateField('logoUrl', URL.createObjectURL(file)); } catch(err) { if (showToast) showToast(err.message, "error"); } finally { setUploadingLogo(false); } }; const handleAutoFillSirene = async () => { const num = prompt("Saisissez le SIREN (9 chiffres) ou SIRET (14 chiffres) de l'entreprise :"); if (!num) return; const cleanNum = num.replace(/\D/g, ''); if (cleanNum.length !== 9 && cleanNum.length !== 14) return showToast("Le numéro doit contenir 9 ou 14 chiffres.", "error"); setIsFetchingSirene(true); try { const res = await fetch(window.ECO_CONFIG.apiBaseUrl + 'system/sirene', { method: 'POST', body: JSON.stringify({ q: cleanNum }) }); const responseData = await res.json(); if (responseData.status !== 'success') throw new Error(responseData.message); const apiData = responseData.data; if (apiData.results && apiData.results.length > 0) { const company = apiData.results[0]; updateField('name', company.nom_complet); const siege = company.siege || {}; const addressParts = [siege.numero_voie, siege.indice_repetition, siege.type_voie, siege.libelle_voie, siege.code_postal, siege.libelle_commune]; const fullAddress = addressParts.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); if (fullAddress) updateField('address', fullAddress); showToast("Informations récupérées !", "success"); } else { showToast("Aucune entreprise trouvée.", "error"); } } catch (err) { showToast("Le service d'annuaire est indisponible.", "error"); } setIsFetchingSirene(false); }; // Protection contre les URL blob mortes const logoSrc = editingEntity.data.logoId ? `?api=file/download&type=image&id=${editingEntity.data.logoId}` : (editingEntity.data.logoUrl?.startsWith('blob:') ? null : editingEntity.data.logoUrl); return ( setEditingEntity(null)} preventClose={uploadingLogo || isFetchingSirene}>
{!isLot && (
)}
updateField('name', e.target.value)} autoFocus className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-slate-800 outline-none transition" />
{!isLot && ( <>
updateField('role', e.target.value)} className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-slate-800 outline-none transition" placeholder="Ex: Architecte D.P.L.G" />
updateField('phone', e.target.value)} className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-slate-800 outline-none transition" />
updateField('email', e.target.value)} className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-slate-800 outline-none transition" />
{logoSrc && logo} {logoSrc && }
)}
); }; const PanneauEditorForm = ({ panneau, setPanneau, managingPanneau, onCancel, onSaveDraft, onPublish, onSaveActive, onPreview, onEditEntity, draggedItem, handleDragStart, handleDragOver, handleDrop, deleteEntity, isSaving, uiMode = 'simplifie', lockedFields = [], validationErrors = [], settings = {}, showToast }) => { const [touched, setTouched] = useState({}); const [uploadingStates, setUploadingStates] = useState({ image: false, pdf: false, moaLogo: false }); const [confirmDialog, setConfirmDialog] = useState(null); const [blacklistWarning, setBlacklistWarning] = useState(null); const updateField = (field, val) => setPanneau(prev => ({ ...prev, [field]: val })); const handleBlur = (field) => setTouched(prev => ({ ...prev, [field]: true })); const isError = (field) => validationErrors.length > 0 && (!panneau[field] || String(panneau[field]).trim() === ''); const isLocked = (field) => uiMode === 'delege' && lockedFields.includes(field); const handleImageUpload = async (e) => { const file = e.target.files[0]; if(!file) return; setUploadingStates(p => ({...p, image: true})); try { const id = await window.uploadFile(file, 'image'); updateField('imageId', id); updateField('imageUrl', URL.createObjectURL(file)); setTouched(p => ({...p, imageId: true})); } catch(err) { if (showToast) showToast(err.message, "error"); } finally { setUploadingStates(p => ({...p, image: false})); e.target.value = null; } }; const handlePdfUpload = async (e) => { const file = e.target.files[0]; if(!file) return; setUploadingStates(p => ({...p, pdf: true})); try { const id = await window.uploadFile(file, 'pdf'); updateField('pdfId', id); setTouched(p => ({...p, pdfId: true})); } catch(err) { if (showToast) showToast(err.message, "error"); } finally { setUploadingStates(p => ({...p, pdf: false})); e.target.value = null; } }; const handleMoaLogoUpload = async (e) => { const file = e.target.files[0]; if(!file) return; setUploadingStates(p => ({...p, moaLogo: true})); try { const id = await window.uploadFile(file, 'image'); updateField('maitreOuvrageLogoId', id); updateField('maitreOuvrageLogoUrl', URL.createObjectURL(file)); setTouched(p => ({...p, moaLogo: true})); } catch(err) { if (showToast) showToast(err.message, "error"); } finally { setUploadingStates(p => ({...p, moaLogo: false})); e.target.value = null; } }; const isUploading = Object.values(uploadingStates).some(v => v); // Protection stricte contre les URL blob const moaLogoSrc = panneau.maitreOuvrageLogoId ? `?api=file/download&type=image&id=${panneau.maitreOuvrageLogoId}` : (panneau.maitreOuvrageLogoUrl?.startsWith('blob:') ? null : panneau.maitreOuvrageLogoUrl); const mainImgSrc = panneau.imageId ? `?api=file/download&type=image&id=${panneau.imageId}` : (panneau.imageUrl?.startsWith('blob:') ? null : panneau.imageUrl); const handleCancelClick = () => { if (Object.keys(touched).length > 0 && !isSaving) { setConfirmDialog({ title: "Annuler les modifications", message: "Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir annuler et perdre ces changements ?", confirmText: "Oui, annuler", isDestructive: true, onConfirm: onCancel }); } else { onCancel(); } }; const checkForBlacklistedWords = () => { if (!settings?.blacklist) return null; const words = settings.blacklist.split(',').map(w => w.trim().toLowerCase()).filter(w => w); const fieldsToCheck = [ { name: 'Nom du chantier', val: panneau.name }, { name: 'Lieu', val: panneau.location }, { name: 'Maître d\'ouvrage', val: panneau.maitreOuvrage }, { name: 'Description', val: panneau.description } ]; for (let field of fieldsToCheck) { if (!field.val) continue; const content = field.val.toLowerCase(); for (let word of words) { if (content.includes(word)) { return { word, field: field.name }; } } } return null; }; const cleanPayload = () => { // PURGE LES URLS BLOB TEMPORAIRES AVANT SAUVEGARDE EN BDD const cleanDetails = { ...managingPanneau, name: managingPanneau.name?.trim() || 'Brouillon sans nom', location: managingPanneau.location?.trim() || '' }; delete cleanDetails.imageUrl; delete cleanDetails.maitreOuvrageLogoUrl; if (cleanDetails.intervenants) { cleanDetails.intervenants = cleanDetails.intervenants.map(i => { const ci = {...i}; delete ci.logoUrl; return ci; }); } if (cleanDetails.lots) { cleanDetails.lots = cleanDetails.lots.map(l => { const cl = {...l}; if (cl.entreprises) cl.entreprises = cl.entreprises.map(e => { const ce = {...e}; delete ce.logoUrl; return ce; }); return cl; }); } return cleanDetails; }; const executeSaveAction = (actionFn) => { const detected = checkForBlacklistedWords(); if (detected) { setBlacklistWarning({ detected, onConfirm: () => { setBlacklistWarning(null); // On transmet la fonction de sauvegarde, l'appelant s'occupera d'utiliser cleanPayload() actionFn(); } }); } else { actionFn(); } }; const handleMoveUp = (loc) => { let newInter = [...(panneau.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(panneau.lots || [])); if (loc.type === 'intervenant' && loc.index > 0) { const item = newInter.splice(loc.index, 1)[0]; newInter.splice(loc.index - 1, 0, item); } else if (loc.type === 'lot' && loc.index > 0) { const item = newLots.splice(loc.index, 1)[0]; newLots.splice(loc.index - 1, 0, item); } else if (loc.type === 'entreprise' && loc.index > 0) { const arr = newLots[loc.lotIndex].entreprises; const item = arr.splice(loc.index, 1)[0]; arr.splice(loc.index - 1, 0, item); } setPanneau({ ...panneau, intervenants: newInter, lots: newLots }); handleBlur('order'); }; const handleMoveDown = (loc) => { let newInter = [...(panneau.intervenants || [])]; let newLots = JSON.parse(JSON.stringify(panneau.lots || [])); if (loc.type === 'intervenant' && loc.index < newInter.length - 1) { const item = newInter.splice(loc.index, 1)[0]; newInter.splice(loc.index + 1, 0, item); } else if (loc.type === 'lot' && loc.index < newLots.length - 1) { const item = newLots.splice(loc.index, 1)[0]; newLots.splice(loc.index + 1, 0, item); } else if (loc.type === 'entreprise' && loc.index < newLots[loc.lotIndex].entreprises.length - 1) { const arr = newLots[loc.lotIndex].entreprises; const item = arr.splice(loc.index, 1)[0]; arr.splice(loc.index + 1, 0, item); } setPanneau({ ...panneau, intervenants: newInter, lots: newLots }); handleBlur('order'); }; const allSimpOptionsActive = settings?.simp_opt_description === '1' && settings?.simp_opt_image === '1' && settings?.simp_opt_theme === '1' && settings?.simp_opt_link === '1' && settings?.simp_opt_emergency === '1' && settings?.simp_opt_schedule === '1'; const isAdvancedMode = uiMode !== 'simplifie' || allSimpOptionsActive; const showOpt = (key) => { if (uiMode !== 'simplifie') return true; return settings?.[`simp_opt_${key}`] === '1'; }; return (

Identité du panneau

updateField('name', e.target.value)} onBlur={() => handleBlur('name')} onFocus={(e) => setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300)} disabled={isLocked('name') || (managingPanneau?.status === 'Actif' && managingPanneau?.offerType === 'purchase')} className={`w-full border-2 rounded-xl p-3 text-sm outline-none transition disabled:bg-slate-100 disabled:text-slate-400 disabled:cursor-not-allowed ${isError('name') ? 'border-red-500 bg-red-50' : 'border-slate-200 focus:border-emerald-500'}`} placeholder="Ex: Résidence Cambon" />
updateField('location', e.target.value)} onBlur={() => handleBlur('location')} onFocus={(e) => setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300)} disabled={isLocked('location') || (managingPanneau?.status === 'Actif' && managingPanneau?.offerType === 'purchase')} className={`w-full border-2 rounded-xl p-3 text-sm outline-none transition disabled:bg-slate-100 disabled:text-slate-400 disabled:cursor-not-allowed ${isError('location') ? 'border-red-500 bg-red-50' : 'border-slate-200 focus:border-emerald-500'}`} placeholder="Ex: Fréjus" />
{updateField('maitreOuvrage', e.target.value); handleBlur('maitreOuvrage');}} onFocus={(e) => setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300)} disabled={isLocked('maitreOuvrage')} className="flex-1 border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-emerald-500 outline-none transition disabled:bg-slate-100 disabled:text-slate-400 disabled:cursor-not-allowed" placeholder="Société ou nom" /> {isAdvancedMode && ( )}
{moaLogoSrc && (
Logo MOA {!isLocked('maitreOuvrage') && }
)}
{updateField('permitNumber', e.target.value); handleBlur('permitNumber');}} onFocus={(e) => setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300)} disabled={isLocked('permitNumber')} className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-emerald-500 outline-none transition uppercase disabled:bg-slate-100 disabled:text-slate-400 disabled:cursor-not-allowed" placeholder="PC 000 000 00 00000" />
0) ? 'bg-red-50 border-red-300' : 'bg-blue-50 border-blue-100'}`}>

0) ? 'text-red-900' : 'text-blue-900'}`}> Arrêté légal *

0) ? 'file:text-red-700 hover:file:bg-red-100' : 'file:text-blue-700 hover:file:bg-blue-100'} ${isLocked('pdfId') ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`} /> {uploadingStates.pdf &&

Upload en cours...

} {panneau.pdfId && !uploadingStates.pdf && (
PDF Actif {!isLocked('pdfId') && }
)}
{(showOpt('description') || showOpt('image') || showOpt('theme') || showOpt('link') || showOpt('emergency') || showOpt('schedule')) && (

Options avancées

{showOpt('description') &&