// ECO-PANNEAU.FR - _react/clients/_clients_messages.jsx // 1. - LOGIQUE MÉTIER : GÉNÉRATION DES CONVERSATIONS CLIENTS window.pano_buildClientChatThreads = (interactions, myClientData, visiblePanels, pendingInvites) => { const threadsMap = {}; const expectedFullName = myClientData.full_name || 'Utilisateur'; const expectedCompany = myClientData.name || 'Société'; const expectedAuthorName = `${expectedFullName} (${expectedCompany})`; // CORRECTION SÉCURITÉ : Tolérance universelle pour l'identification const isMe = (uid) => uid === myClientData.id || uid === myClientData.email_hash; interactions.forEach(m => { if (m.isAlert === 2) return; let threadId, title, targetEmail, type; const isMyOwnMessage = (m.authorType === 'Client' && ( m.author === 'Client' || m.author === myClientData.email || m.author === myClientData.full_name || m.author === myClientData.name || m.author === expectedAuthorName )); let unread = (!m.resolved && !isMyOwnMessage && m.authorType !== 'Systeme' && m.authorType !== 'System') ? 1 : 0; if (m.panneauId === `SUPPORT_${myClientData.id}`) { threadId = m.panneauId; title = "Support Technique"; targetEmail = "eco-panneau.fr"; type = 'support'; } 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) return; const pObj = visiblePanels.find(p => p.id === pId) || pendingInvites.find(p => p.id === pId); threadId = m.panneauId; title = pObj?.name ? `Équipe Projet : ${pObj.name}` : `Équipe Projet`; targetEmail = "Membres de l'équipe"; type = 'group'; } else { const panneauObj = visiblePanels.find(p => p.id === m.panneauId); if (!panneauObj) return; 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) return; const visitorEmail = (m.authorType === 'Admin' || m.authorType === 'Systeme' || m.authorType === 'System' || isMyOwnMessage) ? m.target : m.author; threadId = `${m.panneauId}_${visitorEmail}`; title = `Riverain : ${visitorEmail}`; targetEmail = panneauObj.name || ''; type = 'riverain'; } if (!threadsMap[threadId]) { threadsMap[threadId] = { id: threadId, panneauId: m.panneauId, title, targetEmail, type, messages: [], unread: 0 }; } threadsMap[threadId].messages.push(m); threadsMap[threadId].unread += unread; }); return Object.values(threadsMap).sort((a, b) => { const dateA = a.messages.length > 0 ? new Date(String(a.messages[a.messages.length - 1].created_at || '').replace(' ', 'T')).getTime() : 0; const dateB = b.messages.length > 0 ? new Date(String(b.messages[b.messages.length - 1].created_at || '').replace(' ', 'T')).getTime() : 0; return dateB - dateA; }); }; // 2. - COMPOSANT PRINCIPAL : MESSAGERIE CLIENT window.pano_ClientMessagesTab = ({ data, myClientData, refreshData, showToast, setPanelTeamModal, openLocalDialog, closeCurrentLayer, activeDialog, clientOpts, toggleDashboardPin, isLockedForClient }) => { const { useState, useMemo } = React; // 2.1 - SÉCURITÉ ANTI-FUITE DE MÉMOIRE : Utilisation du Hook Global Zéro-Dette const { isMounted, safeFetch } = window.pano_useSafeFetch(); // 2.2 - États et Routage Zéro-Dette const [isSaving, setIsSaving] = useState(false); const urlModal = window.pano_useUrlModal ? window.pano_useUrlModal() : {}; const routerActiveDialog = activeDialog || urlModal.activeDialog; const routerOpenDialog = openLocalDialog || urlModal.openDialog; const routerCloseLayer = closeCurrentLayer || urlModal.closeCurrentLayer; const [newThreadData, setNewThreadData] = useState({ subject: 'support_technique', message: '', specificPanel: '' }); // 2.3 - Composants et Icônes const { UsersIcon, MessageSquareIcon, MessageCircleIcon, MailIcon, AlertTriangleIcon, SendIcon, LoaderIcon, CheckCircleIcon, PlusIcon, PinIcon } = window.pano_getIcons(); const { IconBadge, NotificationBadge, Button, Modal, FormTextarea, CardGrid, DataCard, SearchBar, EmptySearch, PaginationFooter } = window.pano_getComponents(); // 2.4 - Logique de recherche et pagination const allPanels = data.panneaux || []; const interactions = data.interactions || []; const { pendingInvites, visiblePanels } = window.pano_getPanelAccessRights ? window.pano_getPanelAccessRights(allPanels, myClientData.id, myClientData.email_hash) : { pendingInvites:[], visiblePanels:[] }; let threads = window.pano_buildClientChatThreads(interactions, myClientData, visiblePanels, pendingInvites); if (isLockedForClient) { threads = threads.filter(t => t.type === 'support'); } const { searchQuery, setSearchQuery, visibleCount, setVisibleCount, filteredData } = window.pano_useSearchAndPagination(threads, (t, q) => { const n = window.pano_normalizeString; const statusStr = n(t.unread > 0 ? "non lu " : "lu "); return n(t.title).includes(q) || n(t.targetEmail).includes(q) || n(t.type).includes(q) || statusStr.includes(q); }); const openChat = (id, e) => { if (e) { e.preventDefault(); e.stopPropagation(); } urlModal.openChat(id, false); if (refreshData) refreshData(); }; // 2.5 - Actions métier sécurisées const handleNewThreadSubmit = async (e) => { e.preventDefault(); if (!newThreadData.message.trim()) return; let endpoint = 'interactions'; let body = {}; if (newThreadData.subject === 'support_technique' || newThreadData.subject === 'support_billing' || newThreadData.subject === 'support_legal') { body = { panneauId: `SUPPORT_${myClientData.id}`, detail: `[${newThreadData.subject.split('_')[1].toUpperCase()}] ${newThreadData.message}`, author: 'Client', targetEmail: 'Support Technique', type: 'message' }; } else if (newThreadData.subject === 'equipe_projet') { if (!newThreadData.specificPanel) return showToast("Veuillez sélectionner un panneau.", "error"); body = { panneauId: `GROUP_${newThreadData.specificPanel}`, detail: newThreadData.message, author: 'Client', targetEmail: 'Groupe', type: 'message' }; } else { return; } const d = await safeFetch(endpoint, { body, setLoading: setIsSaving, successMessage: "Message envoyé avec succès !" }); if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit anti-fuite de mémoire if (d) { setNewThreadData({ subject: 'support_technique', message: '', specificPanel: '' }); if (refreshData) refreshData(); // Remplacement de la couche de dialogue actuelle par la modale de Chat urlModal.replaceCurrentLayer('chat', 'chat', body.panneauId, false); } }; // 2.6 - Rendu dynamique des cartes const getThreadIcon = (type) => { switch(type) { case 'support': return MessageSquareIcon; case 'group': return UsersIcon; case 'riverain': return MessageCircleIcon; default: return MailIcon; } }; const getThreadVariant = (type) => { switch(type) { case 'support': return 'success'; case 'group': return 'info'; case 'riverain': return 'warning'; default: return 'secondary'; } }; const getThreadTitleClass = (type) => { if (type === 'support') return 'font-black text-emerald-700'; if (type === 'group') return 'font-black text-blue-700'; if (type === 'riverain') return 'font-black text-amber-700'; return 'font-bold text-slate-800'; }; const pinnedIds = clientOpts?.pinned_threads || []; const attentionThreads = []; const pinnedThreads = []; const otherThreads = []; // CLASSIFICATION : URGENCE > FAVORIS > RESTE filteredData.forEach(t => { const isPinned = pinnedIds.includes(t.id); if (t.unread > 0) { attentionThreads.push(t); } else if (isPinned) { pinnedThreads.push(t); } else { otherThreads.push(t); } }); const displayedOtherThreads = otherThreads.slice(0, visibleCount); const renderThreadCard = (t) => { const lastMsg = t.messages.length > 0 ? t.messages[t.messages.length - 1] : null; const isPinned = pinnedIds.includes(t.id); return ( openChat(t.id, e)} className="group gap-3 relative cursor-pointer p-4 transition-colors hover:border-emerald-300" > {/* ÉPINGLE VOLANTE ET INCLINÉE (INDIGO POUR LES MESSAGES) */}
{ e.stopPropagation(); toggleDashboardPin && toggleDashboardPin('pinned_threads', t.id, e); }} className={`absolute top-2 right-2 z-20 p-1 cursor-pointer transition-all duration-200 ${isPinned ? 'text-indigo-500 opacity-100 hover:scale-110' : 'text-slate-300 opacity-0 group-hover:opacity-100 hover:text-slate-500 hover:scale-110'}`} style={{ transform: 'rotate(30deg)' }} title={isPinned ? "Désépingler" : "Épingler au tableau de bord"} >
{IconBadge && ( )}

