378 lines
16 KiB
TypeScript
378 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { Chart } from 'primereact/chart';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Button } from 'primereact/button';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Divider } from 'primereact/divider';
|
|
import { Toast } from 'primereact/toast';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { materielService, maintenanceService } from '../../../../services/api';
|
|
import { Materiel, MaintenanceMateriel, TypeMateriel, StatutMateriel } from '../../../../types/btp';
|
|
import { formatCurrency } from '../../../../utils/formatters';
|
|
|
|
const MaterielsStatsPage = () => {
|
|
const [materiels, setMateriels] = useState<Materiel[]>([]);
|
|
const [maintenances, setMaintenances] = useState<MaintenanceMateriel[]>([]);
|
|
const [stats, setStats] = useState<any>(null);
|
|
const [valeurTotale, setValeurTotale] = useState<number>(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const toast = useRef<Toast>(null);
|
|
|
|
// Options des graphiques
|
|
const chartOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [materielsData, maintenancesData, statsData, valeurData] = await Promise.all([
|
|
materielService.getAll(),
|
|
maintenanceService.getAll(),
|
|
materielService.getStats(),
|
|
materielService.getValeurTotale()
|
|
]);
|
|
|
|
setMateriels(materielsData);
|
|
setMaintenances(maintenancesData);
|
|
setStats(statsData);
|
|
setValeurTotale(valeurData);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les statistiques',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Calculs des statistiques
|
|
const getStatutStats = () => {
|
|
const statutCounts = materiels.reduce((acc, materiel) => {
|
|
acc[materiel.statut] = (acc[materiel.statut] || 0) + 1;
|
|
return acc;
|
|
}, {} as Record<StatutMateriel, number>);
|
|
|
|
return {
|
|
labels: Object.keys(statutCounts).map(s => s.replace('_', ' ')),
|
|
datasets: [{
|
|
label: 'Matériels par statut',
|
|
data: Object.values(statutCounts),
|
|
backgroundColor: [
|
|
'#4CAF50', // DISPONIBLE
|
|
'#FF9800', // EN_UTILISATION
|
|
'#2196F3', // EN_MAINTENANCE
|
|
'#f44336' // HORS_SERVICE
|
|
]
|
|
}]
|
|
};
|
|
};
|
|
|
|
const getTypeStats = () => {
|
|
const typeCounts = materiels.reduce((acc, materiel) => {
|
|
acc[materiel.type] = (acc[materiel.type] || 0) + 1;
|
|
return acc;
|
|
}, {} as Record<TypeMateriel, number>);
|
|
|
|
return {
|
|
labels: Object.keys(typeCounts).map(t => t.replace('_', ' ')),
|
|
datasets: [{
|
|
label: 'Matériels par type',
|
|
data: Object.values(typeCounts),
|
|
backgroundColor: [
|
|
'#FF6384',
|
|
'#36A2EB',
|
|
'#FFCE56',
|
|
'#4BC0C0',
|
|
'#9966FF',
|
|
'#FF9F40'
|
|
]
|
|
}]
|
|
};
|
|
};
|
|
|
|
const getValeurStats = () => {
|
|
const valeurParType = materiels.reduce((acc, materiel) => {
|
|
const valeur = materiel.valeurActuelle || materiel.valeurAchat || 0;
|
|
acc[materiel.type] = (acc[materiel.type] || 0) + valeur;
|
|
return acc;
|
|
}, {} as Record<TypeMateriel, number>);
|
|
|
|
return {
|
|
labels: Object.keys(valeurParType).map(t => t.replace('_', ' ')),
|
|
datasets: [{
|
|
label: 'Valeur par type (€)',
|
|
data: Object.values(valeurParType),
|
|
backgroundColor: '#42A5F5',
|
|
borderColor: '#1976D2',
|
|
borderWidth: 1
|
|
}]
|
|
};
|
|
};
|
|
|
|
const getMaintenanceStats = () => {
|
|
const monthlyMaintenance = new Array(12).fill(0);
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
maintenances.forEach(maintenance => {
|
|
const date = new Date(maintenance.dateRealisee || maintenance.datePrevue);
|
|
if (date.getFullYear() === currentYear) {
|
|
monthlyMaintenance[date.getMonth()]++;
|
|
}
|
|
});
|
|
|
|
return {
|
|
labels: [
|
|
'Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
|
|
'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'
|
|
],
|
|
datasets: [{
|
|
label: 'Maintenances par mois',
|
|
data: monthlyMaintenance,
|
|
backgroundColor: 'rgba(255, 193, 7, 0.2)',
|
|
borderColor: '#FFC107',
|
|
borderWidth: 2,
|
|
fill: true
|
|
}]
|
|
};
|
|
};
|
|
|
|
// Template pour les matériels les plus coûteux
|
|
const valeurBodyTemplate = (rowData: Materiel) => {
|
|
return formatCurrency(rowData.valeurActuelle || rowData.valeurAchat);
|
|
};
|
|
|
|
const typeBodyTemplate = (rowData: Materiel) => {
|
|
return (
|
|
<Tag
|
|
value={rowData.type?.replace('_', ' ')}
|
|
severity={getTypeSeverity(rowData.type)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const getTypeSeverity = (type?: TypeMateriel) => {
|
|
switch (type) {
|
|
case TypeMateriel.ENGIN_CHANTIER:
|
|
return 'danger';
|
|
case TypeMateriel.OUTIL_ELECTRIQUE:
|
|
case TypeMateriel.OUTIL_MANUEL:
|
|
return 'warning';
|
|
case TypeMateriel.EQUIPEMENT_SECURITE:
|
|
return 'success';
|
|
case TypeMateriel.VEHICULE:
|
|
return 'info';
|
|
case TypeMateriel.GRUE:
|
|
case TypeMateriel.BETONIERE:
|
|
return 'danger';
|
|
default:
|
|
return 'secondary';
|
|
}
|
|
};
|
|
|
|
// Matériels triés par valeur décroissante
|
|
const materielsCouteux = [...materiels]
|
|
.sort((a, b) => (b.valeurActuelle || b.valeurAchat || 0) - (a.valeurActuelle || a.valeurAchat || 0))
|
|
.slice(0, 5);
|
|
|
|
// Calcul des KPI
|
|
const tauxDisponibilite = materiels.length > 0
|
|
? (materiels.filter(m => m.statut === StatutMateriel.DISPONIBLE).length / materiels.length) * 100
|
|
: 0;
|
|
|
|
const tauxMaintenance = materiels.length > 0
|
|
? (materiels.filter(m => m.statut === StatutMateriel.MAINTENANCE || m.statut === StatutMateriel.EN_REPARATION).length / materiels.length) * 100
|
|
: 0;
|
|
|
|
const tauxUtilisation = materiels.length > 0
|
|
? (materiels.filter(m => m.statut === StatutMateriel.UTILISE).length / materiels.length) * 100
|
|
: 0;
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Toast ref={toast} />
|
|
|
|
<div className="flex align-items-center justify-content-between mb-4">
|
|
<h2 className="m-0">Statistiques du Parc Matériel</h2>
|
|
<Button
|
|
label="Actualiser"
|
|
icon="pi pi-refresh"
|
|
className="p-button-text p-button-rounded"
|
|
onClick={loadData}
|
|
loading={loading}
|
|
/>
|
|
</div>
|
|
|
|
{/* KPI Cards */}
|
|
<div className="grid mb-4">
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex align-items-center">
|
|
<div className="mr-3">
|
|
<i className="pi pi-cog text-blue-500 text-3xl"></i>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-semibold">{materiels.length}</div>
|
|
<div className="text-500">Total matériels</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex align-items-center">
|
|
<div className="mr-3">
|
|
<i className="pi pi-euro text-green-500 text-3xl"></i>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-semibold">{formatCurrency(valeurTotale)}</div>
|
|
<div className="text-500">Valeur totale</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex align-items-center">
|
|
<div className="mr-3">
|
|
<i className="pi pi-check-circle text-green-500 text-3xl"></i>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-semibold">{tauxDisponibilite.toFixed(1)}%</div>
|
|
<div className="text-500">Disponibilité</div>
|
|
<ProgressBar value={tauxDisponibilite} showValue={false} style={{height: '6px'}} />
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card>
|
|
<div className="flex align-items-center">
|
|
<div className="mr-3">
|
|
<i className="pi pi-wrench text-orange-500 text-3xl"></i>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-semibold">{maintenances.length}</div>
|
|
<div className="text-500">Maintenances</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Indicateurs de performance */}
|
|
<div className="grid mb-4">
|
|
<div className="col-12 md:col-4">
|
|
<Card title="Taux de Disponibilité">
|
|
<div className="text-center">
|
|
<div className="text-6xl font-bold text-green-500 mb-2">
|
|
{tauxDisponibilite.toFixed(0)}%
|
|
</div>
|
|
<ProgressBar value={tauxDisponibilite} showValue={false} className="mb-3" />
|
|
<div className="text-500">
|
|
{materiels.filter(m => m.statut === StatutMateriel.DISPONIBLE).length} / {materiels.length} disponibles
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-4">
|
|
<Card title="Taux d'Utilisation">
|
|
<div className="text-center">
|
|
<div className="text-6xl font-bold text-blue-500 mb-2">
|
|
{tauxUtilisation.toFixed(0)}%
|
|
</div>
|
|
<ProgressBar value={tauxUtilisation} showValue={false} className="mb-3" />
|
|
<div className="text-500">
|
|
{materiels.filter(m => m.statut === StatutMateriel.UTILISE).length} / {materiels.length} en utilisation
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-4">
|
|
<Card title="Taux de Maintenance">
|
|
<div className="text-center">
|
|
<div className="text-6xl font-bold text-orange-500 mb-2">
|
|
{tauxMaintenance.toFixed(0)}%
|
|
</div>
|
|
<ProgressBar value={tauxMaintenance} showValue={false} className="mb-3" />
|
|
<div className="text-500">
|
|
{materiels.filter(m => m.statut === StatutMateriel.MAINTENANCE || m.statut === StatutMateriel.EN_REPARATION).length} / {materiels.length} en maintenance
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Graphiques */}
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Répartition par Statut">
|
|
<Chart type="doughnut" data={getStatutStats()} options={chartOptions} style={{ height: '400px' }} />
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Répartition par Type">
|
|
<Chart type="pie" data={getTypeStats()} options={chartOptions} style={{ height: '400px' }} />
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Valeur par Type de Matériel">
|
|
<Chart type="bar" data={getValeurStats()} options={chartOptions} style={{ height: '400px' }} />
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Évolution des Maintenances">
|
|
<Chart type="line" data={getMaintenanceStats()} options={chartOptions} style={{ height: '400px' }} />
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Top matériels les plus coûteux */}
|
|
<div className="col-12">
|
|
<Card title="Top 5 - Matériels les plus coûteux">
|
|
<DataTable
|
|
value={materielsCouteux}
|
|
showGridlines
|
|
emptyMessage="Aucun matériel trouvé."
|
|
>
|
|
<Column field="nom" header="Nom" />
|
|
<Column field="marque" header="Marque" />
|
|
<Column field="type" header="Type" body={typeBodyTemplate} />
|
|
<Column field="valeurActuelle" header="Valeur" body={valeurBodyTemplate} />
|
|
<Column field="localisation" header="Localisation" />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MaterielsStatsPage; |