// ECO-PANNEAU.FR - _react/clients/_clients_modals_team.jsx const { useState, useEffect, useMemo } = React; // 1. - MODALE : GESTION DE L'ÉQUIPE DU PANNEAU window.pano_ClientPanelTeamModal = ({ panneau, myClientData, teamMembers, onClose, refreshData, showToast }) => { // ZÉRO-DETTE : Utilisation de notre Hook abstrait ! const { isMounted, safeFetch } = window.pano_useSafeFetch(); // 1.1 - États locaux const [inviteEmail, setInviteEmail] = useState(''); const [selectedTeamMember, setSelectedTeamMember] = useState(''); const [canEdit, setCanEdit] = useState(false); const [chatAccess, setChatAccess] = useState('none'); const [lockedFields, setLockedFields] = useState([]); const [isSaving, setIsSaving] = useState(false); const [confirmDialog, setConfirmDialog] = useState(null); const [tempFullName, setTempFullName] = useState(''); const [optimisticCollabs, setOptimisticCollabs] = useState(panneau.collaborators || []); const isDirty = inviteEmail.trim() !== '' || selectedTeamMember !== '' || canEdit || chatAccess !== 'none' || lockedFields.length > 0 || tempFullName.trim() !== ''; // 1.2 - Composants et Icônes const { UsersIcon, MailIcon, PlusIcon, Trash2Icon, LoaderIcon, ShieldIcon, LogOutIcon, AlertTriangleIcon, SaveIcon, LockIcon, CheckCircleIcon, RefreshCwIcon } = window.pano_getIcons(); const { AlertBox, ConfirmModal, Button, Modal } = window.pano_getComponents(); // 2. - Logique métier et permissions useEffect(() => { setOptimisticCollabs(panneau.collaborators || []); }, [panneau.collaborators]); const isOwner = panneau.client_uid === myClientData.id; const isMe = (uid) => uid === myClientData.id || uid === myClientData.email_hash; // CORRECTION SÉCURITÉ : Application de la fonction `isMe` pour autoriser le vrai UID const visibleCollaborators = isOwner ? optimisticCollabs : optimisticCollabs.filter(c => isMe(c.uid)); const hasFullName = !!(myClientData.full_name && myClientData.full_name.trim()); // Filtrage dynamique : on retire les contacts déjà invités const availableTeamMembers = useMemo(() => { return (teamMembers || []).filter(tm => !optimisticCollabs.some(c => c.email.toLowerCase() === tm.email.toLowerCase()) ); }, [teamMembers, optimisticCollabs]); // Formatage propre du nom du propriétaire "Société (Nom)" const myCollabRecord = !isOwner ? optimisticCollabs.find(c => isMe(c.uid)) : null; const ownerCompanyName = isOwner ? myClientData.name : (panneau.owner_company || myCollabRecord?.inviter_company || 'Propriétaire'); const ownerPersonName = isOwner ? myClientData.full_name : (panneau.owner_name || myCollabRecord?.inviter_name || ''); const formattedOwnerName = ownerPersonName ? `${ownerCompanyName} (${ownerPersonName})` : ownerCompanyName; const ownerDisplayName = isOwner ? `Vous - ${formattedOwnerName}` : formattedOwnerName; const AVAILABLE_FIELDS = [ { id: 'name', label: 'Nom du chantier' }, { id: 'location', label: 'Lieu' }, { id: 'maitreOuvrage', label: "Maître d'ouvrage" }, { id: 'permitNumber', label: 'N° de permis' }, { id: 'description', label: 'Description' }, { id: 'promoterLink', label: 'Lien promoteur' }, { id: 'emergencyPhone', label: 'N° d\'urgence' }, { id: 'noiseSchedule', label: 'Horaires' }, { id: 'intervenants', label: 'Intervenants' }, { id: 'lots', label: 'Lots' }, { id: 'pdfId', label: 'Arrêté (PDF)' } ]; const handleSaveFullName = async (e) => { e.preventDefault(); if (!tempFullName.trim()) return; const payload = { ...myClientData, full_name: tempFullName }; const d = await safeFetch('clients/profile/update', { body: payload, setLoading: setIsSaving, successMessage: "Identité enregistrée avec succès." }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d) { setTempFullName(''); // Reset après succès if (refreshData) refreshData(); } }; const handleInvite = async (e) => { e.preventDefault(); const targetEmail = (selectedTeamMember || inviteEmail).trim(); if (!targetEmail) return; // VERROU ANTI-DOUBLON if (optimisticCollabs.some(c => c.email.toLowerCase() === targetEmail.toLowerCase())) { if (showToast) showToast("Ce contact fait déjà partie de l'équipe ou a déjà été invité.", "warning"); return; } const tempUid = 'temp_' + Date.now(); setOptimisticCollabs(prev => [...prev, { uid: tempUid, email: targetEmail, company: 'En cours...', name: 'Invitation', status: 'pending', rights: { can_edit: canEdit, chat_access: chatAccess, locked_fields: lockedFields } }]); const d = await safeFetch('panneaux/collaborators/invite', { body: { panneau_id: panneau.id, email: targetEmail, can_edit: canEdit, chat_access: chatAccess, locked_fields: lockedFields }, setLoading: setIsSaving, successMessage: "Invitation envoyée avec succès." }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d) { setInviteEmail(''); setSelectedTeamMember(''); setCanEdit(false); setChatAccess('none'); setLockedFields([]); if (refreshData) refreshData(); } else { setOptimisticCollabs(prev => prev.filter(c => c.uid !== tempUid)); } }; const handleResendInvite = async (collab) => { const d = await safeFetch('panneaux/collaborators/invite', { body: { panneau_id: panneau.id, email: collab.email, can_edit: collab.rights?.can_edit || false, chat_access: collab.rights?.chat_access || 'none', locked_fields: collab.rights?.locked_fields || [], resend: true }, setLoading: setIsSaving, successMessage: "L'invitation a été renvoyée avec succès." }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d && refreshData) refreshData(); }; const handleRemove = (uid) => { setConfirmDialog({ title: "Retirer l'accès", message: "Êtes-vous sûr de vouloir retirer ce collaborateur du projet ?", confirmText: "Oui, retirer", isDestructive: true, onConfirm: async () => { setConfirmDialog(null); setOptimisticCollabs(prev => prev.filter(c => c.uid !== uid)); const d = await safeFetch('panneaux/collaborators/remove', { body: { panneau_id: panneau.id, target_uid: uid }, setLoading: setIsSaving, successMessage: "Collaborateur retiré du panneau." }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d && refreshData) refreshData(); } }); }; const handleUpdateRights = async (uid, can_edit, chat_access, locked_fields) => { setOptimisticCollabs(prev => prev.map(c => c.uid === uid ? { ...c, rights: { can_edit, chat_access, locked_fields } } : c )); const d = await safeFetch('panneaux/collaborators/update', { body: { panneau_id: panneau.id, target_uid: uid, rights: { can_edit, chat_access, locked_fields } }, setLoading: setIsSaving, successMessage: "Droits mis à jour avec succès." }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d && refreshData) refreshData(); }; const handleLeave = () => { setConfirmDialog({ title: "Quitter le projet", message: "Êtes-vous sûr de vouloir quitter ce projet ? Vous perdrez l'accès à ce panneau et à son groupe de discussion.", confirmText: "Oui, quitter", isDestructive: true, onConfirm: async () => { setConfirmDialog(null); const d = await safeFetch('panneaux/collaborators/leave', { body: { panneau_id: panneau.id }, setLoading: setIsSaving, successMessage: "Vous avez quitté le projet." }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit if (d) { onClose(); if (refreshData) refreshData(); } } }); }; // 3. - Rendu UI return ( <> ( )} >
Gérez les accès à ce panneau. Chaque membre invité aura accès au groupe de discussion intégré. {!hasFullName && isOwner && (

Identité requise

Pour inviter des collaborateurs, vous devez renseigner votre nom complet afin qu'ils puissent vous identifier dans l'e-mail d'invitation.

setTempFullName(e.target.value)} placeholder="Prénom et Nom" required disabled={isSaving} className="flex-1 min-w-0 border-2 border-orange-200 rounded-xl p-3 text-sm outline-none focus:border-orange-500 bg-white disabled:opacity-50 font-bold text-slate-800" />
)} {isOwner && hasFullName && (

Inviter un collaborateur

setInviteEmail(e.target.value)} placeholder="architecte@exemple.com" className="w-full min-w-0 border-2 border-slate-200 rounded-xl p-2.5 text-sm outline-none focus:border-blue-500 disabled:bg-slate-50 font-bold text-slate-800" />
{canEdit && (
Verrouiller ces champs (Lecture seule) :
{AVAILABLE_FIELDS.map(f => (
!isSaving && setLockedFields(prev => prev.includes(f.id) ? prev.filter(x => x !== f.id) : [...prev, f.id])} className={`px-3 py-1.5 rounded-lg text-xs font-bold transition border ${isSaving ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${lockedFields.includes(f.id) ? 'bg-red-50 text-red-600 border-red-200' : 'bg-slate-50 text-slate-500 border-slate-200 hover:bg-slate-100'}`} > {lockedFields.includes(f.id) && }{f.label}
))}
)}
Accès aux conversations avec les riverains :
)}

