// 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 ( <> ( Fermer {selectedDocs.length > 0 && ( {selectedDocs.length} sélectionné(s) {selectedDocs.length} Supprimer setSelectedDocs([])} disabled={isSaving} className="ml-1 text-red-400 hover:text-red-700 p-1 disabled:opacity-50 transition-colors" title="Annuler la sélection"> )} )} > 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) Ajouter des documents handleVaultUpload(e.target.files)} disabled={isSaving} /> > )} {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 && ( { e.stopPropagation(); if(isSaving) return; setSelectedDocs(prev => isSelected ? prev.filter(id => id !== doc.id) : [...prev, doc.id]); }} className={`absolute top-2 right-2 z-30 p-1.5 w-7 h-7 transition-all duration-200 ${isSelected ? 'opacity-100 shadow-md scale-110 border-red-300' : 'opacity-0 group-hover:opacity-100 shadow-sm border-transparent bg-white/80 backdrop-blur-sm hover:bg-white'}`} title={isSelected ? "Désélectionner" : "Sélectionner pour suppression"} /> )} 0 && !isAttestationActivation ? '' : 'cursor-pointer'} ${isSelected ? 'bg-transparent' : 'bg-white'}`} onClick={(e) => { if (selectedDocs.length > 0 && !isAttestationActivation) { // Événement bubblé géré par le wrapper } else { e.stopPropagation(); if(!isSaving) setViewingDoc(doc); } }} > {VaultDocumentThumbnail ? : {doc.type}} {doc.name} {isAttestationActivation && Preuve légale} {doc.date} { e.stopPropagation(); if(!isSaving) setViewingDoc(doc); }} className={`p-1.5 shadow-none w-7 h-7 ${isSelected ? 'bg-white border-white hover:border-slate-200' : ''}`} title="Consulter" /> {!isAttestationActivation && ( { e.stopPropagation(); if(!isSaving) handleDelete(doc.id); }} className={`p-1.5 shadow-none w-7 h-7 hover:bg-red-50 ${isSelected ? 'bg-white border-white hover:border-red-200' : ''}`} title="Supprimer" /> )} ); })} ) : ( 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) => ( Fermer )} > Certains fichiers n'ont pas pu être importés : {uploadErrors.map((err, i) => ( {err.file} {err.reason} ))} )} {viewingDoc && UniversalViewer && ( setViewingDoc(null)} /> )} {confirmDialog && ConfirmModal && ( setConfirmDialog(null)} /> )} > ); }; /* EOF ========== [_react/clients/_clients_modals_vault.jsx] */
Vous n'avez pas l'autorisation d'accéder au coffre-fort de ce panneau.
{doc.name}
Aucun document stocké.
Certains fichiers n'ont pas pu être importés :