Files
btpxpress-frontend/app/(main)/dashboard/page-broken.tsx

1068 lines
51 KiB
TypeScript
Executable File

'use client';
import React, { useState, useRef, useEffect, useContext } from 'react';
// Imports Atlantis React - Collection complète des composants avancés
import { Button } from 'primereact/button';
import { Chart } from 'primereact/chart';
import { Card } from 'primereact/card';
import { ProgressBar } from 'primereact/progressbar';
import { Badge } from 'primereact/badge';
import { Tag } from 'primereact/tag';
import { Knob } from 'primereact/knob';
import { Column } from 'primereact/column';
import { DataTable } from 'primereact/datatable';
import { Dropdown } from 'primereact/dropdown';
import { Message } from 'primereact/message';
import { Avatar } from 'primereact/avatar';
import { AvatarGroup } from 'primereact/avatargroup';
import { Divider } from 'primereact/divider';
import { Chip } from 'primereact/chip';
import { OverlayPanel } from 'primereact/overlaypanel';
import { Toast } from 'primereact/toast';
import { ConfirmDialog } from 'primereact/confirmdialog';
import { Dialog } from 'primereact/dialog';
import { SplitButton } from 'primereact/splitbutton';
import { Terminal } from 'primereact/terminal';
import { Inplace, InplaceDisplay, InplaceContent } from 'primereact/inplace';
import { Rating } from 'primereact/rating';
import { ColorPicker } from 'primereact/colorpicker';
import { LayoutContext } from '../../../layout/context/layoutcontext';
import { ChartData, ChartOptions } from 'chart.js';
import { useDashboard, ChantierActif } from '../../../hooks/useDashboard';
import { ProgressSpinner } from 'primereact/progressspinner';
import { Panel } from 'primereact/panel';
import { Skeleton } from 'primereact/skeleton';
import { Ripple } from 'primereact/ripple';
import StatsCard from '../../../components/dashboard/StatsCard';
// Interface ChantierActif importée depuis useDashboard
interface KPIData {
chantiersActifs: number;
chantiersEnRetard: number;
caRealise: number;
caObjectif: number;
margeGlobale: number;
effectifsSurSite: number;
satisfactionClient: number;
}
const DashboardBTP = () => {
const { layoutConfig } = useContext(LayoutContext);
const [periode, setPeriode] = useState('mois');
// executive, operational, analytical
const [fullScreenChart, setFullScreenChart] = useState(false);
const [selectedMetric, setSelectedMetric] = useState('ca');
const [autoRefresh, setAutoRefresh] = useState(true);
const [refreshInterval, setRefreshInterval] = useState(30);
const toast = useRef<Toast>(null);
const overlayPanel = useRef<OverlayPanel>(null);
const terminalRef = useRef<Terminal>(null);
// Hook pour les données du dashboard
const {
metrics,
chantiersActifs,
activitesRecentes,
tachesUrgentes,
loading,
error,
refresh,
changePeriode
} = useDashboard(periode as 'semaine' | 'mois' | 'trimestre' | 'annee');
// Calculs réels basés sur les données backend
const calculerIndicateursPerformance = () => {
if (!chantiersActifs || chantiersActifs.length === 0) {
return [
{ label: 'Chantiers à l\'heure', value: 0, color: '#10b981' },
{ label: 'Chantiers en retard', value: 0, color: '#ef4444' }
];
}
const totalChantiers = chantiersActifs.length;
const chantiersEnRetard = chantiersActifs.filter(c => c.statut === 'EN_RETARD').length;
const chantiersALHeure = totalChantiers - chantiersEnRetard;
const tauxALHeure = Math.round((chantiersALHeure / totalChantiers) * 100);
const tauxRetard = Math.round((chantiersEnRetard / totalChantiers) * 100);
return [
{ label: 'Chantiers à l\'heure', value: tauxALHeure, color: '#10b981' },
{ label: 'Chantiers en retard', value: tauxRetard, color: '#ef4444' }
];
};
const performanceMetersCalculated = calculerIndicateursPerformance();
// Auto-refresh effect
useEffect(() => {
let interval: NodeJS.Timeout;
if (autoRefresh) {
interval = setInterval(() => {
refresh();
}, refreshInterval * 1000);
}
return () => {
if (interval) clearInterval(interval);
};
}, [autoRefresh, refreshInterval, refresh]);
// Fonctions utilitaires avancées
const exportDashboard = () => {
toast.current?.show({
severity: 'info',
summary: 'Export en cours',
detail: 'Génération du rapport dashboard complet...',
life: 3000
});
};
const openSupportDialog = () => {
toast.current?.show({
severity: 'info',
summary: 'Support Technique',
detail: 'Redirection vers le centre d\'aide...',
life: 3000
});
};
const toggleFullScreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
};
const exportToPDF = () => {
toast.current?.show({
severity: 'success',
summary: 'Export PDF',
detail: 'Génération du rapport PDF en cours...',
life: 3000
});
};
const exportToExcel = () => {
toast.current?.show({
severity: 'success',
summary: 'Export Excel',
detail: 'Téléchargement du fichier Excel...',
life: 3000
});
};
const syncToCloud = () => {
toast.current?.show({
severity: 'info',
summary: 'Synchronisation',
detail: 'Sauvegarde cloud en cours...',
life: 3000
});
};
const openDeveloperTools = () => {
if (terminalRef.current) {
toast.current?.show({
severity: 'info',
summary: 'Mode Développeur',
detail: 'Console de développement activée',
life: 3000
});
}
};
const handlePeriodeChange = (value: string) => {
setPeriode(value);
changePeriode(value as 'semaine' | 'mois' | 'trimestre' | 'annee');
};
// Affichage de chargement amélioré
if (loading && !metrics) {
return (
<div className="min-h-screen surface-ground flex align-items-center justify-content-center">
<Card className="shadow-3 border-round p-6 text-center" style={{maxWidth: '400px'}}>
<div className="mb-4">
<ProgressSpinner style={{width: '60px', height: '60px'}} strokeWidth="3" />
</div>
<h4 className="text-900 font-semibold mb-2">Chargement du tableau de bord</h4>
<p className="text-600 mb-4">Récupération des données en temps réel...</p>
<ProgressBar mode="indeterminate" style={{height: '6px'}} />
</Card>
</div>
);
}
// Affichage d'erreur sophistiqué
if (error) {
return (
<div className="min-h-screen surface-ground flex align-items-center justify-content-center">
<Panel header="Erreur Système" className="shadow-4 border-round" style={{maxWidth: '600px'}}>
<div className="text-center">
<Avatar
icon="pi pi-exclamation-triangle"
size="xlarge"
shape="circle"
className="mb-4 bg-red-100 text-red-500"
/>
<h3 className="text-900 font-bold mb-3">Impossible de charger les données</h3>
<p className="text-600 mb-4 line-height-3">{error}</p>
<Message severity="error" text="Vérifiez votre connexion réseau et les services backend" className="mb-4" />
<div className="flex gap-2 justify-content-center">
<Button
label="Réessayer"
icon="pi pi-refresh"
onClick={refresh}
className="p-button-primary"
/>
<Button
label="Diagnostic"
icon="pi pi-search"
className="p-button-outlined"
onClick={() => window.location.href = '/diagnostics'}
/>
<Button
label="Support Technique"
icon="pi pi-headphones"
className="p-button-text"
onClick={openSupportDialog}
/>
</div>
</div>
</Panel>
</div>
);
}
// Calculs KPIs étendus avec données réelles backend
const chiffreAffaires = metrics?.chiffreAffaires || 0;
const coutReel = metrics?.coutReel || 0;
const objectifCA = metrics?.objectifCA || 0;
const kpis: KPIData = {
chantiersActifs: metrics?.chantiersActifs || 0,
chantiersEnRetard: metrics?.chantiersEnRetard || 0,
caRealise: chiffreAffaires,
caObjectif: objectifCA,
margeGlobale: chiffreAffaires > 0 ? ((chiffreAffaires - coutReel) / chiffreAffaires * 100) : 0,
effectifsSurSite: metrics?.totalEquipes || 0,
satisfactionClient: metrics?.satisfactionClient || 0
};
const chantiersData = chantiersActifs || [];
const periodeOptions = [
{ label: 'Cette semaine', value: 'semaine', icon: 'pi pi-calendar' },
{ label: 'Ce mois', value: 'mois', icon: 'pi pi-calendar' },
{ label: 'Ce trimestre', value: 'trimestre', icon: 'pi pi-calendar' },
{ label: 'Cette année', value: 'annee', icon: 'pi pi-calendar' }
];
// Données graphiques avancées
const getAdvancedChartData = (): ChartData => {
const documentStyle = typeof window !== 'undefined' ?
getComputedStyle(document.documentElement) : null;
const months = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
return {
labels: months,
datasets: [
{
label: 'CA Réalisé',
data: new Array(12).fill(0), // Données réelles du backend
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 3,
fill: true,
tension: 0.4,
type: 'line' as const
},
{
label: 'Objectif',
data: new Array(12).fill(0), // Données réelles du backend
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 2,
borderDash: [5, 5],
fill: false,
type: 'line' as const
},
{
label: 'Marge',
data: new Array(12).fill(0), // Données réelles du backend
backgroundColor: 'rgba(75, 192, 192, 0.6)',
borderColor: 'rgba(75, 192, 192, 1)',
type: 'bar' as const
}
]
};
};
const chartOptions: ChartOptions = {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index' as const,
intersect: false,
},
plugins: {
legend: {
position: 'top' as const,
labels: {
usePointStyle: true,
padding: 20,
font: {
family: 'Inter, sans-serif',
size: 13,
weight: '500'
}
}
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: 'white',
bodyColor: 'white',
borderColor: 'rgba(255, 255, 255, 0.2)',
borderWidth: 1
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(160, 167, 181, 0.3)'
},
ticks: {
font: {
family: 'Inter, sans-serif'
}
}
},
x: {
grid: {
color: 'rgba(160, 167, 181, 0.3)'
},
ticks: {
font: {
family: 'Inter, sans-serif'
}
}
}
}
};
// Templates de données avancés
const advancedAvancementTemplate = (rowData: ChantierActif) => {
const getProgressColor = (value: number) => {
if (value >= 80) return 'success';
if (value >= 50) return 'info';
if (value >= 25) return 'warning';
return 'danger';
};
return (
<div>
<div className="flex align-items-center gap-2 mb-2">
<ProgressBar
value={rowData.avancement}
className="flex-1"
style={{ height: '8px' }}
color={getProgressColor(rowData.avancement) === 'success' ? '#10b981' :
getProgressColor(rowData.avancement) === 'info' ? '#3b82f6' :
getProgressColor(rowData.avancement) === 'warning' ? '#f59e0b' : '#ef4444'}
/>
<Badge
value={`${rowData.avancement}%`}
severity={getProgressColor(rowData.avancement)}
className="font-semibold"
/>
</div>
<div className="flex align-items-center gap-1">
<Rating
value={Math.floor(rowData.avancement / 20)}
readOnly
cancel={false}
className="text-xs"
/>
</div>
</div>
);
};
const advancedChantierTemplate = (rowData: ChantierActif) => {
return (
<div className="flex align-items-center gap-2">
<div className="flex align-items-center justify-content-center w-2rem h-2rem bg-primary text-white border-round flex-shrink-0">
<i className="pi pi-building text-sm"></i>
</div>
<div>
<div className="font-semibold text-900">{rowData.nom}</div>
<small className="text-500">{typeof rowData.client === 'string' ? rowData.client : rowData.client?.nom || 'Non spécifié'}</small>
</div>
</div>
);
};
const advancedClientTemplate = (rowData: ChantierActif) => {
const clientName = typeof rowData.client === 'string' ? rowData.client : rowData.client?.nom || 'Client non défini';
return (
<div className="flex align-items-center gap-2">
<Avatar
label={clientName.charAt(0)}
size="normal"
shape="circle"
className="bg-blue-100 text-blue-600"
/>
<div>
<div className="font-medium text-900">{clientName}</div>
</div>
</div>
);
};
const advancedStatutTemplate = (rowData: ChantierActif) => {
const getStatutConfig = (statut: string) => {
switch (statut) {
case 'EN_COURS':
return { severity: 'success', icon: 'pi pi-play', label: 'En cours' };
case 'EN_RETARD':
return { severity: 'danger', icon: 'pi pi-exclamation-triangle', label: 'En retard' };
default:
return { severity: 'info', icon: 'pi pi-clock', label: 'Planifié' };
}
};
const config = getStatutConfig(rowData.statut);
return (
<div className="flex align-items-center gap-2">
<Tag
value={config.label}
severity={config.severity as any}
icon={config.icon}
className="font-semibold"
/>
</div>
);
};
const advancedEquipeTemplate = (rowData: ChantierActif) => {
if (!rowData.equipe) return <span className="text-500">Non assignée</span>;
return (
<div className="flex align-items-center gap-2">
<AvatarGroup>
{Array.from({ length: Math.min(4, rowData.equipe.nombreMembres) }).map((_, index) => (
<Avatar
key={index}
label={String.fromCharCode(65 + index)}
size="normal"
shape="circle"
className="bg-primary text-white"
style={{
backgroundColor: `hsl(${(index * 60) % 360}, 70%, 50%)`,
border: '2px solid white'
}}
/>
))}
{rowData.equipe.nombreMembres > 4 && (
<Avatar
label={`+${rowData.equipe.nombreMembres - 4}`}
size="normal"
shape="circle"
className="bg-gray-400 text-white"
/>
)}
</AvatarGroup>
<div>
<div className="font-semibold text-sm">{rowData.equipe.nom}</div>
<div className="text-xs text-500 flex align-items-center gap-1">
<i className="pi pi-users"></i>
{rowData.equipe.nombreMembres} personnes
</div>
</div>
</div>
);
};
const advancedProgressTemplate = (rowData: ChantierActif) => {
try {
const progression = rowData?.avancement || 0;
return (
<div className="text-center">
<span className="font-semibold">{progression}%</span>
</div>
);
} catch (error) {
console.error('Erreur template progression:', error, rowData);
return <span className="text-muted">N/A</span>;
}
};
const advancedBudgetTemplate = (rowData: ChantierActif) => {
try {
if (!rowData?.budget) {
return <span className="text-muted">N/A</span>;
}
return (
<div className="text-right">
<div className="font-bold text-sm">
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
notation: 'compact'
}).format(rowData.budget)}
</div>
</div>
);
} catch (error) {
console.error('Erreur template budget:', error, rowData);
return <span className="text-muted">Erreur</span>;
}
};
// Timeline événements avec style ultra-moderne
const evenementsRecents = (activitesRecentes || []).map(activite => ({
status: activite.titre,
date: new Date(activite.date).toLocaleDateString('fr-FR'),
time: new Date(activite.date).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
icon: getIconForType(activite.type),
color: getColorForStatut(activite.statut),
description: activite.description,
user: activite.utilisateur,
category: activite.type
}));
function getIconForType(type: string) {
const icons: Record<string, string> = {
'CHANTIER': 'pi pi-building',
'MAINTENANCE': 'pi pi-wrench',
'DOCUMENT': 'pi pi-file-pdf',
'EQUIPE': 'pi pi-users',
'FINANCE': 'pi pi-euro',
'ALERTE': 'pi pi-bell'
};
return icons[type] || 'pi pi-circle';
}
function getColorForStatut(statut: string) {
const colors: Record<string, string> = {
'SUCCESS': '#10b981',
'WARNING': '#f59e0b',
'ERROR': '#ef4444',
'INFO': '#3b82f6'
};
return colors[statut] || '#6b7280';
}
// Template du header optimisé et compact
const headerTemplate = () => (
<div className="surface-0 shadow-1 px-4 py-3 mb-4 border-round">
<div className="flex align-items-center justify-content-between">
<div className="flex align-items-center gap-3">
<div className="flex align-items-center justify-content-center w-3rem h-3rem bg-primary border-round">
<i className="pi pi-chart-line text-xl text-white"></i>
</div>
<div>
<div className="text-900 font-semibold text-xl mb-1">Dashboard BTP Xpress</div>
<div className="text-600 text-sm">Pilotage intelligent et temps réel</div>
</div>
</div>
<div className="flex align-items-center gap-2">
<Dropdown
value={periode}
options={periodeOptions}
onChange={(e) => handlePeriodeChange(e.value)}
className="w-9rem"
panelClassName="border-round shadow-4"
/>
<Button
icon="pi pi-refresh"
className="p-button-rounded p-button-outlined"
tooltip="Actualiser maintenant"
onClick={refresh}
loading={loading}
/>
</div>
</div>
</div>
);
return (
<div
className="min-h-screen surface-ground"
>
{/* Composants de dialogue et overlay */}
<Toast ref={toast} />
<ConfirmDialog />
{/* Dialog plein écran pour graphiques */}
<Dialog
visible={fullScreenChart}
onHide={() => setFullScreenChart(false)}
maximizable
style={{ width: '95vw', height: '90vh' }}
header="Analyse Graphique Détaillée"
>
<Chart
type="line"
data={getAdvancedChartData()}
options={chartOptions}
style={{ height: '70vh' }}
/>
</Dialog>
{/* Header style Atlantis */}
{headerTemplate()}
{/* Alertes avancées avec Actions */}
{kpis.chantiersEnRetard > 0 && (
<div className="px-4 md:px-6 lg:px-8 mb-6">
<div className="max-w-screen-xl mx-auto">
<Message
severity="warn"
className="w-full border-round shadow-3 border-l-4 border-orange-500"
content={
<div className="flex align-items-center justify-content-between w-full">
<div className="flex align-items-center gap-3">
<Avatar
icon="pi pi-exclamation-triangle"
className="bg-orange-100 text-orange-600"
size="large"
/>
<div>
<div className="font-bold text-lg text-900">
{kpis.chantiersEnRetard} chantier(s) nécessitent une attention
</div>
<div className="text-sm text-700 mt-1 flex align-items-center gap-2">
<i className="pi pi-clock"></i>
Action immédiate requise pour respecter les délais contractuels
</div>
</div>
</div>
<div className="flex gap-2">
<Button
label="Voir détails"
icon="pi pi-search"
className="p-button-warning p-button-outlined"
size="small"
onClick={() => window.location.href = '/chantiers?filter=en_retard'}
/>
<Button
label="Plan d'action"
icon="pi pi-cog"
className="p-button-warning"
size="small"
onClick={() => toast.current?.show({
severity: 'info',
summary: 'Plan d\'action',
detail: 'Génération du plan de rattrapage...',
life: 3000
})}
/>
</div>
</div>
}
/>
</div>
</div>
)}
{/* KPIs Section ultra-moderne */}
<div className="px-4 md:px-6 lg:px-8 mb-6">
<div className="max-w-screen-xl mx-auto">
<div className="grid">
{/* KPI Chantiers Actifs - Version Premium */}
<div className="col-12 lg:col-3 md:col-6">
<Card className="shadow-4 border-round overflow-hidden h-full hover:shadow-6 transition-all transition-duration-300">
<div className="relative">
<div className="absolute top-0 right-0 bg-blue-500 text-white px-2 py-1 border-round-bl text-xs font-bold">
LIVE
</div>
<div className="flex justify-content-between align-items-start mb-3">
<div className="flex-1">
<div className="text-600 font-medium mb-2 flex align-items-center gap-2">
<i className="pi pi-building text-blue-500"></i>
Chantiers Actifs
</div>
<div className="text-4xl font-bold text-900 mb-2">
{loading ? <Skeleton width="4rem" height="3rem" /> : (
<Inplace closable>
<InplaceDisplay>
{kpis.chantiersActifs}
</InplaceDisplay>
<InplaceContent>
<span className="text-2xl">Détail: {kpis.chantiersActifs} projets</span>
</InplaceContent>
</Inplace>
)}
</div>
<div className="flex align-items-center gap-2 mb-3">
<Chip
label={`${kpis.chantiersEnRetard} en retard`}
className={`text-xs font-bold ${kpis.chantiersEnRetard > 0 ? 'p-chip-danger' : 'p-chip-success'}`}
/>
{kpis.chantiersEnRetard > 0 && (
<Badge value="!" severity="danger" className="animate-pulse" />
)}
</div>
</div>
<div className="bg-blue-100 text-blue-500 border-round p-3 relative">
<i className="pi pi-building text-2xl"></i>
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 border-round animate-pulse"></div>
</div>
</div>
<Divider className="my-3" />
<div className="flex align-items-center justify-content-end">
<Button
icon="pi pi-arrow-right"
className="p-button-text p-button-sm p-button-rounded"
onClick={() => window.location.href = '/chantiers'}
tooltip="Voir tous les chantiers"
/>
</div>
</div>
</Card>
</div>
{/* KPI CA Réalisé - Version Ultra */}
<div className="col-12 lg:col-3 md:col-6">
<Card className="shadow-4 border-round overflow-hidden h-full hover:shadow-6 transition-all transition-duration-300">
<div className="text-center relative">
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-green-400 to-blue-500"></div>
<div className="text-600 font-medium mb-3 mt-2 flex align-items-center justify-content-center gap-2">
<i className="pi pi-euro text-green-500"></i>
CA Réalisé / Objectif
<OverlayPanel ref={overlayPanel}>
<div className="p-3">
<h6 className="font-bold mb-2">Détails du CA</h6>
<p className="m-0 text-sm line-height-3">
Chiffre d'affaires calculé en temps réel basé sur les facturations et acomptes reçus.
</p>
</div>
</OverlayPanel>
<Button
icon="pi pi-info-circle"
className="p-button-text p-button-sm p-button-rounded"
onClick={(e) => overlayPanel.current?.toggle(e)}
/>
</div>
{loading ? (
<Skeleton width="8rem" height="8rem" borderRadius="50%" className="mx-auto mb-4" />
) : (
<div className="flex justify-content-center mb-4">
<Knob
value={kpis.caObjectif > 0 ? Math.min(100, (kpis.caRealise / kpis.caObjectif) * 100) : 0}
size={120}
strokeWidth={8}
valueColor="#10b981"
rangeColor="#e5e7eb"
textColor="#374151"
valueTemplate="{value}%"
className="shadow-2"
/>
</div>
)}
<div className="text-center mb-4">
<div className="text-2xl font-bold text-900 mb-2">
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
notation: 'compact'
}).format(kpis.caRealise)}
</div>
<div className="text-600 text-base">
sur {new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
notation: 'compact'
}).format(kpis.caObjectif || 0)}
</div>
</div>
<Divider />
<div className="w-full">
<h6 className="text-900 mb-3 text-center">Indicateurs de Performance</h6>
{performanceMetersCalculated.map((meter, index) => (
<div key={index} className="mb-3 p-2 surface-50 border-round">
<div className="flex justify-content-between mb-2">
<span className="text-sm font-medium text-900">{meter.label}</span>
<span className="text-sm font-bold" style={{ color: meter.color }}>{meter.value}%</span>
</div>
<ProgressBar
value={meter.value}
className="h-1rem"
style={{ '--p-progressbar-value-bg': meter.color } as any}
/>
</div>
))}
</div>
</div>
</Card>
</div>
{/* KPI Marge - Version Analytique */}
<div className="col-12 lg:col-3 md:col-6">
<Card className="shadow-4 border-round overflow-hidden h-full hover:shadow-6 transition-all transition-duration-300">
<div className="text-center relative">
<div className="absolute top-0 right-0">
<ColorPicker
value="ff6900"
onChange={() => {}}
style={{width: '20px', height: '20px'}}
className="border-round opacity-50"
/>
</div>
<div className="text-600 font-medium mb-3 flex align-items-center justify-content-center gap-2">
<i className="pi pi-percentage text-orange-500"></i>
Marge Globale
</div>
{loading ? (
<Skeleton width="6rem" height="6rem" borderRadius="50%" className="mx-auto mb-3" />
) : (
<div className="flex justify-content-center mb-3">
<Knob
value={Math.max(0, Math.min(100, Number.isFinite(kpis.margeGlobale) ? kpis.margeGlobale : 0))}
size={100}
strokeWidth={12}
valueColor="var(--orange-500)"
rangeColor="var(--surface-300)"
textColor="var(--text-color)"
valueTemplate="{value}%"
className="hover:scale-105 transition-transform transition-duration-200"
/>
</div>
)}
<div className="text-xl font-bold text-900 mb-1">
{Number.isFinite(kpis.margeGlobale) ? kpis.margeGlobale.toFixed(1) : '0.0'}%
</div>
<div className="text-600 text-sm mb-3">de rentabilité</div>
<div className="flex align-items-center justify-content-center gap-2">
<Rating
value={Math.floor((kpis.margeGlobale || 0) / 20)}
readOnly
cancel={false}
className="text-sm"
/>
</div>
</div>
</Card>
</div>
{/* KPI Équipes - Version Interactive */}
<div className="col-12 lg:col-3 md:col-6">
<Card className="shadow-4 border-round overflow-hidden h-full hover:shadow-6 transition-all transition-duration-300">
<div className="relative">
<div className="flex justify-content-between align-items-start mb-3">
<div className="flex-1">
<div className="text-600 font-medium mb-2 flex align-items-center gap-2">
<i className="pi pi-users text-purple-500"></i>
Équipes Actives
</div>
<div className="text-4xl font-bold text-900 mb-2">
{loading ? <Skeleton width="4rem" height="3rem" /> : (
<span className="relative">
{kpis.effectifsSurSite}
<Badge
value={Math.max(0, kpis.effectifsSurSite - 2)}
severity="info"
className="absolute -top-2 -right-4 text-xs"
/>
</span>
)}
</div>
<div className="text-600 text-sm mb-3">
{kpis.effectifsSurSite > 0 ? 'équipes déployées' : 'Aucune équipe active'}
</div>
</div>
<div className="bg-purple-100 text-purple-500 border-round p-3">
<i className="pi pi-users text-2xl"></i>
</div>
</div>
<Divider className="my-3" />
<div className="flex align-items-center justify-content-between">
<div className="text-600 text-sm">
{kpis.effectifsSurSite > 0 ? `${kpis.effectifsSurSite} employés actifs` : 'Aucun employé actif'}
</div>
<Button
icon="pi pi-arrow-right"
className="p-button-text p-button-sm p-button-rounded"
onClick={() => window.location.href = '/employes'}
tooltip="Voir toutes les équipes"
/>
</div>
</div>
</Card>
</div>
</div>
</div>
</div>
{/* Section principale avec layout optimisé */}
<div className="px-4 md:px-6 lg:px-8 mb-6">
<div className="grid">
{/* Graphique principal */}
<div className="col-12 lg:col-8">
<Card className="h-full shadow-3">
<div className="flex align-items-center justify-content-between mb-4">
<div className="flex align-items-center gap-2">
<i className="pi pi-chart-line text-primary text-xl"></i>
<h3 className="m-0 text-xl font-semibold">Performance Financière</h3>
</div>
<Button
icon="pi pi-expand"
className="p-button-text p-button-sm"
tooltip="Plein écran"
onClick={() => setFullScreenChart(true)}
/>
</div>
<div className="relative">
<Chart
type="line"
data={getAdvancedChartData()}
options={chartOptions}
style={{ height: '400px' }}
/>
{loading && (
<div className="absolute inset-0 flex align-items-center justify-content-center bg-white bg-opacity-80">
<ProgressSpinner style={{width: '40px', height: '40px'}} />
</div>
)}
</div>
</Card>
</div>
{/* Activités en Direct - Layout compact */}
<div className="col-12 lg:col-4">
<Card className="h-full shadow-3">
<div className="flex align-items-center gap-2 mb-4">
<i className="pi pi-bell text-orange-500 text-xl"></i>
<h3 className="m-0 text-xl font-semibold">Activités Récentes</h3>
</div>
<div style={{ height: '400px', overflowY: 'auto' }}>
{evenementsRecents.length > 0 ? (
<div className="flex flex-column gap-3">
{evenementsRecents.slice(0, 6).map((item, index) => (
<div key={index} className="flex gap-3 p-2 border-round hover:surface-hover transition-colors transition-duration-200">
<div
className="flex align-items-center justify-content-center w-2rem h-2rem border-round flex-shrink-0"
style={{ backgroundColor: item.color }}
>
<i className={item.icon + " text-white text-sm"}></i>
</div>
<div className="flex-1 min-w-0">
<div className="flex align-items-center justify-content-between mb-1">
<span className="font-semibold text-sm text-900 truncate">{item.status}</span>
<small className="text-500 text-xs flex-shrink-0 ml-2">{item.time}</small>
</div>
<p className="text-600 text-xs m-0 mb-1 line-height-3">{item.description}</p>
<div className="flex align-items-center gap-2">
<Badge value={item.category} severity="info" className="text-xs" />
<small className="text-500 text-xs">{item.user || 'Système'}</small>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center p-4">
<i className="pi pi-clock text-3xl text-300 mb-2"></i>
<div className="text-500 text-sm">Aucune activité récente</div>
</div>
)}
</div>
</Card>
</div>
</div>
</div>
{/* Section tableau des chantiers */}
<div className="px-4 md:px-6 lg:px-8 mb-6">
<Card className="shadow-3">
<div className="flex align-items-center gap-2 mb-4">
<i className="pi pi-building text-primary text-xl"></i>
<h3 className="m-0 text-xl font-semibold">Chantiers Actifs</h3>
</div>
<DataTable
value={chantiersActifs || []}
paginator
rows={8}
className="p-datatable-sm"
emptyMessage="Aucun chantier actif"
loading={loading}
size="normal"
>
<Column
field="nom"
header="Chantier"
body={advancedChantierTemplate}
sortable
/>
<Column
field="client.nom"
header="Client"
body={advancedClientTemplate}
sortable
/>
<Column
field="statut"
header="Statut"
body={advancedStatutTemplate}
sortable
/>
<Column
field="avancement"
header="Progression"
body={advancedProgressTemplate}
sortable
/>
<Column
field="budget"
header="Budget"
body={advancedBudgetTemplate}
sortable
/>
<Column
body={(rowData) => (
<Button
icon="pi pi-eye"
className="p-button-rounded p-button-outlined p-button-sm"
tooltip="Voir"
onClick={() => window.location.href = `/chantiers/${rowData.id}`}
/>
)}
style={{ width: '4rem' }}
/>
</DataTable>
</Card>
</div>
{/* Terminal de développement (caché) */}
<Terminal
ref={terminalRef}
welcomeMessage="BTP Xpress Dashboard - Mode Développeur"
prompt="btpxpress $"
className="hidden"
/>
</div>
);
};
export default DashboardBTP;