Membres du projet

{ownerDisplayName}

{isOwner ? "Tous les droits" : "Propriétaire"}

{visibleCollaborators.map(c => { const isPending = c.status === 'pending'; const isTemp = c.uid.startsWith('temp_'); const currentLocked = c.rights?.locked_fields || []; return (

{c.company} ({c.name})

{isPending ? (
{isTemp ? : null} En attente {!isTemp && ( )}
) : ( Actif )}
{isOwner && (
Riverains:
{!!c.rights?.can_edit && (
Champs verrouillés (Lecture seule) :
{AVAILABLE_FIELDS.map(f => { const isLocked = currentLocked.includes(f.id); return (
{ if (isTemp || isSaving) return; const newLocked = isLocked ? currentLocked.filter(x => x !== f.id) : [...currentLocked, f.id]; handleUpdateRights(c.uid, c.rights?.can_edit, c.rights?.chat_access, newLocked); }} className={`px-2 py-1 rounded text-[10px] font-bold transition border ${isTemp || isSaving ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} ${isLocked ? 'bg-red-50 text-red-600 border-red-200' : 'bg-white text-slate-500 border-slate-200 hover:bg-slate-50'}`} > {isLocked && }{f.label}
); })}
)}
)}
); })} {visibleCollaborators.length === 0 && (

Aucun collaborateur invité sur ce projet.

)} {!isOwner && (
)}
{confirmDialog && ConfirmModal && ( setConfirmDialog(null)} /> )} ); }; /* EOF ========== [_react/clients/_clients_modals_team.jsx] */