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

{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 }) => (
{icon}

{title}

{value}

); const PriceInput = ({ label, value, onChange }) => (
onChange(Number(e.target.value))} className="w-full border border-slate-200 rounded-lg p-2 text-sm pr-8 focus:border-slate-400 outline-none" />
); // ========================================================================= // 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.detail}

{msg.author} • {new Date(msg.created_at).toLocaleString('fr-FR')}
); })} {messages.length === 0 &&
Aucun message.
}
setDraft(e.target.value)} placeholder="Écrire une réponse..." disabled={sending} className="flex-1 border-2 border-slate-100 rounded-xl px-4 py-2 outline-none focus:border-emerald-500 text-sm transition" />
); }; 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 (
{ e.preventDefault(); if (isPreview) { if (showToast) showToast("Ceci est un aperçu interactif. La messagerie n'est pas fonctionnelle ici.", "info"); return; } setSubmitting(true); const data = { chantierId, detail: e.target.detail.value, author: e.target.email.value, isAlert: isAlert ? 1 : 0, hp_text: e.target.hp_text.value, hp_time: e.target.hp_time.value }; try { await fetch(window.ECO_CONFIG.apiBaseUrl + 'interactions', { method: 'POST', body: JSON.stringify(data) }); showToast("Message envoyé. Le responsable vous répondra par e-mail.", "success"); e.target.reset(); setIsAlert(false); if(onSuccess) onSuccess(); } catch(err) { showToast("Erreur d'envoi", "error"); } setSubmitting(false); }} className="space-y-3"> {!hideAlert && ( )}
); }; // ========================================================================= // 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 && logo}

{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}>
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 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 (

Identité du panneau

updateField('name', e.target.value)} onBlur={() => handleBlur('name')} onFocus={(e) => setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300)} disabled={uiMode === 'delege' || (managingChantier?.status === 'Actif' && managingChantier?.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={uiMode === 'delege' || (managingChantier?.status === 'Actif' && managingChantier?.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={uiMode === 'delege'} 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" /> {uiMode !== 'simplifie' && ( )}
{moaLogoSrc && (
Logo MOA
)}
{updateField('permitNumber', e.target.value); handleBlur('permitNumber');}} onFocus={(e) => setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300)} disabled={uiMode === 'delege'} 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-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') &&