{t.title}

{t.targetEmail &&

{t.targetEmail}

}
0 ? 'text-slate-800 font-bold' : 'text-slate-600'}`}> {lastMsg ? lastMsg.detail.replace(/\[ATTACHMENT:[^\]]+\]/g, '[Pièce jointe]').replace(/<[^>]*>?/gm, '') : Nouvelle conversation...}
{t.type} {lastMsg ? new Date(String(lastMsg.created_at || '').replace(' ', 'T')).toLocaleString('fr-FR') : ''}
); }; // 3. - RENDU UI return (
{/* 3.1 - En-tête */}

Messagerie Globale

Boîte de réception centralisée (Support, Riverains, Équipe).

{threads.length > 0 && SearchBar && (
)} {threads.length === 0 && (
)}
{/* 3.2 - Liste des conversations */} {threads.length === 0 ? (

Votre boîte de réception est vide.

) : filteredData.length === 0 ? ( EmptySearch && ) : ( <> {/* A. PRIORITÉ */} {attentionThreads.length > 0 && (

Messages non lus

{attentionThreads.map(renderThreadCard)}
)} {/* B. FAVORIS */} {pinnedThreads.length > 0 && (

Favoris (Épinglés)

{pinnedThreads.map(renderThreadCard)}
)} {/* C. LE RESTE */} {otherThreads.length > 0 && (

Historique des conversations

{displayedOtherThreads.map(renderThreadCard)} {PaginationFooter && ( )}
)} )} {/* 4. - Modale : Nouvelle conversation */} {routerActiveDialog === 'new_thread' && Modal && ( ( <> )} >
{newThreadData.subject === 'equipe_projet' && (

Ce message sera visible par tous les collaborateurs ayant accès à ce panneau.

{newThreadData.specificPanel && setPanelTeamModal && (
{ routerCloseLayer(e); setTimeout(() => { setPanelTeamModal(visiblePanels.find(p=>p.id===newThreadData.specificPanel)); routerOpenDialog('team', null, false); }, 200); }} className="text-[10px] font-bold text-blue-600 hover:text-blue-800 cursor-pointer"> Gérer l'équipe
)}
)} {FormTextarea && ( setNewThreadData({...newThreadData, message: e.target.value})} rows={5} required disabled={isSaving} /> )}
)}
); }; /* EOF ========== [_react/clients/_clients_messages.jsx] */