Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

View File

@@ -0,0 +1,472 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { Card } from 'primereact/card';
import { Chart } from 'primereact/chart';
import { ProgressBar } from 'primereact/progressbar';
import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import { Toast } from 'primereact/toast';
import { DataView } from 'primereact/dataview';
import { Knob } from 'primereact/knob';
import { Timeline } from 'primereact/timeline';
import { Dropdown } from 'primereact/dropdown';
import { Calendar } from 'primereact/calendar';
import { Badge } from 'primereact/badge';
import { Panel } from 'primereact/panel';
import { Page } from '@/types';
import phaseChantierService from '@/services/phaseChantierService';
import { PhaseChantier } from '@/types/btp-extended';
const PhasesDashboardPage: Page = () => {
const [phases, setPhases] = useState<PhaseChantier[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPeriod, setSelectedPeriod] = useState('semaine');
const [chartData, setChartData] = useState<any>({});
const [avancementData, setAvancementData] = useState<any>({});
const [budgetData, setBudgetData] = useState<any>({});
const [timelineData, setTimelineData] = useState<any[]>([]);
const toast = useRef<Toast>(null);
const periodOptions = [
{ label: 'Cette semaine', value: 'semaine' },
{ label: 'Ce mois', value: 'mois' },
{ label: 'Ce trimestre', value: 'trimestre' }
];
useEffect(() => {
loadDashboardData();
}, [selectedPeriod]);
const loadDashboardData = async () => {
try {
setLoading(true);
// Charger les phases actives (exemple avec chantierId = 1)
const data = await phaseChantierService.getByChantier(1);
setPhases(data || []);
// Préparer les données pour les graphiques
prepareChartData(data || []);
prepareTimelineData(data || []);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données du tableau de bord',
life: 3000
});
} finally {
setLoading(false);
}
};
const prepareChartData = (data: PhaseChantier[]) => {
const documentStyle = getComputedStyle(document.documentElement);
// Graphique de répartition par statut
const statutCounts = data.reduce((acc, phase) => {
acc[phase.statut] = (acc[phase.statut] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const statutData = {
labels: Object.keys(statutCounts).map(statut =>
phaseChantierService.getStatutLabel(statut as any)
),
datasets: [{
data: Object.values(statutCounts),
backgroundColor: [
documentStyle.getPropertyValue('--blue-500'),
documentStyle.getPropertyValue('--green-500'),
documentStyle.getPropertyValue('--yellow-500'),
documentStyle.getPropertyValue('--orange-500'),
documentStyle.getPropertyValue('--red-500'),
documentStyle.getPropertyValue('--gray-500'),
],
hoverBackgroundColor: [
documentStyle.getPropertyValue('--blue-400'),
documentStyle.getPropertyValue('--green-400'),
documentStyle.getPropertyValue('--yellow-400'),
documentStyle.getPropertyValue('--orange-400'),
documentStyle.getPropertyValue('--red-400'),
documentStyle.getPropertyValue('--gray-400'),
]
}]
};
// Graphique d'avancement moyen
const avancementMoyen = data.length > 0
? data.reduce((sum, phase) => sum + (phase.pourcentageAvancement || 0), 0) / data.length
: 0;
// Graphique budget vs coût
const budgetVsCout = {
labels: data.map(phase => phase.nom?.substring(0, 15) + '...'),
datasets: [
{
label: 'Budget prévu',
backgroundColor: documentStyle.getPropertyValue('--blue-500'),
borderColor: documentStyle.getPropertyValue('--blue-500'),
data: data.map(phase => phase.budgetPrevu || 0)
},
{
label: 'Coût réel',
backgroundColor: documentStyle.getPropertyValue('--red-500'),
borderColor: documentStyle.getPropertyValue('--red-500'),
data: data.map(phase => phase.coutReel || 0)
}
]
};
setChartData(statutData);
setAvancementData({ value: avancementMoyen });
setBudgetData(budgetVsCout);
};
const prepareTimelineData = (data: PhaseChantier[]) => {
// Créer une timeline des prochaines échéances
const prochaines = data
.filter(phase => phase.dateFinPrevue && phase.statut !== 'TERMINEE')
.sort((a, b) => new Date(a.dateFinPrevue!).getTime() - new Date(b.dateFinPrevue!).getTime())
.slice(0, 5)
.map(phase => {
const dateEcheance = new Date(phase.dateFinPrevue!);
const maintenant = new Date();
const joursRestants = Math.ceil((dateEcheance.getTime() - maintenant.getTime()) / (1000 * 60 * 60 * 24));
return {
phase: phase.nom,
date: dateEcheance.toLocaleDateString('fr-FR'),
joursRestants,
statut: phase.statut,
avancement: phase.pourcentageAvancement || 0,
critique: phase.critique,
color: joursRestants < 0 ? '#FF6B6B' : joursRestants < 7 ? '#FFA726' : '#66BB6A',
icon: joursRestants < 0 ? 'pi pi-exclamation-triangle' :
joursRestants < 7 ? 'pi pi-clock' : 'pi pi-calendar'
};
});
setTimelineData(prochaines);
};
const calculateKPIs = () => {
const stats = phaseChantierService.calculateStatistiques(phases);
const phasesEnRetard = phases.filter(phase => phaseChantierService.isEnRetard(phase)).length;
const phasesCritiques = phases.filter(phase => phase.critique).length;
const budgetUtilise = (stats.coutTotal / stats.budgetTotal) * 100;
return {
avancementGlobal: stats.avancementMoyen,
respectDelais: ((phases.length - phasesEnRetard) / phases.length) * 100,
budgetUtilise,
phasesCritiques,
stats
};
};
const kpis = calculateKPIs();
const phaseItemTemplate = (phase: PhaseChantier) => {
const retard = phaseChantierService.isEnRetard(phase);
const avancement = phase.pourcentageAvancement || 0;
return (
<div className="col-12 md:col-6 lg:col-4">
<Card className="h-full">
<div className="flex flex-column h-full">
<div className="flex justify-content-between align-items-start mb-3">
<div className="flex-1">
<h6 className="m-0 text-lg font-semibold">{phase.nom}</h6>
<p className="text-color-secondary text-sm mt-1">
{phase.chantier?.nom || 'Chantier non défini'}
</p>
</div>
<div className="flex flex-column align-items-end gap-1">
<Tag
value={phaseChantierService.getStatutLabel(phase.statut)}
style={{ backgroundColor: phaseChantierService.getStatutColor(phase.statut) }}
className="text-white"
/>
{phase.critique && (
<Badge value="Critique" severity="danger" />
)}
{retard && (
<Badge value="Retard" severity="warning" />
)}
</div>
</div>
<div className="flex-1 flex flex-column justify-content-between">
<div>
<div className="flex justify-content-between align-items-center mb-2">
<span className="text-sm font-medium">Avancement</span>
<span className="text-sm font-bold">{avancement.toFixed(1)}%</span>
</div>
<ProgressBar
value={avancement}
className="mb-3"
style={{ height: '0.5rem' }}
/>
<div className="grid text-sm">
<div className="col-6">
<div className="text-color-secondary">Début prévu</div>
<div className="font-medium">
{phase.dateDebutPrevue ?
new Date(phase.dateDebutPrevue).toLocaleDateString('fr-FR') :
'Non défini'
}
</div>
</div>
<div className="col-6">
<div className="text-color-secondary">Fin prévue</div>
<div className="font-medium">
{phase.dateFinPrevue ?
new Date(phase.dateFinPrevue).toLocaleDateString('fr-FR') :
'Non défini'
}
</div>
</div>
</div>
</div>
<div className="flex justify-content-between align-items-center mt-3 pt-3 border-top-1 surface-border">
<div className="text-sm">
<div className="text-color-secondary">Budget</div>
<div className="font-bold">
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
notation: 'compact'
}).format(phase.budgetPrevu || 0)}
</div>
</div>
<Button
icon="pi pi-arrow-right"
className="p-button-rounded p-button-outlined p-button-sm"
onClick={() => window.location.href = `/chantiers`}
tooltip="Voir le chantier"
/>
</div>
</div>
</div>
</Card>
</div>
);
};
const chartOptions = {
plugins: {
legend: {
position: 'bottom' as const,
labels: {
usePointStyle: true,
padding: 20
}
}
},
maintainAspectRatio: false
};
const budgetChartOptions = {
plugins: {
legend: {
position: 'top' as const
}
},
responsive: true,
scales: {
x: {
ticks: {
maxRotation: 45
}
},
y: {
beginAtZero: true,
ticks: {
callback: function(value: any) {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
notation: 'compact'
}).format(value);
}
}
}
}
};
return (
<div className="grid">
<div className="col-12">
<Toast ref={toast} />
{/* Header avec filtres */}
<div className="flex justify-content-between align-items-center mb-4">
<h2 className="text-3xl font-bold m-0">Tableau de Bord - Phases</h2>
<div className="flex gap-2">
<Dropdown
value={selectedPeriod}
options={periodOptions}
onChange={(e) => setSelectedPeriod(e.value)}
className="w-12rem"
/>
<Button
icon="pi pi-refresh"
onClick={loadDashboardData}
loading={loading}
/>
</div>
</div>
{/* KPIs principaux */}
<div className="grid mb-4">
<div className="col-12 md:col-3">
<Card className="text-center">
<Knob
value={kpis.avancementGlobal}
size={80}
strokeWidth={8}
valueColor="#2196F3"
rangeColor="#E3F2FD"
/>
<h6 className="mt-3 mb-0">Avancement Global</h6>
<p className="text-color-secondary text-sm">
{kpis.avancementGlobal.toFixed(1)}% terminé
</p>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<Knob
value={kpis.respectDelais}
size={80}
strokeWidth={8}
valueColor="#4CAF50"
rangeColor="#E8F5E8"
/>
<h6 className="mt-3 mb-0">Respect des Délais</h6>
<p className="text-color-secondary text-sm">
{kpis.respectDelais.toFixed(1)}% dans les temps
</p>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<Knob
value={Math.min(kpis.budgetUtilise, 100)}
size={80}
strokeWidth={8}
valueColor={kpis.budgetUtilise > 100 ? "#F44336" : "#FF9800"}
rangeColor="#FFF3E0"
/>
<h6 className="mt-3 mb-0">Budget Utilisé</h6>
<p className="text-color-secondary text-sm">
{kpis.budgetUtilise.toFixed(1)}% du budget
</p>
</Card>
</div>
<div className="col-12 md:col-3">
<Card className="text-center">
<div className="text-4xl font-bold text-purple-500 mb-2">
{kpis.phasesCritiques}
</div>
<h6 className="mt-3 mb-0">Phases Critiques</h6>
<p className="text-color-secondary text-sm">
Nécessitent une attention
</p>
</Card>
</div>
</div>
<div className="grid">
{/* Graphiques */}
<div className="col-12 lg:col-8">
<div className="grid">
<div className="col-12 md:col-6">
<Card title="Répartition par Statut">
<Chart
type="doughnut"
data={chartData}
options={chartOptions}
style={{ height: '300px' }}
/>
</Card>
</div>
<div className="col-12 md:col-6">
<Card title="Budget vs Coût Réel">
<Chart
type="bar"
data={budgetData}
options={budgetChartOptions}
style={{ height: '300px' }}
/>
</Card>
</div>
</div>
</div>
{/* Timeline des prochaines échéances */}
<div className="col-12 lg:col-4">
<Card title="Prochaines Échéances">
<Timeline
value={timelineData}
align="left"
className="customized-timeline"
marker={(item) => (
<span
className="flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1 shadow-1"
style={{ backgroundColor: item.color }}
>
<i className={item.icon}></i>
</span>
)}
content={(item) => (
<Card className="mt-3 mb-3">
<div className="flex flex-column">
<div className="font-bold mb-1">{item.phase}</div>
<div className="text-sm text-color-secondary mb-2">
{item.date} {item.joursRestants >= 0 ?
`${item.joursRestants} jours restants` :
`${Math.abs(item.joursRestants)} jours de retard`
}
</div>
<ProgressBar
value={item.avancement}
style={{ height: '0.4rem' }}
className="mb-2"
/>
<div className="flex justify-content-between align-items-center">
<span className="text-xs">{item.avancement.toFixed(1)}%</span>
{item.critique && (
<Badge value="Critique" severity="danger" />
)}
</div>
</div>
</Card>
)}
/>
</Card>
</div>
</div>
{/* Vue des phases actives */}
<Card title="Phases Actives" className="mt-4">
<DataView
value={phases.filter(p => p.statut === 'EN_COURS')}
layout="grid"
itemTemplate={phaseItemTemplate}
paginator
rows={6}
emptyMessage="Aucune phase active"
/>
</Card>
</div>
</div>
);
};
export default PhasesDashboardPage;