/**
* =========================================================================
* 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 &&

}
{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" />
{logoSrc &&

}
{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 (
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') &&
}
{showOpt('image') && (
{uploadingStates.image &&
Upload en cours...
}
{mainImgSrc && !uploadingStates.image && (

{!isLocked('imageId') &&
}
)}
)}
{showOpt('theme') && (
{window.predefinedThemes.map(c => ())}
)}
{showOpt('link') &&
{updateField('promoterLink', e.target.value); handleBlur('promoterLink');}} onFocus={(e) => setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300)} disabled={isLocked('promoterLink')} className="w-full 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" />
}
{showOpt('emergency') &&
}
{showOpt('schedule') &&
{updateField('noiseSchedule', e.target.value); handleBlur('noiseSchedule');}} onFocus={(e) => setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300)} disabled={isLocked('noiseSchedule')} className="w-full 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="8h à 17h" />
}
{isAdvancedMode && (
)}
)}
{isAdvancedMode && (
L'équipe du projet
Intervenants principaux
{panneau.intervenants?.map((inter, idx) => (
{deleteEntity(loc); handleBlur('intervenants');}} onDragStart={handleDragStart} onDragOver={handleDragOver} onDrop={(e, loc) => {handleDrop(e, loc); handleBlur('intervenants');}} onMoveUp={handleMoveUp} onMoveDown={handleMoveDown} />
))}
Sous-traitants par lot
{panneau.lots?.map((lot, lIdx) => (
{deleteEntity(loc); handleBlur('lots');}} onDragStart={handleDragStart} onDragOver={handleDragOver} onDrop={(e, loc) => {handleDrop(e, loc); handleBlur('lots');}} onMoveUp={handleMoveUp} onMoveDown={handleMoveDown} />
))}
)}
{onPreview && (
)}
{onSaveDraft && (
)}
{(onPublish || onSaveActive) && (
)}
{blacklistWarning && (
setBlacklistWarning(null)}
isDestructive={false}
zIndex="z-[260]"
/>
)}
{confirmDialog && setConfirmDialog(null)} />}
);
};
const PreviewModal = ({ panneau, onClose, initialType = 'virtual', showToast, interactions = [], refreshData }) => {
const [viewType, setViewType] = useState(initialType);
const [generatingA1, setGeneratingA1] = useState(false);
return (
e.stopPropagation()}>
{viewType === 'virtual' ? (
// MOCKUP IPHONE 17 (393x852) - Strictement délimité pour empêcher la fuite du composant
) : (
)}
);
};
Object.assign(window, { EntityCard, EntityEditorModal, PanneauEditorForm, PreviewModal });
/* EOF ===== [_socle_editeur.jsx] =============== */