// ECO-PANNEAU.FR - _react/clients/_clients_modals_vault.jsx const { useState, useEffect } = React; window.pano_ClientLegalVaultModal = ({ panneau, setPanneau, myClientData = {}, onClose, refreshData }) => { // ZÉRO-DETTE : Utilisation de notre Hook abstrait ! const { isMounted, safeFetch } = window.pano_useSafeFetch(); // 1. - Composants et Icônes const { LockIcon, ArrowUpIcon, Trash2Icon, LoaderIcon, EyeIcon, AlertTriangleIcon, PlusIcon, CheckCircleIcon, XIcon } = window.pano_getIcons(); const { Modal, AlertBox, ConfirmModal, Button, VaultDocumentThumbnail, UniversalViewer, CardGrid } = window.pano_getComponents(); // 2. - États locaux const [isSaving, setIsSaving] = useState(false); const [uploadStats, setUploadStats] = useState({ current: 0, total: 0 }); const [uploadDetail, setUploadDetail] = useState(''); const [deleteStats, setDeleteStats] = useState({ current: 0, total: 0 }); const [vaultDragging, setVaultDragging] = useState(false); const [viewingDoc, setViewingDoc] = useState(null); const [confirmDialog, setConfirmDialog] = useState(null); const [uploadErrors, setUploadErrors] = useState([]); const [selectedDocs, setSelectedDocs] = useState([]); // CHARGEMENT DYNAMIQUE DES QUOTAS DEPUIS LE SERVEUR const [quotas, setQuotas] = useState({ privateFile: 10 }); useEffect(() => { safeFetch('sync', { silent: true }).then(d => { if (!isMounted.current) return; // Sécurité if (d?.data?.settings) { setQuotas({ privateFile: parseInt(d.data.settings.quota_upload_private_mb || 10, 10) }); } }); }, [safeFetch, isMounted]); // 3. - Droits d'accès et Sécurité Zéro-Trust Stricte const isOwner = String(panneau.client_uid) === String(myClientData?.id); const isMe = (uid) => String(uid) === String(myClientData?.id) || String(uid) === String(myClientData?.email_hash); const myCollab = !isOwner ? panneau.collaborators?.find(c => isMe(c.uid)) : null; const canEdit = isOwner || myCollab?.rights?.can_edit; if (!canEdit) { return (

