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;

View File

@@ -0,0 +1,706 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { Toast } from 'primereact/toast';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { Calendar } from 'primereact/calendar';
import { Slider } from 'primereact/slider';
import { Tag } from 'primereact/tag';
import { Card } from 'primereact/card';
import { ProgressBar } from 'primereact/progressbar';
import { Toolbar } from 'primereact/toolbar';
import { Badge } from 'primereact/badge';
import { Divider } from 'primereact/divider';
import { InputTextarea } from 'primereact/inputtextarea';
import { InputNumber } from 'primereact/inputnumber';
import { confirmDialog } from 'primereact/confirmdialog';
import { ConfirmDialog } from 'primereact/confirmdialog';
import { Page } from '@/types';
import phaseChantierService from '@/services/phaseChantierService';
import { PhaseChantier, StatutPhase } from '@/types/btp-extended';
import {
ActionButtonGroup,
ViewButton,
EditButton,
DeleteButton,
StartButton
} from '../../../components/ui/ActionButton';
const PhasesChantierPage: Page = () => {
const [phases, setPhases] = useState<PhaseChantier[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPhases, setSelectedPhases] = useState<PhaseChantier[]>([]);
const [phaseDialog, setPhaseDialog] = useState(false);
const [deletePhaseDialog, setDeletePhaseDialog] = useState(false);
const [currentPhase, setCurrentPhase] = useState<PhaseChantier | null>(null);
const [submitted, setSubmitted] = useState(false);
const [globalFilter, setGlobalFilter] = useState('');
const [expandedRows, setExpandedRows] = useState<any>(null);
const [filters, setFilters] = useState({
statut: '',
chantierId: '',
enRetard: false
});
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<PhaseChantier[]>>(null);
// État pour le formulaire de phase
const [formData, setFormData] = useState({
nom: '',
description: '',
dateDebutPrevue: null as Date | null,
dateFinPrevue: null as Date | null,
budgetPrevu: 0,
statut: 'PLANIFIEE' as StatutPhase,
critique: false,
notes: ''
});
const statutOptions = [
{ label: 'Planifiée', value: 'PLANIFIEE' },
{ label: 'En attente', value: 'EN_ATTENTE' },
{ label: 'En cours', value: 'EN_COURS' },
{ label: 'En pause', value: 'EN_PAUSE' },
{ label: 'Terminée', value: 'TERMINEE' },
{ label: 'Annulée', value: 'ANNULEE' },
{ label: 'En retard', value: 'EN_RETARD' }
];
useEffect(() => {
loadPhases();
}, []);
const loadPhases = async () => {
try {
setLoading(true);
// Pour l'exemple, on charge toutes les phases.
// En production, on pourrait charger par chantier spécifique
const data = await phaseChantierService.getByChantier(1); // ID exemple
setPhases(data || []);
} catch (error) {
console.error('Erreur lors du chargement des phases:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les phases',
life: 3000
});
} finally {
setLoading(false);
}
};
const openNew = () => {
setCurrentPhase(null);
setFormData({
nom: '',
description: '',
dateDebutPrevue: null,
dateFinPrevue: null,
budgetPrevu: 0,
statut: 'PLANIFIEE',
critique: false,
notes: ''
});
setSubmitted(false);
setPhaseDialog(true);
};
const editPhase = (phase: PhaseChantier) => {
setCurrentPhase(phase);
setFormData({
nom: phase.nom || '',
description: phase.description || '',
dateDebutPrevue: phase.dateDebutPrevue ? new Date(phase.dateDebutPrevue) : null,
dateFinPrevue: phase.dateFinPrevue ? new Date(phase.dateFinPrevue) : null,
budgetPrevu: phase.budgetPrevu || 0,
statut: phase.statut,
critique: phase.critique || false,
notes: phase.notes || ''
});
setSubmitted(false);
setPhaseDialog(true);
};
const confirmDeletePhase = (phase: PhaseChantier) => {
setCurrentPhase(phase);
setDeletePhaseDialog(true);
};
const deletePhase = async () => {
try {
if (currentPhase?.id) {
await phaseChantierService.delete(currentPhase.id);
await loadPhases();
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Phase supprimée avec succès',
life: 3000
});
}
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de la suppression',
life: 3000
});
}
setDeletePhaseDialog(false);
setCurrentPhase(null);
};
const hideDialog = () => {
setSubmitted(false);
setPhaseDialog(false);
};
const hideDeleteDialog = () => {
setDeletePhaseDialog(false);
};
const savePhase = async () => {
setSubmitted(true);
if (formData.nom.trim()) {
try {
const phaseData = {
...formData,
dateDebutPrevue: formData.dateDebutPrevue?.toISOString(),
dateFinPrevue: formData.dateFinPrevue?.toISOString(),
chantierId: 1 // ID exemple - en production, viendrait du contexte
};
if (currentPhase?.id) {
await phaseChantierService.update(currentPhase.id, phaseData);
} else {
await phaseChantierService.create(phaseData);
}
await loadPhases();
hideDialog();
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: currentPhase ? 'Phase modifiée avec succès' : 'Phase créée avec succès',
life: 3000
});
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de la sauvegarde',
life: 3000
});
}
}
};
const onInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string) => {
const val = e.target.value || '';
setFormData(prev => ({ ...prev, [name]: val }));
};
const onNumberInputChange = (value: number | null, name: string) => {
setFormData(prev => ({ ...prev, [name]: value || 0 }));
};
const onDateChange = (value: Date | null, name: string) => {
setFormData(prev => ({ ...prev, [name]: value }));
};
const onDropdownChange = (e: any, name: string) => {
setFormData(prev => ({ ...prev, [name]: e.value }));
};
// Actions sur les phases
const startPhase = async (phase: PhaseChantier) => {
try {
if (phase.id) {
await phaseChantierService.start(phase.id);
await loadPhases();
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Phase démarrée',
life: 3000
});
}
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors du démarrage',
life: 3000
});
}
};
const completePhase = async (phase: PhaseChantier) => {
try {
if (phase.id) {
await phaseChantierService.complete(phase.id);
await loadPhases();
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Phase terminée',
life: 3000
});
}
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de la finalisation',
life: 3000
});
}
};
const suspendPhase = async (phase: PhaseChantier) => {
try {
if (phase.id) {
await phaseChantierService.suspend(phase.id);
await loadPhases();
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Phase suspendue',
life: 3000
});
}
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de la suspension',
life: 3000
});
}
};
// Templates pour les colonnes
const actionBodyTemplate = (rowData: PhaseChantier) => {
return (
<ActionButtonGroup>
<EditButton
onClick={() => editPhase(rowData)}
tooltip="Modifier"
/>
<DeleteButton
onClick={() => confirmDeletePhase(rowData)}
tooltip="Supprimer"
/>
{rowData.statut === 'PLANIFIEE' && (
<StartButton
onClick={() => startPhase(rowData)}
tooltip="Démarrer"
/>
)}
{rowData.statut === 'EN_COURS' && (
<>
<Button
icon="pi pi-pause"
rounded
severity="warning"
onClick={() => suspendPhase(rowData)}
tooltip="Suspendre"
/>
<Button
icon="pi pi-check"
rounded
severity="success"
onClick={() => completePhase(rowData)}
tooltip="Terminer"
/>
</>
)}
</ActionButtonGroup>
);
};
const statutBodyTemplate = (rowData: PhaseChantier) => {
const severity = phaseChantierService.getStatutColor(rowData.statut);
const label = phaseChantierService.getStatutLabel(rowData.statut);
return (
<Tag
value={label}
style={{ backgroundColor: severity }}
className="text-white"
/>
);
};
const avancementBodyTemplate = (rowData: PhaseChantier) => {
const value = rowData.pourcentageAvancement || 0;
return (
<div className="flex align-items-center gap-2">
<ProgressBar
value={value}
className="w-8rem"
style={{ height: '0.5rem' }}
/>
<span className="text-sm">{value.toFixed(1)}%</span>
</div>
);
};
const dateBodyTemplate = (date: string | null) => {
return date ? new Date(date).toLocaleDateString('fr-FR') : '-';
};
const budgetBodyTemplate = (value: number) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(value || 0);
};
const retardBodyTemplate = (rowData: PhaseChantier) => {
const isEnRetard = phaseChantierService.isEnRetard(rowData);
if (!isEnRetard) return null;
const retard = phaseChantierService.calculateRetard(rowData);
return (
<Badge
value={`${retard}j`}
severity="danger"
tooltip="Jours de retard"
/>
);
};
// Template pour les détails expandables
const rowExpansionTemplate = (data: PhaseChantier) => {
return (
<div className="p-3">
<h5>Détails de la phase: {data.nom}</h5>
<div className="grid">
<div className="col-12 md:col-6">
<p><strong>Description:</strong> {data.description || 'Aucune description'}</p>
<p><strong>Prérequis:</strong> {data.prerequis || 'Aucun'}</p>
<p><strong>Livrables:</strong> {data.livrables || 'Non définis'}</p>
</div>
<div className="col-12 md:col-6">
<p><strong>Risques:</strong> {data.risques || 'Non identifiés'}</p>
<p><strong>Notes:</strong> {data.notes || 'Aucune note'}</p>
<p><strong>Phase critique:</strong> {data.critique ? 'Oui' : 'Non'}</p>
</div>
</div>
</div>
);
};
// Statistiques rapides
const stats = phaseChantierService.calculateStatistiques(phases);
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouvelle Phase"
icon="pi pi-plus"
severity="success"
onClick={openNew}
/>
<Button
label="Phases en Retard"
icon="pi pi-exclamation-triangle"
severity="warning"
onClick={() => {/* TODO: Filtrer les phases en retard */}}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex align-items-center gap-2">
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
<Button
icon="pi pi-refresh"
onClick={loadPhases}
tooltip="Actualiser"
/>
</div>
);
};
return (
<div className="grid">
<div className="col-12">
<Toast ref={toast} />
<ConfirmDialog />
{/* Statistiques rapides */}
<div className="grid mb-4">
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<div className="text-2xl font-bold text-primary">{stats.total}</div>
<div className="text-sm text-color-secondary">Total phases</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<div className="text-2xl font-bold text-green-500">{stats.terminees}</div>
<div className="text-sm text-color-secondary">Terminées</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<div className="text-2xl font-bold text-blue-500">{stats.enCours}</div>
<div className="text-sm text-color-secondary">En cours</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<div className="text-2xl font-bold text-red-500">{stats.enRetard}</div>
<div className="text-sm text-color-secondary">En retard</div>
</div>
</Card>
</div>
</div>
<Card title="Gestion des Phases de Chantier">
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
<DataTable
ref={dt}
value={phases}
selection={selectedPhases}
onSelectionChange={(e) => setSelectedPhases(e.value as PhaseChantier[])}
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} phases"
globalFilter={globalFilter}
header="Liste des Phases de Chantier"
loading={loading}
expandedRows={expandedRows}
onRowToggle={(e) => setExpandedRows(e.data)}
rowExpansionTemplate={rowExpansionTemplate}
className="datatable-responsive"
emptyMessage="Aucune phase trouvée"
>
<Column expander style={{ width: '3rem' }} />
<Column selectionMode="multiple" style={{ width: '3rem' }} exportable={false} />
<Column field="nom" header="Nom" sortable style={{ minWidth: '12rem' }} />
<Column
field="statut"
header="Statut"
body={statutBodyTemplate}
sortable
style={{ minWidth: '8rem' }}
/>
<Column
field="pourcentageAvancement"
header="Avancement"
body={avancementBodyTemplate}
sortable
style={{ minWidth: '10rem' }}
/>
<Column
field="dateDebutPrevue"
header="Début prévu"
body={(rowData) => dateBodyTemplate(rowData.dateDebutPrevue)}
sortable
style={{ minWidth: '8rem' }}
/>
<Column
field="dateFinPrevue"
header="Fin prévue"
body={(rowData) => dateBodyTemplate(rowData.dateFinPrevue)}
sortable
style={{ minWidth: '8rem' }}
/>
<Column
field="budgetPrevu"
header="Budget"
body={(rowData) => budgetBodyTemplate(rowData.budgetPrevu)}
sortable
style={{ minWidth: '8rem' }}
/>
<Column
header="Retard"
body={retardBodyTemplate}
style={{ minWidth: '6rem' }}
/>
<Column
body={actionBodyTemplate}
exportable={false}
style={{ minWidth: '12rem' }}
/>
</DataTable>
</Card>
{/* Dialog pour créer/modifier une phase */}
<Dialog
visible={phaseDialog}
style={{ width: '600px' }}
header="Détails de la Phase"
modal
className="p-fluid"
footer={
<div>
<Button
label="Annuler"
icon="pi pi-times"
outlined
onClick={hideDialog}
/>
<Button
label="Enregistrer"
icon="pi pi-check"
onClick={savePhase}
/>
</div>
}
onHide={hideDialog}
>
<div className="field">
<label htmlFor="nom" className="font-bold">Nom *</label>
<InputText
id="nom"
value={formData.nom}
onChange={(e) => onInputChange(e, 'nom')}
required
autoFocus
className={submitted && !formData.nom ? 'p-invalid' : ''}
/>
{submitted && !formData.nom && <small className="p-error">Le nom est obligatoire.</small>}
</div>
<div className="field">
<label htmlFor="description" className="font-bold">Description</label>
<InputTextarea
id="description"
value={formData.description}
onChange={(e) => onInputChange(e, 'description')}
rows={3}
/>
</div>
<div className="formgrid grid">
<div className="field col">
<label htmlFor="dateDebutPrevue" className="font-bold">Date début prévue *</label>
<Calendar
id="dateDebutPrevue"
value={formData.dateDebutPrevue}
onChange={(e) => onDateChange(e.value, 'dateDebutPrevue')}
dateFormat="dd/mm/yy"
showIcon
className={submitted && !formData.dateDebutPrevue ? 'p-invalid' : ''}
/>
</div>
<div className="field col">
<label htmlFor="dateFinPrevue" className="font-bold">Date fin prévue *</label>
<Calendar
id="dateFinPrevue"
value={formData.dateFinPrevue}
onChange={(e) => onDateChange(e.value, 'dateFinPrevue')}
dateFormat="dd/mm/yy"
showIcon
className={submitted && !formData.dateFinPrevue ? 'p-invalid' : ''}
/>
</div>
</div>
<div className="formgrid grid">
<div className="field col">
<label htmlFor="budgetPrevu" className="font-bold">Budget prévu ()</label>
<InputNumber
id="budgetPrevu"
value={formData.budgetPrevu}
onValueChange={(e) => onNumberInputChange(e.value, 'budgetPrevu')}
mode="currency"
currency="EUR"
locale="fr-FR"
/>
</div>
<div className="field col">
<label htmlFor="statut" className="font-bold">Statut</label>
<Dropdown
id="statut"
value={formData.statut}
options={statutOptions}
onChange={(e) => onDropdownChange(e, 'statut')}
placeholder="Sélectionner un statut"
/>
</div>
</div>
<div className="field">
<label htmlFor="notes" className="font-bold">Notes</label>
<InputTextarea
id="notes"
value={formData.notes}
onChange={(e) => onInputChange(e, 'notes')}
rows={2}
/>
</div>
</Dialog>
{/* Dialog de confirmation de suppression */}
<Dialog
visible={deletePhaseDialog}
style={{ width: '450px' }}
header="Confirmer"
modal
footer={
<div>
<Button
label="Non"
icon="pi pi-times"
outlined
onClick={hideDeleteDialog}
/>
<Button
label="Oui"
icon="pi pi-check"
severity="danger"
onClick={deletePhase}
/>
</div>
}
onHide={hideDeleteDialog}
>
<div className="confirmation-content">
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
{currentPhase && (
<span>
Êtes-vous sûr de vouloir supprimer la phase <b>{currentPhase.nom}</b> ?
</span>
)}
</div>
</Dialog>
</div>
</div>
);
};
export default PhasesChantierPage;

