/**
* =========================================================================
* PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0
* Socle UI (1/4) - Utilitaires, API et Logique Métier
* =========================================================================
*/
// =========================================================================
// 1. SYSTÈME DE NOTIFICATIONS GLOBAL (TOASTS) - DROP IN
// =========================================================================
window.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 bottom-4 right-4 sm:bottom-6 sm:right-6 z-[99999] flex flex-col items-end gap-3 pointer-events-none';
document.body.appendChild(container);
}
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) => ``;
const iconSvg = type === 'success' ? svgWrap(window.svgPaths?.CheckCircle || '') :
(type === 'error' || type === 'warning' ? svgWrap(window.svgPaths?.AlertTriangle || '') :
svgWrap(window.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`;
toast.innerHTML = `
${iconSvg}
${message}
`;
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);
};
// =========================================================================
// 2. UTILITAIRES ET LOGIQUE MÉTIER (BUSINESS LOGIC)
// =========================================================================
window.apiFetch = async (endpoint, options = {}) => {
const { method = 'POST', body, setLoading, successMessage, errorMessage = "Erreur réseau ou serveur" } = options;
if (setLoading) setLoading(true);
try {
const fetchOptions = { method };
if (body) {
const cleanBody = JSON.parse(JSON.stringify(body), (key, value) => {
if (key === 'imageUrl' || key === 'maitreOuvrageLogoUrl' || key === 'logoUrl') {
return undefined;
}
if (typeof value === 'string' && value.includes('data:image/')) {
return value.replace(/(src|href)=["']data:image\/[^;]+;base64,[^"']+["']/gi, '$1="#"');
}
return value;
});
fetchOptions.body = JSON.stringify(cleanBody);
}
const res = await fetch(window.ECO_CONFIG.apiBaseUrl + endpoint, fetchOptions);
const d = await res.json();
if (d.status === 'success') {
if (successMessage) window.showToast(successMessage, "success");
return d;
} else {
window.showToast(d.message || errorMessage, "error");
return null;
}
} catch (e) {
window.showToast(errorMessage, "error");
return null;
} finally {
if (setLoading) setLoading(false);
}
};
window.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.getFileUrl = (type, id, isPrivate = false, isPreview = false) => {
if (!id) return '';
let url = `?api=file/download&type=${type}&id=${id}`;
if (isPrivate) url += '&private=1';
if (isPreview) url += '&preview=1';
return url;
};
window.getPanelAccessRights = (allPanels = [], clientUid) => {
const pendingInvites = allPanels.filter(p => p.client_uid !== clientUid && p.collaborators?.some(c => c.uid === clientUid && 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 => c.uid === clientUid && c.status === 'accepted'));
const visiblePanels = [...ownedPanels, ...acceptedCollabPanels];
return { pendingInvites, ownedPanels, acceptedCollabPanels, visiblePanels };
};
window.computeClientUnread = (interactions = [], myClientData = {}, visiblePanels = [], pendingInvites = []) => {
let unread = 0;
const expectedFullName = myClientData.full_name || 'Utilisateur';
const expectedCompany = myClientData.name || 'Société';
const expectedAuthorName = `${expectedFullName} (${expectedCompany})`;
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') {
unread++;
}
}
else {
if (m.authorType !== 'Client' && m.authorType !== 'Admin' && m.authorType !== 'Systeme') {
unread++;
}
}
});
return unread;
};
window.uploadFile = async (file, type, onProgress, isPublic = false, isPrivate = false, clientUid = null, panneauId = null) => {
let fileToSend = file;
let previewB64s = {};
let totalPages = 1;
let hasLocalProcessing = false;
try {
if (type === 'image' && file.type.startsWith('image/')) {
if (onProgress) onProgress("optimisation", 10);
const img = new Image();
const imgUrl = URL.createObjectURL(file);
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = () => reject(new Error("Le navigateur n'a pas pu lire l'image."));
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);
const blob = await new Promise((resolve, reject) => {
canvas.toBlob(b => {
if (b) resolve(b);
else {
canvas.toBlob(b2 => {
if (b2) resolve(b2);
else reject(new Error("Échec de la conversion de l'image (toBlob)."));
}, 'image/jpeg', 0.85);
}
}, 'image/webp', 0.85);
});
const ext = blob.type === 'image/jpeg' ? '.jpg' : '.webp';
fileToSend = new File([blob], file.name.replace(/\.[^/.]+$/, "") + ext, { type: blob.type });
canvas.width = 0; canvas.height = 0;
hasLocalProcessing = true;
if (onProgress) onProgress("optimisation", 30);
}
else if (type === 'pdf' && file.type === 'application/pdf') {
if (onProgress) onProgress("préparation", 5);
if (!window.pdfjsLib) {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = '_externe/pdfjs/pdf.min.js';
script.onload = () => {
window.pdfjsLib.GlobalWorkerOptions.workerSrc = '_externe/pdfjs/pdf.worker.min.js';
resolve();
};
script.onerror = () => reject(new Error("Impossible de charger PDF.js."));
document.head.appendChild(script);
});
}
if (window.pdfjsLib) {
if (onProgress) onProgress("préparation", 10);
const arrayBuffer = await file.arrayBuffer();
const loadingTask = window.pdfjsLib.getDocument({ data: arrayBuffer });
const pdf = await loadingTask.promise;
totalPages = Math.min(pdf.numPages, 50);
const page1 = await pdf.getPage(1);
const viewportThumb = page1.getViewport({ scale: 0.4 });
const canvasThumb = document.createElement('canvas');
canvasThumb.width = viewportThumb.width;
canvasThumb.height = viewportThumb.height;
await page1.render({ canvasContext: canvasThumb.getContext('2d'), viewport: viewportThumb }).promise;
previewB64s['preview_b64_0'] = canvasThumb.toDataURL('image/webp', 0.6);
canvasThumb.width = 0; canvasThumb.height = 0;
page1.cleanup();
for (let i = 1; i <= totalPages; i++) {
if (onProgress) onProgress(`page ${i}/${totalPages}`, 10 + Math.round((i/totalPages)*20));
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 1.0 });
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.7);
if (!dataUrl || dataUrl === 'data:,') {
dataUrl = canvas.toDataURL('image/jpeg', 0.7);
}
previewB64s[`preview_b64_${i}`] = dataUrl;
canvas.width = 0; canvas.height = 0;
page.cleanup();
}
pdf.destroy();
hasLocalProcessing = true;
}
}
} catch (e) {
console.error("Erreur lors du traitement local du fichier :", e);
window.showToast("Note : Le document est envoyé sans optimisation ni miniature locale (Aperçu web indisponible).", "warning");
hasLocalProcessing = false;
fileToSend = file;
previewB64s = {};
totalPages = 1;
}
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('file', fileToSend);
formData.append('type', type);
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(previewB64s).length > 0) {
for (const [key, b64] of Object.entries(previewB64s)) {
formData.append(key, b64);
}
formData.append('numPages', totalPages);
}
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 ${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("terminé", 100);
const ret = new String(res.data.id);
ret.id = res.data.id;
ret.numPages = res.data.numPages || 1;
resolve(ret);
} 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. HOOKS ET FACTORISATION UI
// =========================================================================
/**
* Hook personnalisé pour gérer l'état des modales/dialogues via l'URL
* Centralise la logique SPA Layer 1 (modal) et Layer 2 (dialog)
*/
window.useUrlModal = () => {
const { useState, useEffect } = React;
const [activeModal, setActiveModal] = useState(new URLSearchParams(window.location.search).get('modal'));
const [activeDialog, setActiveDialog] = useState(new URLSearchParams(window.location.search).get('dialog'));
const [targetId, setTargetId] = useState(new URLSearchParams(window.location.search).get('targetId'));
const [alertParam, setAlertParam] = useState(new URLSearchParams(window.location.search).get('alert'));
useEffect(() => {
const handlePopState = () => {
const params = new URLSearchParams(window.location.search);
setActiveModal(params.get('modal'));
setActiveDialog(params.get('dialog'));
setTargetId(params.get('targetId'));
setAlertParam(params.get('alert'));
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, []);
const openModal = (type, id = null) => {
const u = new URL(window.location);
u.searchParams.set('modal', type);
if (id) u.searchParams.set('targetId', id);
window.history.pushState({ modal: type }, '', u);
window.dispatchEvent(new Event('popstate'));
};
const openDialog = (type, id = null) => {
const u = new URL(window.location);
u.searchParams.set('dialog', type);
if (id) u.searchParams.set('targetId', id);
window.history.pushState({ dialog: type }, '', u);
window.dispatchEvent(new Event('popstate'));
};
const setAlert = (type) => {
const u = new URL(window.location);
u.searchParams.set('alert', type);
window.history.pushState({ alert: type }, '', u);
window.dispatchEvent(new Event('popstate'));
};
const closeCurrentLayer = () => {
window.history.back();
};
return { activeModal, activeDialog, targetId, alertParam, openModal, openDialog, setAlert, closeCurrentLayer };
};
/**
* Utilitaire pour récupérer les icônes de manière sécurisée sans polluer le scope global des composants
*/
window.getIcons = () => {
const fallback = () => null;
return new Proxy({}, {
get: (target, prop) => {
if (typeof window[prop] !== 'undefined') return window[prop];
return fallback;
}
});
};
/* EOF ========== [_www/_react/_ui_utils.jsx] */