// 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 (
{t.title} {t.targetEmail}
Boîte de réception centralisée (Support, Riverains, Équipe).
Votre boîte de réception est vide.