/** * ========================================================================= * 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] */