// ECO-PANNEAU.FR - _react/clients/_clients_dashboard.jsx
window.pano_ClientDashboardTab = ({
data, myClientData, setActiveTab, setManagingPanneau, setValidationErrors, setPreviewPanneau, openLocalModal, openLocalDialog, closeCurrentLayer, activeModal, activeDialog, targetId, dialogId, clientOpts, toggleDashboardPin, handleCreateNewPanel, refreshData, setPanelTeamModal, setArchiveConfig
}) => {
const { useState, useEffect } = React;
// ZÉRO-DETTE : Utilisation de notre Hook abstrait !
const { isMounted, safeFetch } = window.pano_useSafeFetch();
const [isSavingOrder, setIsSavingOrder] = useState(false);
// CORRECTION : Nettoyage des icônes inutilisées (Dette technique)
const {
UsersIcon, BuildingIcon, EyeIcon, MessageSquareIcon, MessageCircleIcon,
ArchiveIcon, AlertTriangleIcon, PackageIcon,
PinIcon, ChevronUpIcon, ChevronDownIcon, SlidersHorizontalIcon, PlusIcon,
CheckCircleIcon, XIcon, EditIcon, QrCodeIcon, MailIcon
} = window.pano_getIcons();
// CORRECTION ZÉRO-DETTE : Retrait de UniversalViewer du composant parent
const {
StatCard, Button, IconBadge, NotificationBadge, Modal,
CardGrid, DataCard, TextLogo, StatusBadge,
ClientPanelCard, ClientInviteCard, ClientDeliveryCard
} = window.pano_getComponents();
// 3. - Gestion de l'ordre d'affichage (Reorder)
const defaultOrder = ['actions', 'pinned', 'stats', 'chats'];
const currentOrder = clientOpts?.dashboard_order || defaultOrder;
const currentActionsMode = clientOpts?.actions_mode || 'full';
const [tempOrder, setTempOrder] = useState(currentOrder);
const [tempActionsMode, setTempActionsMode] = useState(currentActionsMode);
useEffect(() => {
setTempOrder(currentOrder);
setTempActionsMode(currentActionsMode);
}, [JSON.stringify(currentOrder), currentActionsMode]);
const blockNames = {
pinned: "Favoris (Épinglés)",
actions: "À traiter en priorité",
stats: "Vue d'ensemble",
chats: "Conversations récentes"
};
const moveBlock = (index, dir) => {
const newOrder = [...tempOrder];
if (dir === 'up' && index > 0) {
[newOrder[index-1], newOrder[index]] = [newOrder[index], newOrder[index-1]];
} else if (dir === 'down' && index < newOrder.length - 1) {
[newOrder[index+1], newOrder[index]] = [newOrder[index], newOrder[index+1]];
}
setTempOrder(newOrder);
};
const saveOrder = async (e) => {
setIsSavingOrder(true);
const newOpts = { ...clientOpts, dashboard_order: tempOrder, actions_mode: tempActionsMode };
const d = await safeFetch('clients/profile/update', {
body: { ...myClientData, uiMode: JSON.stringify(newOpts) }
});
if (!isMounted.current) return; // SÉCURITÉ : Coupe-circuit
setIsSavingOrder(false);
if (d && refreshData) refreshData();
if (closeCurrentLayer) closeCurrentLayer(e);
};
// 4. - Traitement des données (Entonnoir)
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:[] };
const activePendingInvites = pendingInvites;
const activePanelsCount = visiblePanels.filter(p => p.status === 'Actif').length;
const draftPanelsCount = visiblePanels.filter(p => p.status === 'Brouillon').length;
const totalViews = data.stats?.totalViews || 0;
const threads = window.pano_buildClientChatThreads ? window.pano_buildClientChatThreads(data.interactions || [], myClientData, visiblePanels, pendingInvites) : [];
const dashboardThreads = threads.slice(0, 5);
const unreadMap = {};
threads.forEach(t => {
if (t.unread > 0 && t.panneauId) {
if (!unreadMap[t.panneauId]) unreadMap[t.panneauId] = { total: 0, riverain: 0, group: 0 };
unreadMap[t.panneauId].total += t.unread;
if (t.type === 'riverain') unreadMap[t.panneauId].riverain += t.unread;
if (t.type === 'group') unreadMap[t.panneauId].group += t.unread;
}
});
const pinnedPanelsIds = clientOpts?.pinned_panels || [];
const pinnedPanels = visiblePanels.filter(p => pinnedPanelsIds.includes(p.id));
const pinnedDeliveriesIds = clientOpts?.pinned_deliveries || [];
const pinnedDeliveries = visiblePanels.filter(p => pinnedDeliveriesIds.includes(p.id) && p.physicalPanels > 0 && p.status !== 'Brouillon');
const limitShippingForce = parseInt(data.settings?.limit_shipping_force || 90, 10);
// CORRECTION SÉCURITÉ/UX : Intégration D.O.E non lus ET retrait des alertes logistiques (pour éviter le doublon avec ClientDeliveryCard)
const attentionPanels = visiblePanels.filter(p => {
const unreadCount = unreadMap[p.id]?.total || 0;
const isUnreadDOE = p.status === 'DOE' && !p.doe_downloaded;
return p.status === 'Suspendu' || p.status === 'Attente validation' || p.inactivity_alert_level > 0 || unreadCount > 0 || isUnreadDOE;
});
const pendingLogistics = visiblePanels.filter(p => {
if (p.physicalPanels <= 0) return false;
const currentStatus = p.shipping_status || 'En attente de validation';
if (currentStatus === 'Attente validation client') return true;
if (currentStatus === 'Expédié' && p.updated_at) {
const updateTime = new Date(p.updated_at.replace(' ', 'T')).getTime();
const daysSince = (Date.now() - updateTime) / (1000 * 3600 * 24);
return daysSince > limitShippingForce;
}
return false;
});
const isFeatureEnabled = (adminOpt, clientVal) => {
if (adminOpt === 'disabled') return false;
if (adminOpt === 'active') return true;
if (adminOpt === 'optional_on') return clientVal !== false;
if (adminOpt === 'optional_off' || adminOpt === 'optional') return clientVal === true;
return true;
};
const hasNewsletterFeature = isFeatureEnabled(data.settings?.opt_newsletter, clientOpts?.newsletter);
const hasCollabFeature = isFeatureEnabled(data.settings?.opt_collab, clientOpts?.collab);
const hasMessagingFeature = isFeatureEnabled(data.settings?.opt_messaging, clientOpts?.messaging);
const isSuspended = myClientData.paymentStatus === 'suspended';
// 5. - Blocs de rendu dynamiques
const renderPinnedBlock = () => {
if (pinnedPanels.length === 0 && pinnedDeliveries.length === 0) return null;
return (
Vos favoris
{pinnedPanels.map(p => (
toggleDashboardPin('pinned_panels', id, e)}
unreadMap={unreadMap} setPreviewPanneau={setPreviewPanneau} openLocalModal={openLocalModal} openLocalDialog={openLocalDialog}
setManagingPanneau={setManagingPanneau} setValidationErrors={setValidationErrors}
isSuspended={isSuspended} hasNewsletterFeature={hasNewsletterFeature}
hasCollabFeature={hasCollabFeature} hasMessagingFeature={hasMessagingFeature} refreshData={refreshData} closeCurrentLayer={closeCurrentLayer}
setPanelTeamModal={setPanelTeamModal} setArchiveConfig={setArchiveConfig}
activeModal={activeModal} activeDialog={activeDialog} targetId={targetId} dialogId={dialogId}
/>
))}
{/* CORRECTION ZÉRO-DETTE : Distribution des props du routeur */}
{ClientDeliveryCard && pinnedDeliveries.map(c => (
toggleDashboardPin('pinned_deliveries', id, e)}
refreshData={refreshData}
limitShippingForce={limitShippingForce}
openModal={openLocalModal}
openDialog={openLocalDialog}
closeCurrentLayer={closeCurrentLayer}
activeModal={activeModal}
activeDialog={activeDialog}
targetId={targetId}
dialogId={dialogId}
/>
))}
);
};
const renderActionsBlock = () => {
if (attentionPanels.length === 0 && !isSuspended && activePendingInvites.length === 0 && pendingLogistics.length === 0) return null;
const attentionPanelsExtra = attentionPanels.length > 4 ? attentionPanels.length - 4 : 0;
const pendingLogisticsExtra = pendingLogistics.length > 4 ? pendingLogistics.length - 4 : 0;
return (
À traiter en priorité
{isSuspended && (
Compte suspendu (Impayé)
Veuillez régulariser votre situation pour réactiver vos panneaux.
)}
{(activePendingInvites.length > 0 || attentionPanels.length > 0 || pendingLogistics.length > 0) && (
{currentActionsMode === 'full' ? (
<>
{activePendingInvites.map(p => (
))}
{attentionPanels.slice(0, 4).map(p => (
toggleDashboardPin('pinned_panels', id, e)}
unreadMap={unreadMap} setPreviewPanneau={setPreviewPanneau} openLocalModal={openLocalModal} openLocalDialog={openLocalDialog}
setManagingPanneau={setManagingPanneau} setValidationErrors={setValidationErrors}
isSuspended={isSuspended} hasNewsletterFeature={hasNewsletterFeature}
hasCollabFeature={hasCollabFeature} hasMessagingFeature={hasMessagingFeature} refreshData={refreshData} closeCurrentLayer={closeCurrentLayer}
setPanelTeamModal={setPanelTeamModal} setArchiveConfig={setArchiveConfig}
activeModal={activeModal} activeDialog={activeDialog} targetId={targetId} dialogId={dialogId}
/>
))}
{attentionPanelsExtra > 0 && (
setActiveTab('panels')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">
{window.pano_formatPlural(attentionPanelsExtra, `+ ${attentionPanelsExtra} autre panneau`, `+ ${attentionPanelsExtra} autres panneaux`)}
)}
{ClientDeliveryCard && pendingLogistics.slice(0, 4).map(p => (
toggleDashboardPin('pinned_deliveries', id, e)}
refreshData={refreshData}
limitShippingForce={limitShippingForce}
openModal={openLocalModal}
openDialog={openLocalDialog}
closeCurrentLayer={closeCurrentLayer}
activeModal={activeModal}
activeDialog={activeDialog}
targetId={targetId}
dialogId={dialogId}
/>
))}
{pendingLogisticsExtra > 0 && (
setActiveTab('livraisons')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">
{window.pano_formatPlural(pendingLogisticsExtra, `+ ${pendingLogisticsExtra} autre commande`, `+ ${pendingLogisticsExtra} autres commandes`)}
)}
>
) : (
<>
{activePendingInvites.map(p => (
setActiveTab('panels')}
refreshData={refreshData}
hasCollabFeature={hasCollabFeature}
openModal={openLocalModal}
openDialog={openLocalDialog}
closeCurrentLayer={closeCurrentLayer}
activeModal={activeModal}
activeDialog={activeDialog}
targetId={targetId}
dialogId={dialogId}
/>
))}
{attentionPanels.slice(0, 4).map(p => {
let badgeVariant = 'warning';
if (p.status === 'Suspendu' || p.status === 'Attente validation') badgeVariant = 'danger';
const isUnreadDOE = p.status === 'DOE' && !p.doe_downloaded;
if (isUnreadDOE) badgeVariant = 'warning';
return (
setActiveTab('panels')}
className="cursor-pointer group hover:scale-[1.02] transition-transform min-w-0"
>
{p.name}
{isUnreadDOE ? 'Télécharger D.O.E' : 'Action requise'}
{!isUnreadDOE && }
);
})}
{attentionPanelsExtra > 0 && (
setActiveTab('panels')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">
{window.pano_formatPlural(attentionPanelsExtra, `+ ${attentionPanelsExtra} autre panneau`, `+ ${attentionPanelsExtra} autres panneaux`)}
)}
{pendingLogistics.slice(0, 4).map(p => {
const currentStatus = p.shipping_status || 'En attente de validation';
const isRefused = currentStatus === 'Maquette refusée';
const isClientValidation = currentStatus === 'Attente validation client';
const variant = isRefused ? 'danger' : (isClientValidation ? 'warning' : 'warning');
return (
setActiveTab('livraisons')} className="cursor-pointer group hover:scale-[1.02] transition-transform min-w-0">
{p.name}
Logistique: {currentStatus === 'Expédié' ? `Non réceptionné (> ${limitShippingForce}j)` : currentStatus}
);
})}
{pendingLogisticsExtra > 0 && (
setActiveTab('livraisons')} className="cursor-pointer group flex items-center justify-center hover:scale-[1.02] transition-transform min-w-0">
{window.pano_formatPlural(pendingLogisticsExtra, `+ ${pendingLogisticsExtra} autre commande`, `+ ${pendingLogisticsExtra} autres commandes`)}
)}
>
)}
)}
);
};
const renderStatsBlock = () => (
Vue d'ensemble
} variant="success" onClick={() => setActiveTab('panels')} />
} variant="secondary" onClick={() => setActiveTab('panels')} />
} variant="info" />
);
const renderChatsBlock = () => {
return (
Conversations récentes
setActiveTab('messages')} className="text-xs font-bold text-emerald-600 hover:underline cursor-pointer">Voir tout
{dashboardThreads.map(t => {
const lastMsg = t.messages.length > 0 ? t.messages[t.messages.length - 1] : null;
const IconToUse = t.type === 'support' ? MessageSquareIcon : MessageCircleIcon;
return (
{
e.stopPropagation();
if(openLocalModal) openLocalModal('chat', t.id, false);
}}
className="cursor-pointer hover:border-emerald-300 transition group p-4"
>
{IconBadge &&
}
{t.title}
{t.targetEmail}
0 ? 'text-slate-800 font-bold' : 'text-slate-500'}`}>
{lastMsg ? lastMsg.detail.replace(/\[ATTACHMENT:[^\]]+\]/g, '[Pièce jointe]').replace(/<[^>]*>?/gm, '') : Nouvelle conversation...}
);
})}
{dashboardThreads.length === 0 && (
Aucune interaction récente.
)}
);
};
const blocksMap = {
pinned: renderPinnedBlock(),
actions: renderActionsBlock(),
stats: renderStatsBlock(),
chats: renderChatsBlock()
};
const demoPanneau = allPanels.find(p => p.id === 'demo-panneau') || { id: 'demo-panneau', status: 'Actif', offerType: 'demo', name: 'Panneau de démonstration', location: 'Paris', themeColor: '#059669', hasNoAds: false };
const demoHasDraft = demoPanneau.draft_data && Object.keys(demoPanneau.draft_data).length > 0;
return (
<>
Tableau de bord
Bienvenue sur votre espace personnel .
{visiblePanels.some(p => p.id === 'demo-panneau') && (
Panneau de démonstration
{demoHasDraft && Modifié}
Modifiez ce panneau pour mettre à jour la page d'accueil.
)}
{currentOrder.map(blockKey => {
const blockContent = blocksMap[blockKey];
if (!blockContent) return null;
if (blockKey === 'chats') {
return
{blockContent}
;
}
return (
{blockContent}
);
})}
{/* MODALE DE RÉORGANISATION GLOBALE */}
{activeDialog === 'reorder_dashboard' && Modal && (
(
<>
Annuler
Enregistrer l'ordre
>
)}
>
Utilisez les flèches pour réorganiser les grandes sections de votre tableau de bord selon vos priorités.
{tempOrder.map((blockKey, idx) => (
{blockNames[blockKey]}
moveBlock(idx, 'up')} disabled={idx === 0 || isSavingOrder} className="p-1.5 shadow-none bg-white text-slate-500 hover:text-blue-600" title="Monter" />
moveBlock(idx, 'down')} disabled={idx === tempOrder.length - 1 || isSavingOrder} className="p-1.5 shadow-none bg-white text-slate-500 hover:text-blue-600" title="Descendre" />
))}
)}
>
);
};
/* EOF ========== [_react/clients/_clients_dashboard.jsx] */