Vous n'avez pas l'autorisation d'accéder au coffre-fort de ce panneau.

); } const docs = panneau.privateDocs || []; // ACCÉLÉRATEUR : Sélection de masse via Ctrl+A (ou Cmd+A) useEffect(() => { const handleKeyDown = (e) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a') { if (e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea') { return; } e.preventDefault(); const selectableIds = docs.filter(doc => { const normalizedName = window.pano_normalizeString ? window.pano_normalizeString(doc.name) : (doc.name || '').toLowerCase(); const isProtected = normalizedName.includes('attestation') && normalizedName.includes('activation'); return !isProtected; }).map(d => d.id); if (selectableIds.length > 0 && !isSaving) { setSelectedDocs(selectableIds); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [docs, isSaving]); // 4. - Actions métier const handleDelete = (docId) => { setConfirmDialog({ title: "Supprimer le document", message: "Ce document sera définitivement effacé du coffre-fort et du serveur. Continuer ?", confirmText: "Supprimer", isDestructive: true, onConfirm: async () => { setConfirmDialog(null); setIsSaving(true); const newDocs = docs.filter(d => d.id !== docId); const updatedPanneau = { ...panneau, privateDocs: newDocs }; try { await safeFetch('file/delete', { body: { id: docId }, silent: true }); } catch(e) {} if (!isMounted.current) return; // Sécurité anti-corruption if (setPanneau) setPanneau(updatedPanneau); setSelectedDocs(prev => prev.filter(id => id !== docId)); const d = await safeFetch('panneaux', { body: { id: panneau.id, status: panneau.status, offerType: panneau.offerType, currentRate: panneau.currentRate, physicalPanels: panneau.physicalPanels, details: updatedPanneau }, silent: true }); if (!isMounted.current) return; // Sécurité if (d) { if (window.pano_showToast) window.pano_showToast("Document supprimé.", "success"); if (refreshData) refreshData(); } setIsSaving(false); }, onCancel: () => setConfirmDialog(null) }); }; const handleBatchDelete = () => { if (selectedDocs.length === 0) return; setConfirmDialog({ title: "Supprimer la sélection", message: `Vous êtes sur le point de supprimer définitivement ${selectedDocs.length} document(s) du coffre-fort et du serveur. Continuer ?`, confirmText: "Supprimer", isDestructive: true, onConfirm: async () => { setConfirmDialog(null); setIsSaving(true); // Copie figée pour le parcours const docsToDelete = [...selectedDocs]; let currentDocs = [...docs]; setDeleteStats({ current: 1, total: docsToDelete.length }); let currentCount = 1; for (const docId of docsToDelete) { if (!isMounted.current) return; // SÉCURITÉ : Coupe la boucle si on quitte setDeleteStats({ current: currentCount, total: docsToDelete.length }); try { await safeFetch('file/delete', { body: { id: docId }, silent: true }); if (!isMounted.current) return; // Sécurité post-requête // Retrait visuel immédiat du fichier currentDocs = currentDocs.filter(d => d.id !== docId); if (setPanneau) setPanneau({ ...panneau, privateDocs: currentDocs }); setSelectedDocs(prev => prev.filter(id => id !== docId)); } catch(e) {} currentCount++; } if (!isMounted.current) return; // Sécurité avant sauvegarde finale // Synchronisation finale en base de données avec le tableau nettoyé const finalPanneau = { ...panneau, privateDocs: currentDocs }; const d = await safeFetch('panneaux', { body: { id: panneau.id, status: panneau.status, offerType: panneau.offerType, currentRate: panneau.currentRate, physicalPanels: panneau.physicalPanels, details: finalPanneau }, silent: true }); if (!isMounted.current) return; // Sécurité finale if (d) { if (window.pano_showToast) window.pano_showToast(`${docsToDelete.length} document(s) supprimé(s).`, "success"); if (refreshData) refreshData(); } setSelectedDocs([]); setDeleteStats({ current: 0, total: 0 }); setIsSaving(false); }, onCancel: () => setConfirmDialog(null) }); }; const handleVaultUpload = async (fileList) => { if (!fileList || fileList.length === 0) return; const files = Array.from(fileList); let currentTotalSize = docs.reduce((acc, doc) => acc + (doc.size || 0), 0); setIsSaving(true); setUploadStats({ current: 1, total: files.length }); setUploadDetail('Préparation...'); let uploadedDocs = []; let currentErrors = []; for (let i = 0; i < files.length; i++) { if (!isMounted.current) return; // SÉCURITÉ : Coupe la boucle si on quitte const file = files[i]; setUploadStats({ current: i + 1, total: files.length }); // VERIFICATION DYNAMIQUE DES QUOTAS ADMIN (Taille de fichier unique) if (file.size > quotas.privateFile * 1024 * 1024) { currentErrors.push({ file: file.name, reason: `Taille > ${quotas.privateFile} Mo` }); continue; } let strictType = ''; if (file.type === 'application/pdf') { strictType = 'pdf'; } else if (file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/webp') { strictType = 'image'; } else { currentErrors.push({ file: file.name, reason: "Format non supporté (PDF, JPG, PNG, WEBP)" }); continue; } try { // Utilitaire externe : On garde le coupe-circuit manuel pour lui const idObj = await window.pano_uploadFile( file, strictType, (msg, pct) => { if (isMounted.current) setUploadDetail(`${msg} (${pct}%)`); }, // Sécurité UI false, true, panneau.client_uid, panneau.id ); if (!isMounted.current) return; // Sécurité post-requête const actualId = idObj.id || idObj; if (actualId) { uploadedDocs.push({ id: actualId, type: strictType, name: file.name, date: window.pano_formatDate(new Date().toISOString()), size: file.size, numPages: idObj.numPages || 1 }); currentTotalSize += file.size; } } catch (err) { currentErrors.push({ file: file.name, reason: err.message || "Erreur de transfert" }); } } if (!isMounted.current) return; // Sécurité avant sauvegarde finale if (uploadedDocs.length > 0) { const newDocs = [...docs, ...uploadedDocs]; const updatedPanneau = { ...panneau, privateDocs: newDocs }; if (setPanneau) setPanneau(updatedPanneau); const d = await safeFetch('panneaux', { body: { id: panneau.id, status: panneau.status, offerType: panneau.offerType, currentRate: panneau.currentRate, physicalPanels: panneau.physicalPanels, details: updatedPanneau }, successMessage: `${uploadedDocs.length} document(s) importé(s).` }); if (!isMounted.current) return; // Sécurité finale if (d && refreshData) refreshData(); } setIsSaving(false); setUploadStats({ current: 0, total: 0 }); setUploadDetail(''); if (currentErrors.length > 0) { setUploadErrors(currentErrors); } }; // 5. - Rendu UI return ( <> (
{selectedDocs.length > 0 && (
{selectedDocs.length} sélectionné(s) {selectedDocs.length}
)}
)} >
Espace personnel destiné à conserver vos preuves de dépôt, constats d'huissier ou arrêtés. Ces documents ne sont pas accessibles au public.
{ e.preventDefault(); if(!isSaving) setVaultDragging(true); }} onDragLeave={(e) => { e.preventDefault(); setVaultDragging(false); }} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); setVaultDragging(false); if(!isSaving) handleVaultUpload(e.dataTransfer.files); }} className={`w-full min-h-[160px] border-2 border-dashed rounded-xl flex flex-col items-center justify-center p-6 transition relative ${vaultDragging ? 'border-purple-500 bg-purple-50' : 'border-slate-300 bg-slate-50 hover:bg-slate-100'}`} > {isSaving && uploadStats.total > 0 ? (
Importation en cours... Fichier {uploadStats.current}/{uploadStats.total} {uploadDetail}
) : isSaving && deleteStats.total > 0 ? (
Suppression en cours... Fichier {deleteStats.current}/{deleteStats.total}
) : isSaving ? (
Traitement en cours...
) : ( <>
Glissez-déposez vos documents ici PDF, JPG, PNG (Max {quotas.privateFile} Mo/fichier) )}
{docs.length > 0 ? ( {docs.map((doc, idx) => { const normalizedName = window.pano_normalizeString ? window.pano_normalizeString(doc.name) : (doc.name || '').toLowerCase(); const isAttestationActivation = normalizedName.includes('attestation') && normalizedName.includes('activation'); const isSelected = selectedDocs.includes(doc.id); return (
{ if (selectedDocs.length > 0 && !isAttestationActivation) { if (isSaving) return; setSelectedDocs(prev => isSelected ? prev.filter(id => id !== doc.id) : [...prev, doc.id]); } }} className={`relative group rounded-xl border ${isSelected ? 'border-red-400 ring-2 ring-red-400/20 bg-red-50/50' : 'border-slate-200 bg-slate-50 hover:border-purple-300 hover:shadow-md'} shadow-sm flex flex-col transition ${selectedDocs.length > 0 && !isAttestationActivation ? 'cursor-pointer' : ''}`} > {!isAttestationActivation && (
); })} ) : (

Aucun document stocké.

)}
Espace utilisé : {(docs.reduce((acc, doc) => acc + (doc.size || 0), 0) / (1024*1024)).toFixed(2)} Mo
{uploadErrors.length > 0 && Modal && ( setUploadErrors([])} actions={(close) => ( )} >

Certains fichiers n'ont pas pu être importés :

)} {viewingDoc && UniversalViewer && ( setViewingDoc(null)} /> )} {confirmDialog && ConfirmModal && ( setConfirmDialog(null)} /> )} ); }; /* EOF ========== [_react/clients/_clients_modals_vault.jsx] */