/**
* =========================================================================
* PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0
* Composants UI Globaux, Icônes, Utilitaires et Moteur PDF jsPDF
* =========================================================================
*/
const { useState, useEffect, useRef } = React;
// =========================================================================
// 1. CONFIGURATION GLOBALE ET MÉTHODES D'API
// =========================================================================
window.ECO_CONFIG = window.ECO_CONFIG || {};
window.ECO_CONFIG.pdfFontsCache = window.ECO_CONFIG.pdfFontsCache || {};
window.uploadFile = async (file, type, delegateToken = window.CURRENT_DELEGATE_TOKEN, isPrivate = false) => {
const formData = new FormData(); formData.append('file', file); formData.append('type', type);
if (isPrivate) formData.append('is_private', 'true'); if (delegateToken) formData.append('delegate_token', delegateToken);
try {
const res = await fetch('?api=upload/file', { method: 'POST', body: formData });
if (!res.ok) throw new Error('Erreur HTTP serveur : ' + res.status);
const data = await res.json(); if (data.status === 'success') return data.data.fileId;
throw new Error(data.message || 'Erreur inconnue lors du téléchargement');
} catch (err) { throw new Error("Échec de l'envoi : " + err.message); }
};
window.deleteFile = async (id, type, delegateToken = window.CURRENT_DELEGATE_TOKEN) => {
try { await fetch('?api=file/delete', { method: 'POST', body: JSON.stringify({ id, type, delegate_token: delegateToken }), headers: { 'Content-Type': 'application/json' } }); } catch (err) {}
};
// =========================================================================
// 2. BIBLIOTHÈQUE D'ICÔNES SVG (Depuis _constantes.jsx)
// =========================================================================
const predefinedThemes = window.predefinedThemes || [];
const Icons = {};
if (window.svgPaths) {
Object.keys(window.svgPaths).forEach(key => {
Icons[key] = ({ size = 24, className = "", style = {}, title }) => React.createElement("svg", {
xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24",
fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round",
className, style, dangerouslySetInnerHTML: title ? { __html: `
${title}${window.svgPaths[key]}` } : { __html: window.svgPaths[key] }
});
});
}
const {
Leaf, QrCode, Home, Building, Users, AlertTriangle, CheckCircle, Search, Menu, X, Plus, MessageSquare,
Download, MapPin, Shield, LifeBuoy, ArrowRight, ArrowLeft, ArrowUp, ArrowDown, UserCheck, Smartphone, Check, Settings, FileSignature, HardHat, FileDigit, Info, Edit,
Activity, Server, HardDrive, Wifi, Terminal, RefreshCw, CreditCard, Percent, Tag, Lock, ShoppingCart, FileText, Loader, Copy,
LogIn, LogOut, Calendar, Clock, ExternalLink, Palette, Globe, Phone, Mail, GripVertical, Trash2, Save, Eye, Power, AlertOctagon, Database, KeyRound, History, ShieldAlert, Scale, Zap, UserCircle, ShieldCheck, FileCheck, Image, Package, Archive, BarChart, Newspaper, LinkIcon, Bell, BellRing, HardDriveDownload, Share2
} = Icons;
// =========================================================================
// 3. UTILITAIRES ET COMPOSANTS GLOBAUX
// =========================================================================
class ErrorBoundary extends React.Component {
constructor(props) { super(props); this.state = { hasError: false, error: null }; }
static getDerivedStateFromError(error) { return { hasError: true, error }; }
render() {
if (this.state.hasError) return ({this.state.error.toString()} );
return this.props.children;
}
}
const NativeQRCode = ({ value, size = 120, padding = 0, className = "", ecc = "H", onRendered }) => {
const containerRef = useRef(null);
useEffect(() => {
if (containerRef.current && window.QRCode) {
containerRef.current.innerHTML = '';
const text = value || "https://eco-panneau.fr";
const qr = new window.QRCode({ content: text, padding: padding, width: 256, height: 256, color: "#0f172a", background: "transparent", ecl: ecc });
const svg = qr.svg();
containerRef.current.innerHTML = svg;
const svgElement = containerRef.current.querySelector('svg');
if (svgElement) {
// Suppression stricte des dimensions fixes pour un vrai comportement vectoriel adaptatif
svgElement.removeAttribute('width');
svgElement.removeAttribute('height');
svgElement.style.width = '100%';
svgElement.style.height = '100%';
svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet');
svgElement.style.display = 'block';
if(onRendered) onRendered(svg);
}
}
}, [value, ecc, padding, onRendered]);
return ();
};
const SimpleBarChart = ({ data }) => {
if (!data || data.length === 0) return Pas encore assez de données de scan.
;
const max = Math.max(...data.map(d => d.count), 1);
return (
{data.map((d, i) => (
{d.count}
{new Date(d.date).toLocaleDateString('fr-FR', {day:'2-digit', month:'2-digit'})}
))}
);
};
// =========================================================================
// 4. MOTEUR DE GÉNÉRATION PDF NATIF (jsPDF) ET APERÇU A1 FLUIDE
// =========================================================================
window.generateA1PDF = async (chantier) => {
const { jsPDF } = window.jspdf;
// Format A1 paysage : 841 x 594 mm
const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: [841, 594] });
let fontLoaded = false;
try {
const loadFont = async (fontName, fontStyle, url) => {
const cacheKey = `${fontName}-${fontStyle}`;
let base64 = window.ECO_CONFIG.pdfFontsCache[cacheKey];
if (!base64) {
const resp = await fetch(url);
if (!resp.ok) throw new Error("Network");
const buffer = await resp.arrayBuffer();
let binary = ''; const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); }
base64 = window.btoa(binary);
window.ECO_CONFIG.pdfFontsCache[cacheKey] = base64;
}
const fileName = `${cacheKey}.ttf`;
doc.addFileToVFS(fileName, base64);
doc.addFont(fileName, fontName, fontStyle);
};
await Promise.all([
loadFont("Roboto", "normal", "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Regular.ttf"),
loadFont("Roboto", "bold", "https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.7/fonts/Roboto/Roboto-Medium.ttf")
]);
fontLoaded = true;
} catch(e) {
console.warn("Échec du chargement des polices UTF-8. Bascule sur Helvetica.");
}
const sanitizeForPDF = (str) => {
if (!str) return '';
if (fontLoaded) return str.toString().replace(/[\u1000-\uFFFF]/g, '');
let s = str.toString().replace(/[’‘`]/g, "'").replace(/[“”«»]/g, '"').replace(/[–—]/g, '-').replace(/œ/g, 'oe').replace(/Œ/g, 'Oe').replace(/€/g, 'EUR');
return s.replace(/[^\x00-\xFF]/g, "");
};
const mainFont = fontLoaded ? "Roboto" : "helvetica";
doc.setFillColor(255, 255, 255); doc.rect(0, 0, 841, 594, 'F');
const hexToRgb = (hex) => {
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex || '#059669');
return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : { r:5, g:150, b:105 };
};
const themeRgb = hexToRgb(chantier.themeColor);
// Bandeau supérieur couleur
doc.setFillColor(themeRgb.r, themeRgb.g, themeRgb.b); doc.rect(0, 0, 841, 45, 'F');
// Bandeau inférieur pub
if (!chantier.hasNoAds) {
doc.setFillColor(15, 23, 42); doc.rect(0, 594 - 45, 841, 45, 'F');
doc.setFillColor(16, 185, 129); doc.rect(0, 594 - 50, 841, 5, 'F');
doc.setTextColor(255, 255, 255); doc.setFont(mainFont, "bold"); doc.setFontSize(35);
doc.text("eco-panneau.fr | Professionnels du BTP : digitalisez vos obligations.", 420.5, 594 - 18, { align: 'center' });
}
// Colonne de gauche (QR Code)
const qrSize = 210; const qrX = 50; const qrY = 110;
doc.setDrawColor(226, 232, 240); doc.setLineWidth(3); doc.setFillColor(255, 255, 255);
doc.roundedRect(qrX - 15, qrY - 15, qrSize + 30, qrSize + 30, 5, 5, 'FD');
// Retrait du padding=4 natif pour un dessin précis et centré dans notre box jsPDF
const qr = new window.QRCode({ content: `https://eco-panneau.fr/?scan=${chantier.id}`, padding: 0, width: 256, height: 256, ecl: "H" });
const modules = qr.qrcode.modules; const moduleCount = modules.length; const moduleSize = qrSize / moduleCount;
doc.setFillColor(15, 23, 42);
for (let row = 0; row < moduleCount; row++) {
for (let col = 0; col < moduleCount; col++) {
if (modules[row][col]) { doc.rect(qrX + col * moduleSize, qrY + row * moduleSize, moduleSize, moduleSize, 'F'); }
}
}
doc.setTextColor(15, 23, 42); doc.setFontSize(40); doc.setFont(mainFont, "bold");
doc.text("Scannez pour les informations", qrX + qrSize/2, qrY + qrSize + 50, { align: 'center' });
doc.text("légales et utiles", qrX + qrSize/2, qrY + qrSize + 70, { align: 'center' });
// Séparateur vertical
doc.setDrawColor(226, 232, 240); doc.setLineWidth(2); doc.line(340, 80, 340, chantier.hasNoAds ? 550 : 500);
const drawAutoText = (text, x, y, maxW, maxH, startSize, fontName, fontStyle, color) => {
const cleanText = sanitizeForPDF(text);
if (!cleanText || cleanText.trim() === '') return;
doc.setFont(fontName, fontStyle); doc.setTextColor(color[0], color[1], color[2]);
let fontSize = startSize; doc.setFontSize(fontSize);
let lines = doc.splitTextToSize(cleanText, maxW);
let loopCount = 0;
// Multiplicateur 0.352 pour convertir les points en mm
while (lines.length * (fontSize * 0.352) > maxH && fontSize > 20) {
loopCount++; if (loopCount > 20) break;
fontSize -= 2; doc.setFontSize(fontSize); lines = doc.splitTextToSize(cleanText, maxW);
}
if (lines.length * (fontSize * 0.352) > maxH) {
const maxAllowedLines = Math.floor(maxH / (fontSize * 0.352));
lines = lines.slice(0, maxAllowedLines);
if (lines.length > 0) lines[lines.length - 1] += "...";
}
doc.text(lines, x, y);
};
// Colonne de droite (Textes)
const textX = 380; const maxTextW = 420;
doc.setTextColor(themeRgb.r, themeRgb.g, themeRgb.b); doc.setFontSize(45); doc.setFont(mainFont, "bold");
doc.text("Panneau d'affichage légal", textX, 110);
drawAutoText(chantier.name, textX, 140, maxTextW, 120, 120, mainFont, "bold", [15, 23, 42]);
drawAutoText(chantier.location, textX, 280, maxTextW, 60, 60, mainFont, "normal", [71, 85, 105]);
let boxY = 380;
if (chantier.maitreOuvrage || chantier.clientName) {
doc.setFillColor(248, 250, 252); doc.setDrawColor(226, 232, 240); doc.roundedRect(textX, boxY, 420, 70, 5, 5, 'FD');
doc.setTextColor(100, 116, 139); doc.setFontSize(30); doc.setFont(mainFont, "bold");
doc.text("Maître d'ouvrage", textX + 15, boxY + 22);
drawAutoText((chantier.maitreOuvrage || chantier.clientName), textX + 15, boxY + 48, 390, 30, 60, mainFont, "bold", [15, 23, 42]);
boxY += 90;
}
if (chantier.permitNumber) {
doc.setTextColor(148, 163, 184); doc.setFontSize(35); doc.setFont(mainFont, "bold");
doc.text("Permis N°", textX, boxY + 25);
drawAutoText(chantier.permitNumber.toString(), textX + 100, boxY + 25, 320, 40, 70, mainFont, "bold", [15, 23, 42]);
}
return doc.output('blob');
};
/**
* Aperçu visuel en CSS pur (Fluid) du panneau A1.
* Utilise des dimensions relatives (cqw) pour s'adapter à n'importe quel conteneur de façon réaliste.
*/
const PrintA1View = ({ chantier }) => {
if (!chantier) return Panneau introuvable...
;
const displayMoaLogoUrl = chantier.maitreOuvrageLogoId ? `?api=file/download&type=image&id=${chantier.maitreOuvrageLogoId}` : chantier.maitreOuvrageLogoUrl;
const textThemeColor = chantier.themeColor || '#059669';
return (
{/* Bandeau supérieur couleur */}
{/* Zone de contenu principale */}
{/* Colonne Gauche (QR Code) */}
Scannez pour les informations légales et utiles
{/* Colonne Droite (Textes) */}
Panneau d'affichage légal
{chantier.name}
{chantier.location}
{/* Bloc Maître d'ouvrage & Permis */}
{(chantier.maitreOuvrage || chantier.clientName) && (
Maître d'ouvrage
{displayMoaLogoUrl &&

}
{chantier.maitreOuvrage || chantier.clientName}
)}
{chantier.permitNumber && (
Permis N°
{chantier.permitNumber}
)}
{/* Bandeau inférieur (Sponsor) */}
{!chantier.hasNoAds && (
eco-panneau.fr
|
Professionnels du BTP : digitalisez vos obligations sur eco-panneau.fr
)}
);
};
// =========================================================================
// 5. COMPOSANTS DE L'INTERFACE UTILISATEUR
// =========================================================================
const SiteLogo = ({ className = "text-xl", qrSize = 24, onClick, theme = "light" }) => (
{ if(onClick) { e.preventDefault(); onClick(); } }} className={`flex items-center gap-2 font-black tracking-tight ${className}`} title="Retour à l'accueil">
eco
-panneau
.fr
);
// Modale floutée globale avec fermeture au clic externe
const Modal = ({ title, onClose, children, preventClose }) => (
e.stopPropagation()}>
{title}
{!preventClose && }
{children}
);
const DashboardLayout = ({ children, role, user, theme, navItems, activeTab, setActiveTab, onLogout, customLogout, sidebarFooter }) => {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const themeColors = { emerald: 'bg-emerald-600', purple: 'bg-slate-900' };
const colorClass = themeColors[theme] || 'bg-slate-800';
useEffect(() => {
const url = new URL(window.location);
if (url.searchParams.get('tab') !== activeTab) {
url.searchParams.set('tab', activeTab);
window.history.pushState({}, '', url);
}
}, [activeTab]);
useEffect(() => {
const handlePopState = () => {
const url = new URL(window.location);
const tab = url.searchParams.get('tab');
if (tab && tab !== activeTab) { setActiveTab(tab); }
};
window.addEventListener('popstate', handlePopState);
handlePopState();
return () => window.removeEventListener('popstate', handlePopState);
}, []);
return (
{ setActiveTab('dashboard'); setIsMobileMenuOpen(false); }} />
{isMobileMenuOpen && (
setIsMobileMenuOpen(false)}>
)}
{children}
);
};
const StatCard = ({ title, value, icon, color, bg, onClick }) => (
);
const PriceInput = ({ label, value, onChange }) => (
);
// =========================================================================
// 6. COMPOSANTS MÉTIER : MESSAGERIE ET FORMULAIRES
// =========================================================================
const ChatBox = ({ messages, currentUserRole, chantierId, refreshData, onSend }) => {
const endRef = useRef(null);
const [draft, setDraft] = useState('');
const [sending, setSending] = useState(false);
useEffect(() => {
endRef.current?.scrollIntoView({ behavior: 'smooth' });
const authorRole = currentUserRole === 'admin' ? 'Admin' : 'Client';
const checkAuthor = (a) => a === authorRole;
const unread = messages.filter(m => !m.resolved && !checkAuthor(m.author));
if (unread.length > 0) {
fetch(window.ECO_CONFIG.apiBaseUrl + 'interactions/read', { method: 'POST', body: JSON.stringify({ chantierId, author: authorRole }) }).then(() => { if(refreshData) refreshData(); });
if (window.Notification && Notification.permission === 'granted' && localStorage.getItem('eco_push_enabled') === 'true') {
const latestMsgId = unread[0].id || unread[0].created_at;
const lastNotified = sessionStorage.getItem('last_notified_msg_' + chantierId);
if (lastNotified !== latestMsgId) {
new Notification("eco-panneau.fr", {
body: "Nouveau message reçu.",
icon: "/favicon.svg"
});
sessionStorage.setItem('last_notified_msg_' + chantierId, latestMsgId);
}
}
}
}, [messages, chantierId, currentUserRole]);
const handleSend = async (e) => {
e.preventDefault(); if(!draft.trim()) return;
setSending(true);
if (onSend) await onSend(draft.trim());
setDraft(''); setSending(false);
};
return (
{[...messages].reverse().map((msg, idx) => {
const isMe = (currentUserRole === 'admin' && msg.author === 'Admin') || (currentUserRole === 'client' && msg.author === 'Client') || (currentUserRole === 'public' && msg.author === 'Riverain');
return (
{msg.author} • {new Date(msg.created_at).toLocaleString('fr-FR')}
);
})}
{messages.length === 0 &&
Aucun message.
}
);
};
const PublicContactForm = ({ chantierId, showToast, onSuccess, isPreview, hideAlert = false }) => {
const [submitting, setSubmitting] = useState(false);
const [isAlert, setIsAlert] = useState(false);
const [mountTime] = useState(Math.floor(Date.now() / 1000));
return (
);
};
// =========================================================================
// 7. ÉDITEUR DE PANNEAU ET GESTION DES ENTITÉS
// =========================================================================
const EntityCard = ({ data, type, index, lotIndex, onEdit, onDelete, onDragStart, onDragOver, onDrop }) => {
const isLot = type === 'lot';
const logoSrc = data.logoId ? `?api=file/download&type=image&id=${data.logoId}` : data.logoUrl;
return (
onDragStart(e, {type, index, lotIndex})} onDragOver={onDragOver} onDrop={(e) => onDrop(e, {type, index, lotIndex})} className={`bg-white border rounded-xl p-3 flex flex-col gap-2 shadow-sm relative group transition 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'}
}
{isLot && (
{data.entreprises?.map((ent, eIdx) => (
))}
)}
);
};
const EntityEditorModal = ({ editingEntity, setEditingEntity, saveEditedEntity }) => {
const isLot = editingEntity.location.type === 'lot';
const [uploadingLogo, setUploadingLogo] = 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) { alert(err.message); } finally { setUploadingLogo(false); }
};
const logoSrc = editingEntity.data.logoId ? `?api=file/download&type=image&id=${editingEntity.data.logoId}` : editingEntity.data.logoUrl;
return (
setEditingEntity(null)} preventClose={uploadingLogo}>
);
};
const ChantierEditorForm = ({ chantier, setChantier, managingChantier, onCancel, onSaveDraft, onPublish, onSaveActive, onPreview, onEditEntity, draggedItem, handleDragStart, handleDragOver, handleDrop, deleteEntity, isSaving, uiMode = 'simplifie', validationErrors = [], settings = {} }) => {
const [touched, setTouched] = useState({});
const [uploadingStates, setUploadingStates] = useState({ image: false, pdf: false, moaLogo: false });
const updateField = (field, val) => setChantier(prev => ({ ...prev, [field]: val }));
const handleBlur = (field) => setTouched(prev => ({ ...prev, [field]: true }));
const isError = (field) => validationErrors.length > 0 && (!chantier[field] || String(chantier[field]).trim() === '');
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) { alert(err.message); } 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) { alert(err.message); } 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) { alert(err.message); } finally { setUploadingStates(p => ({...p, moaLogo: false})); e.target.value = null; }
};
const isUploading = Object.values(uploadingStates).some(v => v);
const moaLogoSrc = chantier.maitreOuvrageLogoId ? `?api=file/download&type=image&id=${chantier.maitreOuvrageLogoId}` : chantier.maitreOuvrageLogoUrl;
const mainImgSrc = chantier.imageId ? `?api=file/download&type=image&id=${chantier.imageId}` : chantier.imageUrl;
const handleCancelClick = () => {
if (Object.keys(touched).length > 0 && !isSaving) {
if (confirm("Vous avez peut-être des modifications non sauvegardées. Êtes-vous sûr de vouloir annuler ?")) {
onCancel();
}
} else {
onCancel();
}
};
const showOpt = (key) => {
if (uiMode !== 'simplifie') return true;
return settings?.[`simp_opt_${key}`] === '1';
};
return (
0) ? 'bg-red-50 border-red-200' : 'bg-blue-50 border-blue-100'}`}>
0) ? 'text-red-900' : 'text-blue-900'}`}> Arrêté légal (obligatoire)
0) ? 'file:text-red-700 hover:file:bg-red-100' : 'file:text-blue-700 hover:file:bg-blue-100'}`} />
{uploadingStates.pdf &&
Upload en cours...
}
{chantier.pdfId && !uploadingStates.pdf && (
PDF actif
)}
{(showOpt('description') || showOpt('image') || showOpt('theme') || showOpt('link') || showOpt('emergency') || showOpt('schedule')) && uiMode !== 'delege' && (
Options avancées
{showOpt('description') &&
}
{showOpt('image') && (
{uploadingStates.image &&
Upload en cours...
}
{mainImgSrc && !uploadingStates.image && (
)}
)}
{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)} className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-emerald-500 outline-none transition" />
}
{showOpt('emergency') &&
}
{showOpt('schedule') &&
{updateField('noiseSchedule', e.target.value); handleBlur('noiseSchedule');}} onFocus={(e) => setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300)} className="w-full border-2 border-slate-200 rounded-xl p-3 text-sm focus:border-emerald-500 outline-none transition" placeholder="8h à 17h" />
}
)}
{uiMode !== 'simplifie' && (
L'équipe du chantier
Intervenants principaux
{chantier.intervenants?.map((inter, idx) => (
{deleteEntity(loc); handleBlur('intervenants');}} onDragStart={handleDragStart} onDragOver={handleDragOver} onDrop={(e, loc) => {handleDrop(e, loc); handleBlur('intervenants');}} />
))}
Sous-traitants par lots
{chantier.lots?.map((lot, lIdx) => (
{deleteEntity(loc); handleBlur('lots');}} onDragStart={handleDragStart} onDragOver={handleDragOver} onDrop={(e, loc) => {handleDrop(e, loc); handleBlur('lots');}} />
))}
)}
{onPreview && (
)}
{onSaveDraft && (
)}
{(onPublish || onSaveActive) && (
)}
);
};
const PreviewModal = ({ chantier, onClose, initialType = 'virtual', showToast, interactions = [], refreshData }) => {
const [viewType, setViewType] = useState(initialType);
const [generatingA1, setGeneratingA1] = useState(false);
return (
e.stopPropagation()}>
{viewType === 'virtual' ? (
) : (
)}
);
};
// =========================================================================
// 8. REGISTRE GLOBAL
// =========================================================================
Object.assign(window, Icons);
Object.assign(window, {
DashboardLayout, StatCard, PriceInput, Modal,
ChatBox, PublicContactForm, EntityCard, EntityEditorModal,
ChantierEditorForm, PreviewModal
});
/* EOF ===== [_soclecommun.jsx] =============== */