View File

@@ -0,0 +1,497 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { Toast } from 'primereact/toast';
import { Card } from 'primereact/card';
import { Tag } from 'primereact/tag';
import { Badge } from 'primereact/badge';
import { ProgressBar } from 'primereact/progressbar';
import { Toolbar } from 'primereact/toolbar';
import { Panel } from 'primereact/panel';
import { Chart } from 'primereact/chart';
import { Timeline } from 'primereact/timeline';
import { Message } from 'primereact/message';
import { Page } from '@/types';
import phaseChantierService from '@/services/phaseChantierService';
import { PhaseChantier } from '@/types/btp-extended';
const PhasesEnRetardPage: Page = () => {
const [phasesEnRetard, setPhasesEnRetard] = useState<PhaseChantier[]>([]);
const [loading, setLoading] = useState(true);
const [statistiques, setStatistiques] = useState({
total: 0,
retardMoyen: 0,
impactBudget: 0,
phasesUrgentes: 0
});
const [chartData, setChartData] = useState<any>({});
const [chartOptions, setChartOptions] = useState<any>({});
const toast = useRef<Toast>(null);
useEffect(() => {
loadPhasesEnRetard();
initChart();
}, []);
const loadPhasesEnRetard = async () => {
try {
setLoading(true);
const data = await phaseChantierService.getEnRetard();
setPhasesEnRetard(data || []);
// Calculer les statistiques
if (data && data.length > 0) {
const retards = data.map(phase => phaseChantierService.calculateRetard(phase));
const retardMoyen = retards.reduce((a, b) => a + b, 0) / retards.length;
const impactBudget = data.reduce((total, phase) => {
const ecart = (phase.coutReel || 0) - (phase.budgetPrevu || 0);
return total + (ecart > 0 ? ecart : 0);
}, 0);
const phasesUrgentes = data.filter(phase =>
phaseChantierService.calculateRetard(phase) > 30
).length;
setStatistiques({
total: data.length,
retardMoyen: Math.round(retardMoyen),
impactBudget,
phasesUrgentes
});
}
} catch (error) {
console.error('Erreur lors du chargement des phases en retard:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les phases en retard',
life: 3000
});
} finally {
setLoading(false);
}
};
const initChart = () => {
const documentStyle = getComputedStyle(document.documentElement);
const textColor = documentStyle.getPropertyValue('--text-color');
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
const data = {
labels: ['1-7 jours', '8-15 jours', '16-30 jours', '> 30 jours'],
datasets: [
{
label: 'Phases en retard',
backgroundColor: ['#FFF3CD', '#FCF8E3', '#F8D7DA', '#D32F2F'],
borderColor: ['#856404', '#856404', '#721C24', '#B71C1C'],
data: [0, 0, 0, 0] // Sera calculé dynamiquement
}
]
};
const options = {
maintainAspectRatio: false,
aspectRatio: 0.6,
plugins: {
legend: {
labels: {
fontColor: textColor
}
}
},
scales: {
x: {
ticks: {
color: textColorSecondary
},
grid: {
color: surfaceBorder
}
},
y: {
ticks: {
color: textColorSecondary
},
grid: {
color: surfaceBorder
}
}
}
};
setChartData(data);
setChartOptions(options);
};
const actionBodyTemplate = (rowData: PhaseChantier) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-play"
rounded
severity="warning"
onClick={() => relancerPhase(rowData)}
tooltip="Relancer la phase"
size="small"
/>
<Button
icon="pi pi-calendar"
rounded
severity="info"
onClick={() => replanifierPhase(rowData)}
tooltip="Replanifier"
size="small"
/>
<Button
icon="pi pi-exclamation-triangle"
rounded
severity="danger"
onClick={() => escaladerPhase(rowData)}
tooltip="Escalader"
size="small"
/>
</div>
);
};
const retardBodyTemplate = (rowData: PhaseChantier) => {
const retard = phaseChantierService.calculateRetard(rowData);
let severity: 'info' | 'warning' | 'danger' = 'info';
if (retard > 30) severity = 'danger';
else if (retard > 15) severity = 'warning';
return (
<Badge
value={`${retard} jours`}
severity={severity}
size="large"
/>
);
};
const impactBodyTemplate = (rowData: PhaseChantier) => {
const budgetPrevu = rowData.budgetPrevu || 0;
const coutReel = rowData.coutReel || 0;
const ecart = coutReel - budgetPrevu;
if (ecart <= 0) return <Tag value="Budget respecté" severity="success" />;
const pourcentageEcart = budgetPrevu > 0 ? (ecart / budgetPrevu * 100) : 0;
return (
<div className="flex flex-column gap-1">
<Tag
value={`+${new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(ecart)}`}
severity="danger"
/>
<small className="text-color-secondary">
+{pourcentageEcart.toFixed(1)}%
</small>
</div>
);
};
const prioriteBodyTemplate = (rowData: PhaseChantier) => {
const retard = phaseChantierService.calculateRetard(rowData);
const critique = rowData.critique;
let priorite = 'Normale';
let severity: 'info' | 'warning' | 'danger' = 'info';
if (critique && retard > 15) {
priorite = 'URGENTE';
severity = 'danger';
} else if (retard > 30) {
priorite = 'Très haute';
severity = 'danger';
} else if (retard > 15) {
priorite = 'Haute';
severity = 'warning';
}
return <Tag value={priorite} severity={severity} />;
};
const responsableBodyTemplate = (rowData: PhaseChantier) => {
return rowData.responsable ?
`${rowData.responsable.prenom} ${rowData.responsable.nom}` :
'Non assigné';
};
const relancerPhase = async (phase: PhaseChantier) => {
try {
if (phase.id) {
await phaseChantierService.resume(phase.id);
await loadPhasesEnRetard();
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Phase relancée avec succès',
life: 3000
});
}
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Erreur lors de la relance',
life: 3000
});
}
};
const replanifierPhase = (phase: PhaseChantier) => {
// TODO: Ouvrir un dialog de replanification
toast.current?.show({
severity: 'info',
summary: 'Fonctionnalité',
detail: 'Replanification à implémenter',
life: 3000
});
};
const escaladerPhase = (phase: PhaseChantier) => {
// TODO: Implémenter l'escalade (notification aux responsables)
toast.current?.show({
severity: 'warn',
summary: 'Escalade',
detail: `Phase ${phase.nom} escaladée vers la direction`,
life: 3000
});
};
const exportData = () => {
// TODO: Implémenter l'export des données
toast.current?.show({
severity: 'info',
summary: 'Export',
detail: 'Export en cours...',
life: 3000
});
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Actualiser"
icon="pi pi-refresh"
onClick={loadPhasesEnRetard}
/>
<Button
label="Exporter"
icon="pi pi-download"
severity="help"
onClick={exportData}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex align-items-center gap-2">
<Tag
value={`${phasesEnRetard.length} phases en retard`}
severity="danger"
icon="pi pi-exclamation-triangle"
/>
</div>
);
};
// Timeline des actions recommandées
const actionsRecommandees = [
{
status: 'Immédiat',
date: 'Aujourd\'hui',
icon: 'pi pi-exclamation-triangle',
color: '#FF6B6B',
description: `${statistiques.phasesUrgentes} phases critiques à traiter en urgence`
},
{
status: 'Cette semaine',
date: '7 jours',
icon: 'pi pi-calendar',
color: '#4ECDC4',
description: 'Replanification des phases avec retard modéré'
},
{
status: 'Ce mois',
date: '30 jours',
icon: 'pi pi-chart-line',
color: '#45B7D1',
description: 'Analyse des causes et mise en place d\'actions préventives'
}
];
return (
<div className="grid">
<div className="col-12">
<Toast ref={toast} />
{/* Alerte si phases critiques */}
{statistiques.phasesUrgentes > 0 && (
<Message
severity="error"
text={`⚠️ ALERTE: ${statistiques.phasesUrgentes} phases critiques nécessitent une intervention immédiate!`}
className="w-full mb-4"
/>
)}
{/* Statistiques de retard */}
<div className="grid mb-4">
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<div className="text-3xl font-bold text-red-500 mb-2">
<i className="pi pi-exclamation-triangle mr-2"></i>
{statistiques.total}
</div>
<div className="text-color-secondary">Phases en retard</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<div className="text-3xl font-bold text-orange-500 mb-2">
{statistiques.retardMoyen}j
</div>
<div className="text-color-secondary">Retard moyen</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<div className="text-3xl font-bold text-red-600 mb-2">
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
notation: 'compact'
}).format(statistiques.impactBudget)}
</div>
<div className="text-color-secondary">Impact budget</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="text-center">
<div className="text-3xl font-bold text-purple-500 mb-2">
{statistiques.phasesUrgentes}
</div>
<div className="text-color-secondary">Phases urgentes</div>
</div>
</Card>
</div>
</div>
<div className="grid">
{/* Liste des phases en retard */}
<div className="col-12 lg:col-8">
<Card title="Phases en Retard - Actions Requises">
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
<DataTable
value={phasesEnRetard}
paginator
rows={10}
dataKey="id"
loading={loading}
emptyMessage="Aucune phase en retard (excellente nouvelle !)"
className="datatable-responsive"
>
<Column field="nom" header="Phase" sortable style={{ minWidth: '12rem' }} />
<Column
header="Chantier"
body={(rowData) => rowData.chantier?.nom || 'N/A'}
sortable
style={{ minWidth: '10rem' }}
/>
<Column
header="Responsable"
body={responsableBodyTemplate}
style={{ minWidth: '10rem' }}
/>
<Column
header="Retard"
body={retardBodyTemplate}
sortable
style={{ minWidth: '8rem' }}
/>
<Column
header="Priorité"
body={prioriteBodyTemplate}
style={{ minWidth: '8rem' }}
/>
<Column
header="Impact Budget"
body={impactBodyTemplate}
style={{ minWidth: '10rem' }}
/>
<Column
body={actionBodyTemplate}
exportable={false}
style={{ minWidth: '12rem' }}
header="Actions"
/>
</DataTable>
</Card>
</div>
{/* Actions recommandées */}
<div className="col-12 lg:col-4">
<Card title="Plan d'Action Recommandé">
<Timeline
value={actionsRecommandees}
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">
<div className="flex flex-column">
<div className="text-lg font-bold mb-2" style={{ color: item.color }}>
{item.status}
</div>
<div className="text-sm text-color-secondary mb-2">
{item.date}
</div>
<div className="text-color">
{item.description}
</div>
</div>
</Card>
)}
/>
</Card>
<Card title="Répartition des Retards" className="mt-4">
<Chart type="doughnut" data={chartData} options={chartOptions} style={{ height: '300px' }} />
</Card>
</div>
</div>
</div>
</div>
);
};
export default PhasesEnRetardPage;