/**
* =========================================================================
* PLATEFORME ECO-PANNEAU.FR - VERSION 1.0.0
* Interface Client - Onglets : Dashboard, Messagerie et Facturation
* =========================================================================
*/
// =========================================================================
// FACTORISATION : LOGIQUE MÉTIER DES MESSAGES CLIENT
// =========================================================================
window.buildClientChatThreads = (interactions, myClientData, visiblePanels, pendingInvites) => {
const supportThreadId = `SUPPORT_${myClientData.id}`;
const threadsMap = {
[supportThreadId]: { id: supportThreadId, title: "Support technique", targetEmail: 'Admin', messages: [], unread: 0, type: 'support' }
};
[...visiblePanels, ...pendingInvites].forEach(p => {
const groupId = `GROUP_${p.id}`;
threadsMap[groupId] = {
id: groupId, panneauId: p.id, title: `Équipe du panneau : ${p.name || 'Brouillon'}`,
targetEmail: 'Groupe', messages: [], unread: 0, type: 'group', panneauData: p
};
});
interactions.forEach(m => {
if (m.panneauId === supportThreadId) {
threadsMap[supportThreadId].messages.push(m);
if (!m.resolved && m.author !== 'Client' && m.author !== myClientData.email && m.author !== myClientData.full_name && m.author !== myClientData.name) {
threadsMap[supportThreadId].unread++;
}
} else if (m.panneauId.startsWith('GROUP_')) {
if (threadsMap[m.panneauId]) {
threadsMap[m.panneauId].messages.push(m);
if (!m.resolved && m.author !== 'Client' && m.author !== myClientData.email && m.author !== myClientData.full_name && m.author !== myClientData.name) {
threadsMap[m.panneauId].unread++;
}
}
} else {
const isInternalMsg = m.authorType === 'Client' || m.authorType === 'Admin';
const riverainEmail = (!isInternalMsg && m.author !== myClientData.email && m.author !== myClientData.full_name && m.author !== myClientData.name) ? m.author : m.target;
if (riverainEmail && riverainEmail !== 'Groupe') {
const threadId = `${m.panneauId}_${riverainEmail}`;
const panneauObj = visiblePanels.find(c => c.id === m.panneauId);
if (panneauObj) {
if (!threadsMap[threadId]) {
threadsMap[threadId] = { id: threadId, panneauId: m.panneauId, title: `Riverain : ${panneauObj.name} - ${riverainEmail}`, targetEmail: riverainEmail, messages: [], unread: 0, type: 'riverain' };
}
threadsMap[threadId].messages.push(m);
if (!isInternalMsg && !m.resolved) threadsMap[threadId].unread++;
}
}
}
});
return Object.values(threadsMap)
.filter(t => t.type === 'support' || t.messages.length > 0 || t.type === 'group')
.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;
});
};
// =========================================================================
// 1. ONGLET : TABLEAU DE BORD
// =========================================================================
window.ClientDashboardTab = ({
data,
myClientData,
refreshData,
showToast,
setActiveTab,
setManagingPanneau,
setValidationErrors,
setModalStep,
setIsModalOpen,
setPreviewPanneau
}) => {
const { useState } = React;
const [isSaving, setIsSaving] = useState(false);
// RÉCUPÉRATION SÉCURISÉE DES COMPOSANTS ET ICÔNES
const PlusIcon = window.PlusIcon || (() => null);
const AlertTriangleIcon = window.AlertTriangleIcon || (() => null);
const UsersIcon = window.UsersIcon || (() => null);
const CheckCircleIcon = window.CheckCircleIcon || (() => null);
const XIcon = window.XIcon || (() => null);
const MessageSquareIcon = window.MessageSquareIcon || (() => null);
const EyeIcon = window.EyeIcon || (() => null);
const ShieldCheckIcon = window.ShieldCheckIcon || (() => null);
const EditIcon = window.EditIcon || (() => null);
const UserCircleIcon = window.UserCircleIcon || (() => null);
const ActivityIcon = window.ActivityIcon || (() => null);
const StatCard = window.StatCard || (() => null);
const SimpleBarChart = window.SimpleBarChart || (() => null);
// Fallback robuste avec restauration de la soumission native au clavier
const Button = window.Button || (({children, onClick, className, disabled, title, icon: Icon, type}) => {
const handleAction = (e) => {
if (disabled) return;
if (onClick) onClick(e);
if (type === 'submit') {
const form = e.currentTarget.closest('form');
if (form && typeof form.requestSubmit === 'function') form.requestSubmit();
}
};
return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleAction(e); } }}
className={`flex items-center justify-center gap-2 cursor-pointer transition ${className || ''} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
title={title}
>
{Icon && }
{children}
{type === 'submit' && }
);
});
// FACTORISATION : Logique d'accès
const { pendingInvites, visiblePanels } = window.getPanelAccessRights
? window.getPanelAccessRights(data.panneaux || [], myClientData.id)
: { pendingInvites:[], visiblePanels:[] };
const activePanneauxCount = visiblePanels.filter(c => c.status === 'Actif').length;
const draftPanneauxCount = visiblePanels.filter(c => c.status === 'Brouillon').length;
const isSuspended = myClientData.paymentStatus === 'suspended';
const purchasesAllowed = data.settings?.allow_new_purchases !== '0';
// FACTORISATION : Décompte des messages
const totalUnread = window.computeClientUnread
? window.computeClientUnread(data.interactions || [], myClientData, visiblePanels, pendingInvites)
: 0;
const respondToInvite = async (panneauId, action) => {
const d = await window.apiFetch('panneaux/collaborators/respond', {
body: { panneau_id: panneauId, action },
setLoading: setIsSaving,
successMessage: "Invitation " + (action === 'accept' ? 'acceptée' : 'refusée') + " avec succès."
});
if (d) refreshData();
};
return (
{/* Fallback propre : full_name en priorité, sinon name (société), sinon 'Client' */}
Bienvenue, {myClientData.full_name || myClientData.name || 'Client'}
Gérez vos obligations légales BTP en toute simplicité sur eco-panneau.fr.
{!isSuspended && purchasesAllowed && (
)}
{/* Alertes système */}
{(!purchasesAllowed) && (
La création de nouvelles commandes est temporairement suspendue.
)}
{isSuspended && (
Facture impayée - compte suspendu
L'édition de vos panneaux est temporairement bloquée. Veuillez régulariser votre situation dans l'onglet Factures.
)}
{/* Invitations en attente */}
{pendingInvites.length > 0 && (
Invitations en attente
{pendingInvites.map(p => {
const collab = p.collaborators?.find(c => c.uid === myClientData.id);
return (
{p.name}
Invitation de {collab?.inviter_name || 'le propriétaire'} ({collab?.inviter_company || 'Société'})
);
})}
)}
{StatCard && (
<>
} color="text-emerald-600" bg="bg-emerald-100" onClick={() => setActiveTab('panels')} />
} color="text-slate-600" bg="bg-slate-200" onClick={() => setActiveTab('panels')} />
} color="text-blue-600" bg="bg-blue-100" />
} color="text-amber-600" bg="bg-amber-100" onClick={() => setActiveTab('messages')} />
>
)}
{data.stats?.history && data.stats.history.length > 0 && SimpleBarChart && (
Activité des riverains (30 derniers jours)
)}
);
};
// =========================================================================
// 2. ONGLET : MESSAGERIE ET COMMUNICATIONS (DRY)
// =========================================================================
window.ClientMessagesTab = ({ data, myClientData, refreshData }) => {
const { useState } = React;
const [mobileChatView, setMobileChatView] = useState(false);
const ChatBox = window.ChatBox || (() => null);
const ChatLayout = window.ChatLayout || (({children}) => {children}
);
const { pendingInvites, visiblePanels } = window.getPanelAccessRights
? window.getPanelAccessRights(data.panneaux || [], myClientData.id)
: { pendingInvites:[], visiblePanels:[] };
const threads = window.buildClientChatThreads
? window.buildClientChatThreads(data.interactions || [], myClientData, visiblePanels, pendingInvites)
: [];
const requestedChatId = new URLSearchParams(window.location.search).get('chat_id');
const selectedThreadId = requestedChatId || (threads[0]?.id);
const setChatId = (id) => {
const u = new URL(window.location);
u.searchParams.set('chat_id', id);
window.history.replaceState({}, '', u);
setMobileChatView(true);
refreshData();
};
const handleBack = () => {
setMobileChatView(false);
const u = new URL(window.location);
u.searchParams.delete('chat_id');
window.history.replaceState({}, '', u);
};
const selectedThread = threads.find(t => t.id === selectedThreadId);
const handleSendClient = async (text) => {
if (!selectedThread) return;
const isGroup = selectedThread.type === 'group';
const targetMail = isGroup ? 'Groupe' : selectedThread.targetEmail;
const optimisticMsg = {
id: 'temp_' + Date.now(),
panneauId: selectedThread.type === 'support' || selectedThread.type === 'group' ? selectedThread.id : selectedThread.panneauId,
author: myClientData.full_name || myClientData.name || 'Client',
target: targetMail,
detail: text,
isAlert: 0,
resolved: 0,
created_at: new Date().toISOString(),
authorType: 'Client'
};
window.dispatchEvent(new CustomEvent('optimistic_message', { detail: optimisticMsg }));
const payload = {
panneauId: selectedThread.type === 'support' || selectedThread.type === 'group' ? selectedThread.id : selectedThread.panneauId,
detail: text,
author: 'Client',
targetEmail: targetMail,
type: isGroup ? 'group' : 'message'
};
await window.apiFetch('interactions', { body: payload });
};
const forceChatMobile = mobileChatView || !!requestedChatId || threads.length === 1;
const getThreadTitleClass = (type) => {
if (type === 'support') return 'font-black text-emerald-700';
if (type === 'group') return 'font-black text-blue-700';
return 'font-bold text-slate-800';
};
const rightHeaderExtras = selectedThread?.type === 'group' && selectedThread?.panneauData ? (
{selectedThread.panneauData.owner_full_name || selectedThread.panneauData.owner_company || 'Propriétaire'} ({selectedThread.panneauData.owner_company}) - Propriétaire
{selectedThread.panneauData.collaborators?.filter(c => c.status === 'accepted').map((c, i) => (
{c.name || c.company} ({c.company})
))}
) : null;
return (
{selectedThread && ChatBox && (
)}
);
};
// =========================================================================
// 3. ONGLET : FACTURES ET AVOIRS
// =========================================================================
window.ClientInvoicesTab = ({ data, myClientData, setPromptDialog, refreshData, setActiveTab }) => {
const { useState } = React;
const [isSaving, setIsSaving] = useState(false);
const DownloadIcon = window.DownloadIcon || (() => null);
const ZapIcon = window.ZapIcon || (() => null);
const FileTextIcon = window.FileTextIcon || (() => null);
const AlertTriangleIcon = window.AlertTriangleIcon || (() => null);
// Fallback robuste avec restauration de la soumission native
const Button = window.Button || (({children, onClick, className, disabled, title, icon: Icon, type}) => {
const handleAction = (e) => {
if (disabled) return;
if (onClick) onClick(e);
if (type === 'submit') {
const form = e.currentTarget.closest('form');
if (form && typeof form.requestSubmit === 'function') form.requestSubmit();
}
};
return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleAction(e); } }}
className={`flex items-center justify-center gap-2 cursor-pointer transition ${className || ''} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
title={title}
>
{Icon && }
{children}
{type === 'submit' && }
);
});
const handleRefundRequest = (inv) => {
if (!setPromptDialog) return;
setPromptDialog({
title: "Demande d'analyse ou signalement",
type: "info",
message: `Veuillez indiquer le motif de votre demande concernant la facture ${inv.invoiceNumber} d'un montant de ${inv.amount} €. Aucun remboursement n'est jamais accepté sauf après demande explicite et validation stricte par l'administrateur (ex: erreur de facturation prouvée). Un ticket sera ouvert auprès de notre service comptabilité.`,
placeholder: "Motif de votre demande...",
confirmText: "Envoyer la demande",
onConfirm: async (reason) => {
const payload = {
panneauId: `SUPPORT_${myClientData.id}`,
detail: `Demande d'analyse concernant la facture ${inv.invoiceNumber} (${inv.amount} €).\nMotif : ${reason}`,
author: 'Client',
targetEmail: 'Admin',
type: 'billing'
};
const d = await window.apiFetch('interactions', {
body: payload,
setLoading: setIsSaving,
successMessage: "Demande transmise au service comptabilité."
});
if (d) {
setActiveTab('messages');
if (refreshData) refreshData();
}
}
});
};
return (
Factures
Historique comptable et paiements des achats sur eco-panneau.fr.
{myClientData.wallet_balance > 0 && (
Solde disponible (Avoir / Crédit)
Ce montant sera automatiquement déduit de vos prochaines commandes ou de vos factures d'abonnement.
{myClientData.wallet_balance.toFixed(2)} €
)}
Date
Panneau
Détail
Montant
Actions
{data.invoices?.map((inv, i) => (
Date
{window.formatDate ? window.formatDate(inv.created_at) : new Date(String(inv.created_at || '').replace(' ', 'T')).toLocaleDateString('fr-FR')}
Panneau
{inv.panneauName}
Détail
{inv.type}
Montant
{inv.amount} €
))}
{(!data.invoices || data.invoices.length === 0) && (
Aucune facture pour le moment.
)}
);
};
/* EOF ========== [_www/_react/_clients_dashboard_messagerie.jsx] */