570 lines
22 KiB
TypeScript
570 lines
22 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { Button } from 'primereact/button';
|
|
import { Chart } from 'primereact/chart';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { useRouter } from 'next/navigation';
|
|
import { apiClient } from '../../../../services/api-client';
|
|
|
|
interface StatistiquesMaintenance {
|
|
periode: {
|
|
debut: string;
|
|
fin: string;
|
|
};
|
|
resume: {
|
|
totalMaintenances: number;
|
|
maintenancesTerminees: number;
|
|
maintenancesEnCours: number;
|
|
maintenancesPlanifiees: number;
|
|
tauxReussite: number;
|
|
coutTotal: number;
|
|
dureeMovenneMaintenance: number;
|
|
tempsMovenReponse: number;
|
|
tempsMovenResolution: number;
|
|
};
|
|
repartitionParType: Array<{
|
|
type: string;
|
|
nombre: number;
|
|
pourcentage: number;
|
|
coutMoyen: number;
|
|
}>;
|
|
repartitionParPriorite: Array<{
|
|
priorite: string;
|
|
nombre: number;
|
|
pourcentage: number;
|
|
tempsMovenResolution: number;
|
|
}>;
|
|
evolutionMensuelle: Array<{
|
|
mois: string;
|
|
preventives: number;
|
|
correctives: number;
|
|
urgentes: number;
|
|
cout: number;
|
|
}>;
|
|
performanceTechniciens: Array<{
|
|
technicienId: number;
|
|
technicienNom: string;
|
|
nombreMaintenances: number;
|
|
tauxReussite: number;
|
|
dureeMovenne: number;
|
|
evaluationMoyenne: number;
|
|
specialites: string[];
|
|
}>;
|
|
materielsProblematiques: Array<{
|
|
materielId: number;
|
|
materielNom: string;
|
|
materielType: string;
|
|
nombrePannes: number;
|
|
coutMaintenance: number;
|
|
tempsArret: number;
|
|
dernierePanne: string;
|
|
fiabilite: number;
|
|
}>;
|
|
indicateursPerformance: {
|
|
mtbf: number; // Mean Time Between Failures
|
|
mttr: number; // Mean Time To Repair
|
|
disponibilite: number;
|
|
fiabilite: number;
|
|
maintenabilite: number;
|
|
};
|
|
coutParCategorie: Array<{
|
|
categorie: string;
|
|
cout: number;
|
|
pourcentage: number;
|
|
}>;
|
|
tendances: {
|
|
evolutionCouts: number; // pourcentage d'évolution
|
|
evolutionNombreMaintenances: number;
|
|
evolutionTempsReponse: number;
|
|
evolutionDisponibilite: number;
|
|
};
|
|
}
|
|
|
|
const StatistiquesMaintenancePage = () => {
|
|
const [statistiques, setStatistiques] = useState<StatistiquesMaintenance | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [dateDebut, setDateDebut] = useState<Date>(new Date(Date.now() - 6 * 30 * 24 * 60 * 60 * 1000)); // -6 mois
|
|
const [dateFin, setDateFin] = useState<Date>(new Date());
|
|
const [filterType, setFilterType] = useState<string | null>(null);
|
|
const [chartOptions] = useState({
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top' as const,
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
});
|
|
const router = useRouter();
|
|
|
|
const typeOptions = [
|
|
{ label: 'Tous', value: null },
|
|
{ label: 'Préventive', value: 'PREVENTIVE' },
|
|
{ label: 'Corrective', value: 'CORRECTIVE' },
|
|
{ label: 'Planifiée', value: 'PLANIFIEE' },
|
|
{ label: 'Urgente', value: 'URGENTE' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadStatistiques();
|
|
}, [dateDebut, dateFin, filterType]);
|
|
|
|
const loadStatistiques = async () => {
|
|
try {
|
|
setLoading(true);
|
|
console.log('🔄 Chargement des statistiques maintenance...');
|
|
|
|
const params = new URLSearchParams();
|
|
params.append('dateDebut', dateDebut.toISOString().split('T')[0]);
|
|
params.append('dateFin', dateFin.toISOString().split('T')[0]);
|
|
if (filterType) params.append('type', filterType);
|
|
|
|
const response = await apiClient.get(`/api/maintenances/statistiques?${params.toString()}`);
|
|
console.log('✅ Statistiques chargées:', response.data);
|
|
setStatistiques(response.data);
|
|
} catch (error) {
|
|
console.error('❌ Erreur lors du chargement des statistiques:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getEvolutionChartData = () => {
|
|
if (!statistiques?.evolutionMensuelle) return {};
|
|
|
|
return {
|
|
labels: statistiques.evolutionMensuelle.map(e => e.mois),
|
|
datasets: [
|
|
{
|
|
label: 'Préventives',
|
|
data: statistiques.evolutionMensuelle.map(e => e.preventives),
|
|
borderColor: '#3b82f6',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
tension: 0.4
|
|
},
|
|
{
|
|
label: 'Correctives',
|
|
data: statistiques.evolutionMensuelle.map(e => e.correctives),
|
|
borderColor: '#f59e0b',
|
|
backgroundColor: 'rgba(245, 158, 11, 0.1)',
|
|
tension: 0.4
|
|
},
|
|
{
|
|
label: 'Urgentes',
|
|
data: statistiques.evolutionMensuelle.map(e => e.urgentes),
|
|
borderColor: '#ef4444',
|
|
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
tension: 0.4
|
|
}
|
|
]
|
|
};
|
|
};
|
|
|
|
const getRepartitionTypeChartData = () => {
|
|
if (!statistiques?.repartitionParType) return {};
|
|
|
|
return {
|
|
labels: statistiques.repartitionParType.map(r => r.type),
|
|
datasets: [{
|
|
data: statistiques.repartitionParType.map(r => r.nombre),
|
|
backgroundColor: [
|
|
'#3b82f6',
|
|
'#f59e0b',
|
|
'#10b981',
|
|
'#ef4444'
|
|
]
|
|
}]
|
|
};
|
|
};
|
|
|
|
const getCoutChartData = () => {
|
|
if (!statistiques?.coutParCategorie) return {};
|
|
|
|
return {
|
|
labels: statistiques.coutParCategorie.map(c => c.categorie),
|
|
datasets: [{
|
|
data: statistiques.coutParCategorie.map(c => c.cout),
|
|
backgroundColor: [
|
|
'#8b5cf6',
|
|
'#06b6d4',
|
|
'#84cc16',
|
|
'#f97316',
|
|
'#ec4899'
|
|
]
|
|
}]
|
|
};
|
|
};
|
|
|
|
const performanceBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<ProgressBar value={rowData.tauxReussite} className="flex-1" />
|
|
<span className="text-sm font-medium">{rowData.tauxReussite}%</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const evaluationBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<span className="font-medium">{rowData.evaluationMoyenne}/5</span>
|
|
<div className="flex">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<i
|
|
key={star}
|
|
className={`pi pi-star${star <= rowData.evaluationMoyenne ? '-fill' : ''} text-yellow-500`}
|
|
style={{ fontSize: '0.8rem' }}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const fiabiliteBodyTemplate = (rowData: any) => {
|
|
const couleur = rowData.fiabilite > 80 ? 'success' :
|
|
rowData.fiabilite > 60 ? 'warning' : 'danger';
|
|
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<ProgressBar
|
|
value={rowData.fiabilite}
|
|
className="flex-1"
|
|
color={couleur === 'success' ? '#10b981' : couleur === 'warning' ? '#fbbf24' : '#f87171'}
|
|
/>
|
|
<span className="text-sm font-medium">{rowData.fiabilite}%</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const coutBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex flex-column gap-1">
|
|
<span className="font-medium text-red-600">
|
|
{rowData.coutMaintenance.toLocaleString('fr-FR')} €
|
|
</span>
|
|
<span className="text-sm text-500">
|
|
{rowData.nombrePannes} pannes
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
label="Retour Maintenance"
|
|
icon="pi pi-arrow-left"
|
|
className="p-button-outlined"
|
|
onClick={() => router.push('/maintenance')}
|
|
/>
|
|
<Button
|
|
label="Rapport Détaillé"
|
|
icon="pi pi-file-pdf"
|
|
className="p-button-info"
|
|
onClick={() => router.push('/maintenance/rapport-detaille')}
|
|
/>
|
|
<Button
|
|
label="Exporter Données"
|
|
icon="pi pi-download"
|
|
className="p-button-secondary"
|
|
onClick={() => router.push('/maintenance/export-statistiques')}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const rightToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
className="p-button-outlined"
|
|
onClick={loadStatistiques}
|
|
tooltip="Actualiser"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-center">
|
|
<i className="pi pi-spin pi-spinner" style={{ fontSize: '2rem' }} />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!statistiques) {
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="text-center">
|
|
<p>Aucune donnée disponible pour la période sélectionnée</p>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Toolbar
|
|
className="mb-4"
|
|
left={leftToolbarTemplate}
|
|
right={rightToolbarTemplate}
|
|
/>
|
|
</div>
|
|
|
|
{/* Filtres */}
|
|
<div className="col-12">
|
|
<Card className="mb-4">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-3">
|
|
<label className="font-medium mb-2 block">Date début</label>
|
|
<Calendar
|
|
value={dateDebut}
|
|
onChange={(e) => setDateDebut(e.value as Date)}
|
|
showIcon
|
|
dateFormat="dd/mm/yy"
|
|
/>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<label className="font-medium mb-2 block">Date fin</label>
|
|
<Calendar
|
|
value={dateFin}
|
|
onChange={(e) => setDateFin(e.value as Date)}
|
|
showIcon
|
|
dateFormat="dd/mm/yy"
|
|
/>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<label className="font-medium mb-2 block">Type</label>
|
|
<Dropdown
|
|
value={filterType}
|
|
options={typeOptions}
|
|
onChange={(e) => setFilterType(e.value)}
|
|
placeholder="Tous les types"
|
|
/>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<label className="font-medium mb-2 block">Actions</label>
|
|
<Button
|
|
label="Réinitialiser"
|
|
icon="pi pi-filter-slash"
|
|
className="p-button-outlined w-full"
|
|
onClick={() => {
|
|
setDateDebut(new Date(Date.now() - 6 * 30 * 24 * 60 * 60 * 1000));
|
|
setDateFin(new Date());
|
|
setFilterType(null);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Métriques principales */}
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<div className="text-2xl font-bold text-blue-600">{statistiques.resume.totalMaintenances}</div>
|
|
<div className="text-500">Total maintenances</div>
|
|
</div>
|
|
<i className="pi pi-wrench text-blue-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<div className="text-2xl font-bold text-green-600">{statistiques.resume.tauxReussite}%</div>
|
|
<div className="text-500">Taux de réussite</div>
|
|
</div>
|
|
<i className="pi pi-check-circle text-green-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<div className="text-2xl font-bold text-orange-600">
|
|
{statistiques.resume.coutTotal.toLocaleString('fr-FR')} €
|
|
</div>
|
|
<div className="text-500">Coût total</div>
|
|
</div>
|
|
<i className="pi pi-euro text-orange-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex align-items-center justify-content-between">
|
|
<div>
|
|
<div className="text-2xl font-bold text-purple-600">
|
|
{statistiques.indicateursPerformance.disponibilite}%
|
|
</div>
|
|
<div className="text-500">Disponibilité</div>
|
|
</div>
|
|
<i className="pi pi-chart-line text-purple-500 text-3xl" />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Graphiques */}
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Évolution des Maintenances">
|
|
<Chart type="line" data={getEvolutionChartData()} options={chartOptions} />
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Répartition par Type">
|
|
<Chart type="doughnut" data={getRepartitionTypeChartData()} />
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Coûts par Catégorie">
|
|
<Chart type="pie" data={getCoutChartData()} />
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Indicateurs de Performance">
|
|
<div className="grid">
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-blue-600">{statistiques.indicateursPerformance.mtbf}h</div>
|
|
<div className="text-500 text-sm">MTBF</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-green-600">{statistiques.indicateursPerformance.mttr}h</div>
|
|
<div className="text-500 text-sm">MTTR</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-orange-600">{statistiques.indicateursPerformance.fiabilite}%</div>
|
|
<div className="text-500 text-sm">Fiabilité</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-center">
|
|
<div className="text-xl font-bold text-purple-600">{statistiques.indicateursPerformance.maintenabilite}%</div>
|
|
<div className="text-500 text-sm">Maintenabilité</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Tableaux détaillés */}
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Performance des Techniciens">
|
|
<DataTable
|
|
value={statistiques.performanceTechniciens}
|
|
responsiveLayout="scroll"
|
|
paginator
|
|
rows={5}
|
|
>
|
|
<Column field="technicienNom" header="Technicien" />
|
|
<Column field="nombreMaintenances" header="Nb" />
|
|
<Column field="tauxReussite" header="Réussite" body={performanceBodyTemplate} />
|
|
<Column field="evaluationMoyenne" header="Évaluation" body={evaluationBodyTemplate} />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-6">
|
|
<Card title="Matériels Problématiques">
|
|
<DataTable
|
|
value={statistiques.materielsProblematiques}
|
|
responsiveLayout="scroll"
|
|
paginator
|
|
rows={5}
|
|
>
|
|
<Column field="materielNom" header="Matériel" />
|
|
<Column field="nombrePannes" header="Pannes" />
|
|
<Column field="fiabilite" header="Fiabilité" body={fiabiliteBodyTemplate} />
|
|
<Column field="coutMaintenance" header="Coût" body={coutBodyTemplate} />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Tendances */}
|
|
<div className="col-12">
|
|
<Card title="Tendances">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center">
|
|
<div className={`text-xl font-bold ${statistiques.tendances.evolutionCouts > 0 ? 'text-red-600' : 'text-green-600'}`}>
|
|
{statistiques.tendances.evolutionCouts > 0 ? '+' : ''}{statistiques.tendances.evolutionCouts}%
|
|
</div>
|
|
<div className="text-500">Évolution coûts</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center">
|
|
<div className={`text-xl font-bold ${statistiques.tendances.evolutionNombreMaintenances > 0 ? 'text-red-600' : 'text-green-600'}`}>
|
|
{statistiques.tendances.evolutionNombreMaintenances > 0 ? '+' : ''}{statistiques.tendances.evolutionNombreMaintenances}%
|
|
</div>
|
|
<div className="text-500">Évolution nombre</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center">
|
|
<div className={`text-xl font-bold ${statistiques.tendances.evolutionTempsReponse > 0 ? 'text-red-600' : 'text-green-600'}`}>
|
|
{statistiques.tendances.evolutionTempsReponse > 0 ? '+' : ''}{statistiques.tendances.evolutionTempsReponse}%
|
|
</div>
|
|
<div className="text-500">Temps réponse</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center">
|
|
<div className={`text-xl font-bold ${statistiques.tendances.evolutionDisponibilite > 0 ? 'text-green-600' : 'text-red-600'}`}>
|
|
{statistiques.tendances.evolutionDisponibilite > 0 ? '+' : ''}{statistiques.tendances.evolutionDisponibilite}%
|
|
</div>
|
|
<div className="text-500">Disponibilité</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default StatistiquesMaintenancePage;
|