// ECO-PANNEAU.FR - _react/ui/_ui_utils.jsx // 1. - SYSTÈME DE NOTIFICATIONS GLOBAL (TOASTS) ET LOGS DE FALLBACK window.pano_showToast = (message, type = 'info', duration = 4000) => { let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.className = 'fixed right-4 sm:right-6 flex flex-col items-end gap-3 pointer-events-none transition-all duration-300 ease-out'; container.style.zIndex = window.pano_getNextModalZIndex ? (window.pano_getNextModalZIndex() + 100) : 999999; document.body.appendChild(container); } else { container.style.zIndex = window.pano_getNextModalZIndex ? (window.pano_getNextModalZIndex() + 100) : 999999; } let maxOffset = window.innerWidth >= 640 ? 24 : 16; const bottomBars = document.querySelectorAll('.bottom-0'); bottomBars.forEach(bar => { const rect = bar.getBoundingClientRect(); if (rect.bottom >= window.innerHeight - 5 && rect.height > 0) { const currentOffset = rect.height + 16; if (currentOffset > maxOffset) { maxOffset = currentOffset; } } }); container.style.bottom = `${maxOffset}px`; const toast = document.createElement('div'); const bgColor = type === 'success' ? 'bg-emerald-600/95 border-emerald-500 text-white' : (type === 'error' ? 'bg-red-500/95 border-red-400 text-white' : (type === 'warning' ? 'bg-amber-500/95 border-amber-400 text-white' : 'bg-blue-600/95 border-blue-500 text-white')); const svgWrap = (path) => `${path}`; const iconSvg = type === 'success' ? svgWrap(window.pano_svgPaths?.CheckCircle || '') : (type === 'error' || type === 'warning' ? svgWrap(window.pano_svgPaths?.AlertTriangle || '') : svgWrap(window.pano_svgPaths?.Info || '')); toast.className = `flex items-center gap-3 px-5 py-4 rounded-2xl border shadow-2xl ${bgColor} pointer-events-auto backdrop-blur-md max-w-xs transition-all duration-500 ease-out`; const iconDiv = document.createElement('div'); iconDiv.className = 'shrink-0'; iconDiv.innerHTML = iconSvg; const msgDiv = document.createElement('div'); msgDiv.className = 'font-bold text-sm leading-tight break-words'; msgDiv.textContent = message; toast.appendChild(iconDiv); toast.appendChild(msgDiv); toast.style.transform = 'translateY(-100vh)'; toast.style.opacity = '0'; container.appendChild(toast); void toast.offsetWidth; toast.style.transform = 'translateY(0)'; toast.style.opacity = '1'; setTimeout(() => { toast.style.transform = 'translateX(120%)'; setTimeout(() => { toast.remove(); if (container && container.childNodes.length === 0) container.remove(); }, 500); }, duration); }; window.pano_loggedErrors = window.pano_loggedErrors || new Set(); window.pano_logFallback = (details) => { if (window.pano_loggedErrors.has(details)) return; window.pano_loggedErrors.add(details); setTimeout(() => window.pano_loggedErrors.delete(details), 60000); try { const baseUrl = window.pano_CONFIG ? window.pano_CONFIG.apiBaseUrl : '?api='; fetch(baseUrl + 'system/log_fallback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ details }) }).catch(() => {}); } catch(e) {} }; // 2. - UTILITAIRES ET LOGIQUE MÉTIER // CORRECTION SÉCURITÉ : Générateur d'ID universel infaillible (Polyfill) window.pano_generateID = () => { if (typeof crypto !== 'undefined' && crypto.randomUUID) { try { return crypto.randomUUID(); } catch(e) {} } // Fallback mathématique si HTTPS ou navigateur restrictif bloque l'API Crypto return Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10); }; window.pano_formatPlural = (count, singular, plural) => { return count > 1 ? `${count} ${plural}` : `${count} ${singular}`; }; window.pano_normalizeString = (str) => { if (str === null || str === undefined) return ''; return String(str) .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase() .trim(); }; window.pano_apiFetch = async (endpoint, options = {}) => { const { method = 'POST', body, setLoading, successMessage, errorMessage = "Erreur réseau ou serveur", silent = false } = options; if (setLoading) setLoading(true); try { const fetchOptions = { method, headers: {} }; if (silent) fetchOptions.headers['x-silent'] = '1'; // CORRECTION SÉCURITÉ : Interdiction stricte de forcer un body sur GET/HEAD if ((body || window.pano_CURRENT_DELEGATE_TOKEN) && method !== 'GET' && method !== 'HEAD') { let cleanBody = {}; if (body) { cleanBody = JSON.parse(JSON.stringify(body), (key, value) => { if (['imageUrl', 'maitreOuvrageLogoUrl', 'logoUrl', 'architecteLogoUrl'].includes(key)) return undefined; if (typeof value === 'string' && value.includes('data:image/')) return value.replace(/(src|href)=["']data:image\/[^;]+;base64,[^"']+["']/gi, '$1="#"'); return value; }); } if (window.pano_CURRENT_DELEGATE_TOKEN && method === 'POST') cleanBody.delegate_token = window.pano_CURRENT_DELEGATE_TOKEN; fetchOptions.body = JSON.stringify(cleanBody); // CORRECTION RÉSEAU 1 : Injection du header MIME approprié fetchOptions.headers['Content-Type'] = 'application/json'; } const baseUrl = window.pano_CONFIG ? window.pano_CONFIG.apiBaseUrl : '?api='; const fullEndpoint = baseUrl + endpoint; const targetUrl = silent ? (fullEndpoint + (fullEndpoint.includes('?') ? '&' : '?') + 'silent=1') : fullEndpoint; const res = await fetch(targetUrl, fetchOptions); const d = await res.json(); if (d.status === 'success') { if (successMessage && window.pano_showToast && !silent) window.pano_showToast(successMessage, "success"); return d; } else { const isGlobalBlock = res.status === 423 || (res.status === 429 && d.data && d.data.is_blocked); if (!isGlobalBlock && window.pano_showToast && !silent) { window.pano_showToast(d.message || errorMessage, "error"); } return null; } } catch (e) { if (window.pano_showToast && !silent) window.pano_showToast(errorMessage, "error"); return null; } finally { if (setLoading) setLoading(false); } }; // UTILITAIRE GLOBAL ZÉRO-DETTE : Wrapper pour protéger les requêtes asynchrones window.pano_useSafeFetch = () => { const isMounted = React.useRef(true); React.useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); const safeFetch = async (endpoint, options = {}) => { const { setLoading, ...restOptions } = options; // On enveloppe silencieusement le setLoading const wrappedSetLoading = setLoading ? (val) => { if (isMounted.current) setLoading(val); } : undefined; const res = await window.pano_apiFetch(endpoint, { ...restOptions, setLoading: wrappedSetLoading }); return res; }; return { isMounted, safeFetch }; }; window.pano_formatDate = (dateStr, fallback = '-') => { if (!dateStr) return fallback; const d = new Date(String(dateStr).replace(' ', 'T')); return isNaN(d.getTime()) ? fallback : d.toLocaleDateString('fr-FR'); }; window.pano_getPanelAccessRights = (allPanels = [], clientUid, clientEmailHash) => { const isMe = (uid) => uid === clientUid || uid === clientEmailHash; const pendingInvites = allPanels.filter(p => p.client_uid !== clientUid && p.collaborators?.some(c => isMe(c.uid) && c.status === 'pending')); const ownedPanels = allPanels.filter(p => p.client_uid === clientUid); const acceptedCollabPanels = allPanels.filter(p => p.client_uid !== clientUid && p.collaborators?.some(c => isMe(c.uid) && c.status === 'accepted')); const visiblePanels = [...ownedPanels, ...acceptedCollabPanels]; return { pendingInvites, ownedPanels, acceptedCollabPanels, visiblePanels }; }; window.pano_computeClientUnread = (interactions = [], myClientData = {}, visiblePanels = [], pendingInvites = []) => { let unread = 0; const expectedFullName = myClientData.full_name || 'Utilisateur'; const expectedCompany = myClientData.name || 'Société'; const expectedAuthorName = `${expectedFullName} (${expectedCompany})`; const isMe = (uid) => uid === myClientData.id || uid === myClientData.email_hash; interactions.forEach(m => { if (m.resolved) return; const isMyOwnMessage = (m.authorType === 'Client' && ( m.author === 'Client' || m.author === myClientData.email || m.author === myClientData.full_name || m.author === myClientData.name || m.author === expectedAuthorName )); if (m.panneauId.startsWith('SUPPORT_')) { if (m.authorType !== 'Client') unread++; } else if (m.panneauId.startsWith('GROUP_')) { const pId = m.panneauId.replace('GROUP_', ''); const hasAccess = visiblePanels.some(p => p.id === pId) || pendingInvites.some(p => p.id === pId); if (hasAccess && !isMyOwnMessage && m.authorType !== 'Systeme' && m.authorType !== 'System') unread++; } else { if (m.authorType !== 'Client' && m.authorType !== 'Admin' && m.authorType !== 'Systeme' && m.authorType !== 'System') { const panneauObj = visiblePanels.find(p => p.id === m.panneauId); if (panneauObj) { const isOwner = panneauObj.client_uid === myClientData.id; const myCollab = !isOwner ? panneauObj.collaborators?.find(c => isMe(c.uid)) : null; const hasRiverainAccess = isOwner || (myCollab && myCollab.rights?.chat_access !== 'none'); if (hasRiverainAccess) unread++; } } } }); return unread; }; window.pano_uploadFile = async (file, type, onProgress, isPublic = false, isPrivate = false, clientUid = null, panneauId = null) => { let fileToSend = file; let previewBlobs = {}; let realTotalPages = 1; let hasLocalProcessing = false; try { if (type === 'image' && file.type.startsWith('image/')) { if (onProgress) onProgress("Optimisation de l'image", 10); const img = new Image(); const imgUrl = URL.createObjectURL(file); await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = () => reject(new Error("Image illisible.")); img.src = imgUrl; }); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); let width = img.width, height = img.height; const MAX_SIZE = 1920; const targetScale = MAX_SIZE / Math.max(width, height); if (targetScale < 1) { width = Math.round(width * targetScale); height = Math.round(height * targetScale); } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); URL.revokeObjectURL(imgUrl); const blob = await new Promise((resolve, reject) => { canvas.toBlob(b => { if (b) resolve(b); else canvas.toBlob(b2 => b2 ? resolve(b2) : reject(new Error("Echec toBlob")), 'image/jpeg', 0.85); }, 'image/webp', 0.85); }); const ext = blob.type === 'image/jpeg' ? '.jpg' : '.webp'; const safeName = file.name || 'image_collee'; fileToSend = new File([blob], safeName.replace(/\.[^/.]+$/, "") + ext, { type: blob.type }); if (onProgress) onProgress("Génération de la miniature", 20); const thumbScale = 400 / Math.max(img.width, img.height); if (thumbScale < 1) { const canvasThumb = document.createElement('canvas'); canvasThumb.width = Math.round(img.width * thumbScale); canvasThumb.height = Math.round(img.height * thumbScale); const ctxThumb = canvasThumb.getContext('2d'); ctxThumb.drawImage(img, 0, 0, canvasThumb.width, canvasThumb.height); let thumbDataUrl = canvasThumb.toDataURL('image/webp', 0.25); if (!thumbDataUrl || thumbDataUrl === 'data:,') thumbDataUrl = canvasThumb.toDataURL('image/jpeg', 0.25); previewBlobs['preview_thumb'] = thumbDataUrl; canvasThumb.width = 0; canvasThumb.height = 0; } else { let thumbDataUrl = canvas.toDataURL('image/webp', 0.25); if (!thumbDataUrl || thumbDataUrl === 'data:,') thumbDataUrl = canvas.toDataURL('image/jpeg', 0.25); previewBlobs['preview_thumb'] = thumbDataUrl; } canvas.width = 0; canvas.height = 0; hasLocalProcessing = true; if (onProgress) onProgress("Préparation de l'envoi", 30); } else if (type === 'pdf' && file.type === 'application/pdf') { if (onProgress) onProgress("Analyse du PDF", 5); let limitPdfPages = 20; try { const res = await fetch((window.pano_CONFIG ? window.pano_CONFIG.apiBaseUrl : '?api=') + 'sync&limit=1&silent=1'); const d = await res.json(); if (d?.data?.settings?.limit_pdf_pages) limitPdfPages = parseInt(d.data.settings.limit_pdf_pages, 10); } catch(e) {} if (!window.pano_pdfjsLib) { await new Promise((resolve, reject) => { const assetsUrl = (window.pano_CONFIG && window.pano_CONFIG.assetsUrl) ? window.pano_CONFIG.assetsUrl : '_assets/'; const script = document.createElement('script'); script.src = assetsUrl + 'pdfjs/pdf.min.js'; script.onload = () => { window.pano_pdfjsLib.GlobalWorkerOptions.workerSrc = assetsUrl + 'pdfjs/pdf.worker.min.js'; resolve(); }; script.onerror = () => { const cdnScript = document.createElement('script'); cdnScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'; cdnScript.crossOrigin = 'anonymous'; cdnScript.onload = () => { window.pano_pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; resolve(); }; cdnScript.onerror = () => reject(new Error("Impossible de charger PDF.js.")); document.head.appendChild(cdnScript); }; document.head.appendChild(script); }); } if (window.pano_pdfjsLib) { if (onProgress) onProgress("Extraction des pages", 10); const arrayBuffer = await file.arrayBuffer(); const loadingTask = window.pano_pdfjsLib.getDocument({ data: arrayBuffer }); const pdf = await loadingTask.promise; realTotalPages = pdf.numPages; const pagesToExtract = Math.min(realTotalPages, limitPdfPages); for (let i = 1; i <= pagesToExtract; i++) { if (onProgress) onProgress(`Rendu page ${i}/${pagesToExtract}`, 10 + Math.round((i/pagesToExtract)*20)); const page = await pdf.getPage(i); if (i === 1) { const vpBaseThumb = page.getViewport({ scale: 1.0 }); const viewportThumb = page.getViewport({ scale: 400 / Math.max(vpBaseThumb.width, vpBaseThumb.height) }); const canvasThumb = document.createElement('canvas'); canvasThumb.width = viewportThumb.width; canvasThumb.height = viewportThumb.height; await page.render({ canvasContext: canvasThumb.getContext('2d'), viewport: viewportThumb }).promise; let thumbDataUrl = canvasThumb.toDataURL('image/webp', 0.25); if (!thumbDataUrl || thumbDataUrl === 'data:,') thumbDataUrl = canvasThumb.toDataURL('image/jpeg', 0.25); previewBlobs['preview_thumb'] = thumbDataUrl; canvasThumb.width = 0; canvasThumb.height = 0; } const vpBase = page.getViewport({ scale: 1.0 }); const scaleW = 896 / vpBase.width; const scaleH = 1280 / vpBase.height; const targetScale = Math.min(scaleW, scaleH); const viewport = page.getViewport({ scale: targetScale }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise; let dataUrl = canvas.toDataURL('image/webp', 0.50); if (!dataUrl || dataUrl === 'data:,') dataUrl = canvas.toDataURL('image/jpeg', 0.50); previewBlobs[`preview_b64_${i}`] = dataUrl; canvas.width = 0; canvas.height = 0; page.cleanup(); } pdf.destroy(); hasLocalProcessing = true; } } } catch (e) { if (window.pano_showToast) window.pano_showToast("Aperçu local indisponible. Envoi du fichier brut en cours.", "info"); hasLocalProcessing = false; fileToSend = file; previewBlobs = {}; realTotalPages = 1; } return new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', fileToSend); formData.append('type', type); if (window.pano_CURRENT_DELEGATE_TOKEN) formData.append('delegate_token', window.pano_CURRENT_DELEGATE_TOKEN); if (isPublic) formData.append('isPublic', 'true'); if (isPrivate) formData.append('is_private', 'true'); if (clientUid) formData.append('client_uid', clientUid); if (panneauId) formData.append('panneau_id', panneauId); if (Object.keys(previewBlobs).length > 0) { for (const [key, b64] of Object.entries(previewBlobs)) { formData.append(key, b64); } formData.append('numPages', realTotalPages); } const xhr = new XMLHttpRequest(); const baseUrl = window.pano_CONFIG ? window.pano_CONFIG.apiBaseUrl : '?api='; xhr.open('POST', baseUrl + 'file/upload', true); xhr.upload.onprogress = (e) => { if (e.lengthComputable && onProgress) { const base = hasLocalProcessing ? 30 : 0; const multiplier = hasLocalProcessing ? 70 : 100; onProgress(`Transfert`, Math.round(base + ((e.loaded / e.total) * multiplier))); } }; xhr.onload = () => { window.dispatchEvent(new CustomEvent('api_call_end')); if (xhr.status === 401) { window.dispatchEvent(new CustomEvent('session_expired')); } if (xhr.status >= 200 && xhr.status < 300) { try { const res = JSON.parse(xhr.responseText); if (res.status === 'success') { if (onProgress) onProgress("Terminé", 100); const ret = new String(res.data.id); ret.id = res.data.id; ret.numPages = res.data.numPages || 1; resolve(ret); } else reject(new Error(res.message || 'Erreur upload serveur')); } catch (err) { reject(new Error('Réponse invalide du serveur')); } } else reject(new Error('Erreur HTTP ' + xhr.status)); }; xhr.onerror = () => { window.dispatchEvent(new CustomEvent('api_call_end')); reject(new Error("Erreur réseau (déconnexion ou blocage)")); }; window.dispatchEvent(new CustomEvent('api_call_start')); xhr.send(formData); }); }; // 3. - LE ROUTEUR (STACK NAVIGATOR) SINGLETON (ZÉRO FUITE MÉMOIRE) if (typeof window !== 'undefined' && !window._panoHistoryPatched) { window._panoHistoryPatched = true; window.pano_current_depth = 0; if (!window.history.state || typeof window.history.state.depth !== 'number') { const initialState = window.history.state || {}; initialState.depth = window.pano_current_depth; window.history.replaceState(initialState, '', window.location.href); } else { window.pano_current_depth = window.history.state.depth; } const originalPushState = window.history.pushState; window.history.pushState = function(state, title, url) { window.pano_current_depth += 1; const newState = state || {}; newState.depth = window.pano_current_depth; return originalPushState.apply(this, [newState, title, url]); }; const originalReplaceState = window.history.replaceState; window.history.replaceState = function(state, title, url) { const newState = state || {}; newState.depth = window.pano_current_depth; return originalReplaceState.apply(this, [newState, title, url]); }; } if (typeof window !== 'undefined' && !window._panoRouterSingleton) { window._panoRouterSingleton = true; window._panoRouterSubscribers = new Set(); window._panoRouterMaxLevel = 0; const handlePopState = (e) => { const state = e.state; if (!state || !state.panoStack) return; const incomingLevel = state.level || 0; if (incomingLevel > window._panoRouterMaxLevel) { window.history.back(); } else { window._panoRouterMaxLevel = incomingLevel; if (window.pano_internal_pushes > 0) window.pano_internal_pushes--; window._panoRouterSubscribers.forEach(fn => fn({ ...state })); } }; const handleSync = () => { const state = window.history.state; if (state && state.panoStack) { window._panoRouterMaxLevel = state.level; window._panoRouterSubscribers.forEach(fn => fn({ ...state })); } }; window.addEventListener('popstate', handlePopState); window.addEventListener('pano_stack_sync', handleSync); } window.pano_useUrlModal = () => { const { useState, useEffect, useCallback, useRef } = React; const getParams = () => { const p = new URLSearchParams(window.location.search); return { modal: p.get('modal'), dialog: p.get('dialog'), chat_id: p.get('chat_id'), targetId: p.get('targetId'), dialogId: p.get('dialogId'), alert: p.get('alert'), tab: p.get('tab') || 'dashboard' }; }; const [appState, setAppState] = useState(() => { if (typeof window !== 'undefined' && window.history.state && window.history.state.panoStack) { window._panoRouterMaxLevel = window.history.state.level; return window.history.state; } const p = getParams(); const stack = []; if (p.modal) stack.push({ type: 'modal', name: p.modal, targetId: p.targetId }); if (p.dialog) stack.push({ type: 'dialog', name: p.dialog, targetId: p.dialogId }); if (p.chat_id) stack.push({ type: 'chat', name: 'chat', targetId: p.chat_id }); if (p.alert) stack.push({ type: 'alert', name: p.alert, targetId: null }); const st = { panoStack: stack, level: stack.length, currentTab: p.tab, depth: window.pano_current_depth || 0 }; window._panoRouterMaxLevel = stack.length; return st; }); const isInit = useRef(false); useEffect(() => { window._panoRouterSubscribers.add(setAppState); if (!isInit.current && typeof window !== 'undefined') { isInit.current = true; window.pano_internal_pushes = window.pano_internal_pushes || 0; const state = window.history.state || {}; if (!state.panoStack) { const cleanUrl = new URL(window.location.href); ['modal', 'targetId', 'dialog', 'dialogId', 'chat_id', 'alert'].forEach(k => cleanUrl.searchParams.delete(k)); const initSt = { ...state, panoStack: appState.panoStack, level: appState.level, currentTab: appState.currentTab, depth: window.pano_current_depth || 0 }; window.history.replaceState(initSt, '', cleanUrl); window._panoRouterMaxLevel = appState.level; setAppState(initSt); } } return () => { window._panoRouterSubscribers.delete(setAppState); }; }, []); const broadcastSync = () => { window.dispatchEvent(new Event('pano_stack_sync')); }; const buildUrl = (tab, type, name, targetId) => { let qs = `tab=${tab}`; if (type === 'modal') { qs += `&modal=${name}`; if (targetId) qs += `&targetId=${targetId}`; } else if (type === 'dialog') { qs += `&dialog=${name}`; if (targetId) qs += `&dialogId=${targetId}`; } else if (type === 'chat') { qs += `&chat_id=${targetId}`; } else if (type === 'alert') { qs += `&alert=${name}`; } return `?${qs}`; }; const pushLayer = useCallback((type, name, targetId = null, silent = true) => { const prev = window.history.state || { panoStack: [], level: 0, currentTab: 'dashboard' }; const currentStack = prev.panoStack || []; const newStack = currentStack.filter(l => l.type !== type); newStack.push({ type, name, targetId }); const newLevel = newStack.length; const currentTab = prev.currentTab || 'dashboard'; const newState = { ...prev, panoStack: newStack, level: newLevel, currentTab }; const url = silent ? window.location.href : buildUrl(currentTab, type, name, targetId); window.history.pushState(newState, '', url); window.pano_internal_pushes = (window.pano_internal_pushes || 0) + 1; window._panoRouterMaxLevel = newLevel; setAppState(newState); broadcastSync(); }, []); const replaceCurrentLayer = useCallback((type, name, targetId = null, silent = true) => { const prev = window.history.state || { panoStack: [], level: 0, currentTab: 'dashboard' }; const currentStack = prev.panoStack || []; if (currentStack.length === 0) { const newStack = [{ type, name, targetId }]; const currentTab = prev.currentTab || 'dashboard'; const newState = { ...prev, panoStack: newStack, level: 1, currentTab }; const url = silent ? window.location.href : buildUrl(currentTab, type, name, targetId); window.history.pushState(newState, '', url); window.pano_internal_pushes = (window.pano_internal_pushes || 0) + 1; window._panoRouterMaxLevel = 1; setAppState(newState); broadcastSync(); return; } const newStack = [...currentStack.slice(0, -1), { type, name, targetId }]; const currentTab = prev.currentTab || 'dashboard'; const newState = { ...prev, panoStack: newStack, level: prev.level || newStack.length, currentTab }; const url = silent ? window.location.href : buildUrl(currentTab, type, name, targetId); window.history.replaceState(newState, '', url); setAppState(newState); broadcastSync(); }, []); const closeCurrentLayer = useCallback(() => { const prev = window.history.state || { panoStack: [], level: 0, currentTab: 'dashboard' }; const currentStack = prev.panoStack || []; if (currentStack.length === 0) return; if (window.history.length > 1 && window.pano_internal_pushes > 0) { window.pano_internal_pushes--; window.history.back(); } else { const newStack = currentStack.slice(0, -1); const newLevel = newStack.length; const currentTab = prev.currentTab || 'dashboard'; const newState = { ...prev, panoStack: newStack, level: newLevel, currentTab }; let url = `?tab=${currentTab}`; if (newStack.length > 0) { const top = newStack[newStack.length - 1]; url = buildUrl(currentTab, top.type, top.name, top.targetId); } window.history.replaceState(newState, '', url); window._panoRouterMaxLevel = newLevel; setAppState(newState); broadcastSync(); } }, []); const changeTab = useCallback((tabId) => { const newState = { panoStack: [], level: 0, currentTab: tabId, depth: window.pano_current_depth || 0 }; window.history.pushState(newState, '', `?tab=${tabId}`); window.pano_internal_pushes = 0; window._panoRouterMaxLevel = 0; setAppState(newState); broadcastSync(); }, []); const openModal = useCallback((name, id = null, silent = true) => pushLayer('modal', name, id, silent), [pushLayer]); const openDialog = useCallback((name, id = null, silent = true) => pushLayer('dialog', name, id, silent), [pushLayer]); const openChat = useCallback((chatId, silent = true) => pushLayer('chat', 'chat', chatId, silent), [pushLayer]); const setAlert = useCallback((name, silent = true) => pushLayer('alert', name, null, silent), [pushLayer]); const getFromStack = useCallback((type) => (appState.panoStack || []).find(l => l.type === type), [appState.panoStack]); return { activeModal: getFromStack('modal')?.name || null, activeDialog: getFromStack('dialog')?.name || null, activeChat: getFromStack('chat')?.targetId || null, targetId: getFromStack('modal')?.targetId || getFromStack('dialog')?.targetId || null, dialogId: getFromStack('dialog')?.targetId || null, alertParam: getFromStack('alert')?.name || null, currentTab: appState.currentTab, openModal, openDialog, openChat, setAlert, replaceCurrentLayer, closeCurrentLayer, changeTab }; }; // 4. - MOTEUR DE RECHERCHE ET PAGINATION window.pano_useSearchAndPagination = (initialData, filterFn, initialCount = 50) => { const { useState, useMemo, useEffect, useRef } = React; const [searchQuery, setSearchQuery] = useState(""); const [visibleCount, setVisibleCount] = useState(initialCount); // CORRECTION PERF 2 : Pattern useRef pour casser la boucle de dépendance du useMemo const filterFnRef = useRef(filterFn); useEffect(() => { filterFnRef.current = filterFn; }, [filterFn]); useEffect(() => { setVisibleCount(initialCount); }, [searchQuery, initialCount]); const filteredData = useMemo(() => { if (!searchQuery.trim() || typeof filterFnRef.current !== 'function') return initialData || []; const q = window.pano_normalizeString(searchQuery); return (initialData || []).filter(item => filterFnRef.current(item, q)); }, [initialData, searchQuery]); // <-- filterFn n'est plus dans les dépendances, fin des re-rendus infinis const displayedData = filteredData.slice(0, visibleCount); return { searchQuery, setSearchQuery, visibleCount, setVisibleCount, filteredData, displayedData }; }; // 5. - REGISTRES CENTRALISÉS window.pano_getIcons = () => { const fallback = () => null; return new Proxy({}, { get: (target, prop) => { if (typeof window['pano_' + prop] !== 'undefined') return window['pano_' + prop]; window.pano_logFallback(`Icône manquante : ${prop}`); return fallback; } }); }; window.pano_getComponents = () => { const FallbackNull = () => null; const FallbackTextLogo = ({ className = "", colorEco = "text-slate-800" }) => { const isHome = window.location.pathname === '/' && (!window.location.search || window.location.search === '?tab=accueil' || window.location.search === ''); const content = ( eco-panneau.fr ); return isHome ? content : {content}; }; const FallbackCardGrid = ({children, className = ""}) => (
{children}
); const FallbackDataCard = ({children, className = "", onClick, variant}) => (
{children}
); const fallbacks = { TextLogo: FallbackTextLogo, CardGrid: FallbackCardGrid, DataCard: FallbackDataCard, AdminPanelCard: FallbackNull, AdminLogisticsCard: FallbackNull, AdminMessageCard: FallbackNull }; return new Proxy({}, { get: (target, prop) => { if (typeof window['pano_' + prop] !== 'undefined') return window['pano_' + prop]; window.pano_logFallback(`Composant manquant : ${prop}`); return fallbacks[prop] || FallbackNull; } }); }; /* EOF ========== [_react/ui/_ui_utils.jsx] */