1068 lines
51 KiB
TypeScript
1068 lines
51 KiB
TypeScript
'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;
|
|
|
|
|