Files
btpxpress-frontend/app/(main)/phases-chantier/retard/page.tsx
2025-10-15 20:12:57 +00:00

498 lines
19 KiB
TypeScript

'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;