Initial commit
This commit is contained in:
569
app/(main)/maintenance/stats/page.tsx
Normal file
569
app/(main)/maintenance/stats/page.tsx
Normal file
@@ -0,0 +1,569 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user