/** * ========================================================================= * PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0 * Socle UI - Composants partagés, Icônes et Utilitaires * ========================================================================= */ const { useState, useEffect, useRef } = React; // ========================================================================= // 1. SYSTÈME DE NOTIFICATIONS GLOBAL (TOASTS) // ========================================================================= window.showToast = (message, type = 'success', duration = 4000) => { let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.className = 'fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none'; document.body.appendChild(container); } const toast = document.createElement('div'); const bgColor = type === 'success' ? 'bg-emerald-50 border-emerald-200 text-emerald-800' : (type === 'error' ? 'bg-red-50 border-red-200 text-red-800' : 'bg-blue-50 border-blue-200 text-blue-800'); toast.className = `flex items-center gap-3 px-4 py-3 rounded-xl border shadow-lg ${bgColor} transform transition-all duration-300 translate-x-full opacity-0 pointer-events-auto`; toast.innerHTML = `
${message}
`; container.appendChild(toast); // Animation d'entrée requestAnimationFrame(() => { toast.classList.remove('translate-x-full', 'opacity-0'); toast.classList.add('translate-x-0', 'opacity-100'); }); // Suppression automatique setTimeout(() => { toast.classList.remove('translate-x-0', 'opacity-100'); toast.classList.add('translate-x-full', 'opacity-0'); setTimeout(() => toast.remove(), 300); }, duration); }; // ========================================================================= // 2. UTILITAIRES ET UPLOADS (COMPRESSION IMAGE & APERÇU PDF CLIENT) // ========================================================================= window.uploadFile = async (file, type, onProgress) => { let fileToSend = file; let previewBlob = null; let hasLocalProcessing = false; try { // A. TRAITEMENT CLIENT DES IMAGES (Compression WEBP + Redimensionnement) if (type === 'image' && file.type.startsWith('image/')) { if (onProgress) onProgress("Optimisation de l'image en cours...", 10); const img = new Image(); const imgUrl = URL.createObjectURL(file); await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; img.src = imgUrl; }); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); let width = img.width; let height = img.height; const MAX_SIZE = 1920; if (width > MAX_SIZE || height > MAX_SIZE) { if (width > height) { height = Math.round((height * MAX_SIZE) / width); width = MAX_SIZE; } else { width = Math.round((width * MAX_SIZE) / height); height = MAX_SIZE; } } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); URL.revokeObjectURL(imgUrl); // Conversion en WebP (qualité 85%) const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 0.85)); fileToSend = new File([blob], file.name.replace(/\.[^/.]+$/, "") + ".webp", { type: 'image/webp' }); hasLocalProcessing = true; if (onProgress) onProgress("Image optimisée, préparation de l'envoi...", 30); } // B. TRAITEMENT CLIENT DES PDF (Génération de la miniature) else if (type === 'pdf' && file.type === 'application/pdf' && window.pdfjsLib) { if (onProgress) onProgress("Génération de l'aperçu PDF en cours...", 10); const arrayBuffer = await file.arrayBuffer(); const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer }).promise; const page = await pdf.getPage(1); // Échelle 1.5 offre un bon équilibre entre lisibilité de l'aperçu et poids final const viewport = page.getViewport({ scale: 1.5 }); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport: viewport }).promise; // Conversion de l'aperçu en WebP (qualité 80%) previewBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 0.8)); hasLocalProcessing = true; if (onProgress) onProgress("Aperçu généré, préparation de l'envoi...", 30); } } catch (e) { console.error("Erreur lors du traitement local du fichier :", e); window.showToast("L'optimisation locale a échoué. Le fichier sera envoyé tel quel.", "error"); hasLocalProcessing = false; } return new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', fileToSend); formData.append('type', type); if (previewBlob) { formData.append('preview', previewBlob, 'preview.webp'); } const xhr = new XMLHttpRequest(); xhr.open('POST', window.ECO_CONFIG.apiBaseUrl + 'file/upload', true); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const base = hasLocalProcessing ? 30 : 0; const multiplier = hasLocalProcessing ? 70 : 100; const percent = Math.round(base + ((e.loaded / e.total) * multiplier)); if (onProgress) { onProgress(`Envoi au serveur : ${Math.round((e.loaded / e.total) * 100)}%`, percent); } } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { const res = JSON.parse(xhr.responseText); if (res.status === 'success') { if (onProgress) onProgress("Traitement terminé !", 100); resolve(res.data.id); } else { reject(res.message || 'Erreur upload serveur'); } } catch (err) { reject('Réponse invalide du serveur'); } } else { reject('Erreur HTTP ' + xhr.status); } }; xhr.onerror = () => reject("Erreur réseau (déconnexion ou blocage)"); xhr.send(formData); }); }; // ========================================================================= // 3. BIBLIOTHÈQUE D'ICÔNES VECTORIELLES (SVG) - EXHAUSTIVE POUR ÉVITER LES CRASHS // ========================================================================= const createIcon = (svgPaths) => ({ size = 24, className = '', ...props }) => ( {svgPaths} ); window.Home = createIcon(<>); window.Users = createIcon(<>); window.User = createIcon(<>); window.Building = createIcon(<>); window.Settings = createIcon(<>); window.MessageSquare = createIcon(<>); window.Package = createIcon(<>); window.FileText = createIcon(<>); window.Activity = createIcon(<>); window.Eye = createIcon(<>); window.Terminal = createIcon(<>); window.Archive = createIcon(<>); window.Save = createIcon(<>); window.Download = createIcon(<>); window.RefreshCw = createIcon(<>); window.Trash = createIcon(<>); window.Trash2 = createIcon(<>); window.Loader = createIcon(<>); window.ShieldAlert = createIcon(<>); window.Shield = createIcon(<>); window.CreditCard = createIcon(<>); window.AlertTriangle = createIcon(<>); window.Zap = createIcon(<>); window.KeyRound = createIcon(<>); window.CheckCircle = createIcon(<>); window.Check = createIcon(<>); window.Mail = createIcon(<>); window.AlertOctagon = createIcon(<>); window.Power = createIcon(<>); window.MapPin = createIcon(<>); window.Edit = createIcon(<>); window.Plus = createIcon(<>); window.ArrowLeft = createIcon(<>); window.X = createIcon(<>); window.UploadCloud = createIcon(<>); window.FileIcon = createIcon(<>); window.ImageIcon = createIcon(<>); window.Camera = createIcon(<>); window.Menu = createIcon(<>); window.LogOut = createIcon(<>); window.Calendar = createIcon(<>); window.Clock = createIcon(<>); window.Phone = createIcon(<>); window.Link = createIcon(<>); window.AlignLeft = createIcon(<>); window.Palette = createIcon(<>); window.Info = createIcon(<>); window.ChevronRight = createIcon(<>); window.ChevronDown = createIcon(<>); window.Search = createIcon(<>); window.Bell = createIcon(<>); window.Lock = createIcon(<>); window.Send = createIcon(<>); // ========================================================================= // 4. COMPOSANTS DE PRÉSENTATION // ========================================================================= window.StatCard = ({ title, value, icon, color, bg, onClick }) => (
{icon}

{title}

{value}

); window.PriceInput = ({ label, value, onChange }) => (
onChange(parseInt(e.target.value) || 0)} className="w-full border-2 border-slate-200 rounded-xl p-2.5 pr-8 text-sm focus:border-emerald-500 outline-none transition font-bold text-slate-800" />
); // ========================================================================= // 5. COMPOSANTS MODALES // ========================================================================= window.Modal = ({ title, onClose, children, preventClose = false, zIndex = "z-[100]" }) => { useEffect(() => { const handleEsc = (e) => { if (e.key === 'Escape' && !preventClose) onClose(); }; window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); }, [onClose, preventClose]); return (
{ if (e.target === e.currentTarget && !preventClose) onClose(); }}>
e.stopPropagation()}>

{title}

{!preventClose && ( )}
{children}
); }; window.ConfirmModal = ({ title, message, confirmText = "Confirmer", cancelText = "Annuler", isDestructive = false, onConfirm, onCancel, zIndex = "z-[200]" }) => { const [isProcessing, setIsProcessing] = useState(false); return (

{message}

); }; window.PromptModal = ({ title, message, placeholder = "", confirmText = "Valider", cancelText = "Annuler", onConfirm, onCancel, zIndex = "z-[200]" }) => { const [val, setVal] = useState(''); const [isProcessing, setIsProcessing] = useState(false); return (
{ e.preventDefault(); setIsProcessing(true); await onConfirm(val); }} className="space-y-6">

{message}

setVal(e.target.value)} placeholder={placeholder} className="w-full border-2 border-slate-200 rounded-xl p-3 focus:border-emerald-500 outline-none transition font-bold" autoFocus required />
); }; // ========================================================================= // 6. LAYOUT PRINCIPAL (SHELL) // ========================================================================= window.DashboardLayout = ({ role, user, theme = 'emerald', navItems, activeTab, setActiveTab, onLogout, children }) => { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const themeConfig = { emerald: { bg: 'bg-emerald-900', hover: 'hover:bg-emerald-800', active: 'bg-emerald-800 text-white border-emerald-400', text: 'text-emerald-100', border: 'border-emerald-800' }, purple: { bg: 'bg-slate-900', hover: 'hover:bg-slate-800', active: 'bg-purple-600 text-white border-purple-400', text: 'text-slate-300', border: 'border-slate-800' }, blue: { bg: 'bg-blue-900', hover: 'hover:bg-blue-800', active: 'bg-blue-800 text-white border-blue-400', text: 'text-blue-100', border: 'border-blue-800' } }[theme]; return (
{/* Sidebar Desktop */} {/* Header & Menu Mobile */}
Logo

{role}

{user}

{/* Overlay Menu Mobile */} {isMobileMenuOpen && (
)} {/* Contenu Principal */}
{children}
); }; /* EOF ========== [_www/_react/_socle_ui.jsx] */