Initial commit
This commit is contained in:
538
app/(main)/dashboard/maintenance/page.tsx
Normal file
538
app/(main)/dashboard/maintenance/page.tsx
Normal file
@@ -0,0 +1,538 @@
|
||||
'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 { Tag } from 'primereact/tag';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { Timeline } from 'primereact/timeline';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface MaintenanceDashboard {
|
||||
id: string;
|
||||
materiel: string;
|
||||
type: string;
|
||||
statut: string;
|
||||
priorite: string;
|
||||
dateCreation: Date;
|
||||
datePrevue: Date;
|
||||
dateRealisation?: Date;
|
||||
technicien: string;
|
||||
coutEstime: number;
|
||||
coutReel: number;
|
||||
dureeEstimee: number;
|
||||
dureeReelle?: number;
|
||||
description: string;
|
||||
localisation: string;
|
||||
}
|
||||
|
||||
const DashboardMaintenance = () => {
|
||||
const toast = useRef<Toast>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const [maintenances, setMaintenances] = useState<MaintenanceDashboard[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedType, setSelectedType] = useState('');
|
||||
const [selectedStatut, setSelectedStatut] = useState('');
|
||||
const [dateRange, setDateRange] = useState<Date[]>([]);
|
||||
|
||||
const [chartData, setChartData] = useState({});
|
||||
const [chartOptions, setChartOptions] = useState({});
|
||||
const [timelineEvents, setTimelineEvents] = useState([]);
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'Tous les types', value: '' },
|
||||
{ label: 'Préventive', value: 'PREVENTIVE' },
|
||||
{ label: 'Corrective', value: 'CORRECTIVE' },
|
||||
{ label: 'Prédictive', value: 'PREDICTIVE' },
|
||||
{ label: 'Urgente', value: 'URGENTE' }
|
||||
];
|
||||
|
||||
const statutOptions = [
|
||||
{ label: 'Tous les statuts', value: '' },
|
||||
{ label: 'Planifiée', value: 'PLANIFIEE' },
|
||||
{ label: 'En cours', value: 'EN_COURS' },
|
||||
{ label: 'Terminée', value: 'TERMINEE' },
|
||||
{ label: 'Reportée', value: 'REPORTEE' },
|
||||
{ label: 'Annulée', value: 'ANNULEE' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadMaintenances();
|
||||
initCharts();
|
||||
initTimeline();
|
||||
}, [selectedType, selectedStatut, dateRange]);
|
||||
|
||||
const loadMaintenances = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// TODO: Remplacer par un vrai appel API
|
||||
// const response = await maintenanceService.getDashboardData({
|
||||
// type: selectedType,
|
||||
// statut: selectedStatut,
|
||||
// dateRange
|
||||
// });
|
||||
|
||||
// Données simulées pour la démonstration
|
||||
const mockMaintenances: MaintenanceDashboard[] = [
|
||||
{
|
||||
id: '1',
|
||||
materiel: 'Grue mobile Liebherr LTM 1050',
|
||||
type: 'PREVENTIVE',
|
||||
statut: 'PLANIFIEE',
|
||||
priorite: 'MOYENNE',
|
||||
dateCreation: new Date('2024-12-15'),
|
||||
datePrevue: new Date('2025-01-15'),
|
||||
technicien: 'Jean Dupont',
|
||||
coutEstime: 1200,
|
||||
coutReel: 0,
|
||||
dureeEstimee: 4,
|
||||
description: 'Révision générale et contrôle sécurité',
|
||||
localisation: 'Atelier Central'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
materiel: 'Pelleteuse CAT 320D',
|
||||
type: 'CORRECTIVE',
|
||||
statut: 'EN_COURS',
|
||||
priorite: 'HAUTE',
|
||||
dateCreation: new Date('2024-12-20'),
|
||||
datePrevue: new Date('2024-12-28'),
|
||||
dateRealisation: new Date('2024-12-28'),
|
||||
technicien: 'Marie Martin',
|
||||
coutEstime: 800,
|
||||
coutReel: 950,
|
||||
dureeEstimee: 2,
|
||||
dureeReelle: 3,
|
||||
description: 'Réparation système hydraulique',
|
||||
localisation: 'Chantier Résidence Les Jardins'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
materiel: 'Bétonnière Schwing S36X',
|
||||
type: 'URGENTE',
|
||||
statut: 'TERMINEE',
|
||||
priorite: 'CRITIQUE',
|
||||
dateCreation: new Date('2024-12-18'),
|
||||
datePrevue: new Date('2024-12-19'),
|
||||
dateRealisation: new Date('2024-12-19'),
|
||||
technicien: 'Pierre Leroy',
|
||||
coutEstime: 1500,
|
||||
coutReel: 1650,
|
||||
dureeEstimee: 6,
|
||||
dureeReelle: 8,
|
||||
description: 'Remplacement pompe défaillante',
|
||||
localisation: 'Chantier Centre Commercial'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
materiel: 'Compacteur Bomag BW 213',
|
||||
type: 'PREDICTIVE',
|
||||
statut: 'PLANIFIEE',
|
||||
priorite: 'BASSE',
|
||||
dateCreation: new Date('2024-12-22'),
|
||||
datePrevue: new Date('2025-02-01'),
|
||||
technicien: 'Luc Bernard',
|
||||
coutEstime: 600,
|
||||
coutReel: 0,
|
||||
dureeEstimee: 3,
|
||||
description: 'Analyse vibratoire et diagnostic',
|
||||
localisation: 'Atelier Central'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
materiel: 'Nacelle Genie Z-45/25J',
|
||||
type: 'PREVENTIVE',
|
||||
statut: 'REPORTEE',
|
||||
priorite: 'MOYENNE',
|
||||
dateCreation: new Date('2024-12-10'),
|
||||
datePrevue: new Date('2024-12-25'),
|
||||
technicien: 'Sophie Dubois',
|
||||
coutEstime: 400,
|
||||
coutReel: 0,
|
||||
dureeEstimee: 2,
|
||||
description: 'Contrôle sécurité annuel',
|
||||
localisation: 'Atelier Central'
|
||||
}
|
||||
];
|
||||
|
||||
// Filtrer selon les critères sélectionnés
|
||||
let filteredMaintenances = mockMaintenances;
|
||||
|
||||
if (selectedType) {
|
||||
filteredMaintenances = filteredMaintenances.filter(m => m.type === selectedType);
|
||||
}
|
||||
|
||||
if (selectedStatut) {
|
||||
filteredMaintenances = filteredMaintenances.filter(m => m.statut === selectedStatut);
|
||||
}
|
||||
|
||||
setMaintenances(filteredMaintenances);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des maintenances:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Impossible de charger les données de maintenance'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const initCharts = () => {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
|
||||
// Graphique de répartition par type
|
||||
const typeData = {
|
||||
labels: ['Préventive', 'Corrective', 'Prédictive', 'Urgente'],
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
maintenances.filter(m => m.type === 'PREVENTIVE').length,
|
||||
maintenances.filter(m => m.type === 'CORRECTIVE').length,
|
||||
maintenances.filter(m => m.type === 'PREDICTIVE').length,
|
||||
maintenances.filter(m => m.type === 'URGENTE').length
|
||||
],
|
||||
backgroundColor: [
|
||||
documentStyle.getPropertyValue('--green-500'),
|
||||
documentStyle.getPropertyValue('--orange-500'),
|
||||
documentStyle.getPropertyValue('--blue-500'),
|
||||
documentStyle.getPropertyValue('--red-500')
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const options = {
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
color: documentStyle.getPropertyValue('--text-color')
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setChartData(typeData);
|
||||
setChartOptions(options);
|
||||
};
|
||||
|
||||
const initTimeline = () => {
|
||||
const events = maintenances
|
||||
.filter(m => m.statut === 'PLANIFIEE' || m.statut === 'EN_COURS')
|
||||
.sort((a, b) => a.datePrevue.getTime() - b.datePrevue.getTime())
|
||||
.slice(0, 5)
|
||||
.map(m => ({
|
||||
status: m.priorite === 'CRITIQUE' ? 'danger' : m.priorite === 'HAUTE' ? 'warning' : 'info',
|
||||
date: m.datePrevue.toLocaleDateString('fr-FR'),
|
||||
icon: m.type === 'URGENTE' ? 'pi pi-exclamation-triangle' : 'pi pi-wrench',
|
||||
color: m.priorite === 'CRITIQUE' ? '#e74c3c' : m.priorite === 'HAUTE' ? '#f39c12' : '#3498db',
|
||||
materiel: m.materiel,
|
||||
type: m.type,
|
||||
technicien: m.technicien
|
||||
}));
|
||||
|
||||
setTimelineEvents(events);
|
||||
};
|
||||
|
||||
const getStatutSeverity = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'PLANIFIEE': return 'info';
|
||||
case 'EN_COURS': return 'warning';
|
||||
case 'TERMINEE': return 'success';
|
||||
case 'REPORTEE': return 'warning';
|
||||
case 'ANNULEE': return 'danger';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const getPrioriteSeverity = (priorite: string) => {
|
||||
switch (priorite) {
|
||||
case 'CRITIQUE': return 'danger';
|
||||
case 'HAUTE': return 'warning';
|
||||
case 'MOYENNE': return 'info';
|
||||
case 'BASSE': return 'success';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeSeverity = (type: string) => {
|
||||
switch (type) {
|
||||
case 'URGENTE': return 'danger';
|
||||
case 'CORRECTIVE': return 'warning';
|
||||
case 'PREVENTIVE': return 'success';
|
||||
case 'PREDICTIVE': return 'info';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const statutBodyTemplate = (rowData: MaintenanceDashboard) => (
|
||||
<Tag value={rowData.statut} severity={getStatutSeverity(rowData.statut)} />
|
||||
);
|
||||
|
||||
const prioriteBodyTemplate = (rowData: MaintenanceDashboard) => (
|
||||
<Tag value={rowData.priorite} severity={getPrioriteSeverity(rowData.priorite)} />
|
||||
);
|
||||
|
||||
const typeBodyTemplate = (rowData: MaintenanceDashboard) => (
|
||||
<Tag value={rowData.type} severity={getTypeSeverity(rowData.type)} />
|
||||
);
|
||||
|
||||
const coutBodyTemplate = (rowData: MaintenanceDashboard) => {
|
||||
const ecart = rowData.coutReel - rowData.coutEstime;
|
||||
const couleur = ecart > 0 ? 'text-red-500' : ecart < 0 ? 'text-green-500' : 'text-gray-500';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="font-semibold">
|
||||
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.coutEstime)}
|
||||
</div>
|
||||
{rowData.coutReel > 0 && (
|
||||
<div className={`text-sm ${couleur}`}>
|
||||
Réel: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.coutReel)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const dureeBodyTemplate = (rowData: MaintenanceDashboard) => (
|
||||
<div>
|
||||
<div className="font-semibold">{rowData.dureeEstimee}h</div>
|
||||
{rowData.dureeReelle && (
|
||||
<div className={`text-sm ${rowData.dureeReelle > rowData.dureeEstimee ? 'text-red-500' : 'text-green-500'}`}>
|
||||
Réel: {rowData.dureeReelle}h
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const actionBodyTemplate = (rowData: MaintenanceDashboard) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
className="p-button-text p-button-sm"
|
||||
tooltip="Voir détails"
|
||||
onClick={() => router.push(`/maintenance/${rowData.id}`)}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
className="p-button-text p-button-sm"
|
||||
tooltip="Modifier"
|
||||
onClick={() => router.push(`/maintenance/${rowData.id}/edit`)}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-calendar"
|
||||
className="p-button-text p-button-sm"
|
||||
tooltip="Planifier"
|
||||
onClick={() => router.push(`/maintenance/planification?materiel=${rowData.materiel}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Calculs des métriques
|
||||
const totalCoutEstime = maintenances.reduce((sum, m) => sum + m.coutEstime, 0);
|
||||
const totalCoutReel = maintenances.reduce((sum, m) => sum + (m.coutReel || 0), 0);
|
||||
const maintenancesUrgentes = maintenances.filter(m => m.priorite === 'CRITIQUE' || m.type === 'URGENTE').length;
|
||||
const tauxRealisation = maintenances.length > 0 ?
|
||||
(maintenances.filter(m => m.statut === 'TERMINEE').length / maintenances.length) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<Toast ref={toast} />
|
||||
|
||||
{/* En-tête avec filtres */}
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<div className="flex justify-content-between align-items-center mb-4">
|
||||
<h2 className="text-2xl font-bold m-0">Dashboard Maintenance</h2>
|
||||
<Button
|
||||
label="Nouvelle maintenance"
|
||||
icon="pi pi-plus"
|
||||
onClick={() => router.push('/maintenance/nouveau')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 align-items-center">
|
||||
<div className="field">
|
||||
<label htmlFor="type" className="font-semibold">Type</label>
|
||||
<Dropdown
|
||||
id="type"
|
||||
value={selectedType}
|
||||
options={typeOptions}
|
||||
onChange={(e) => setSelectedType(e.value)}
|
||||
className="w-full md:w-14rem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="statut" className="font-semibold">Statut</label>
|
||||
<Dropdown
|
||||
id="statut"
|
||||
value={selectedStatut}
|
||||
options={statutOptions}
|
||||
onChange={(e) => setSelectedStatut(e.value)}
|
||||
className="w-full md:w-14rem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="dateRange" className="font-semibold">Plage de dates</label>
|
||||
<Calendar
|
||||
id="dateRange"
|
||||
value={dateRange}
|
||||
onChange={(e) => setDateRange(e.value as Date[])}
|
||||
selectionMode="range"
|
||||
readOnlyInput
|
||||
hideOnRangeSelection
|
||||
className="w-full md:w-14rem"
|
||||
placeholder="Sélectionner une période"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
className="p-button-outlined"
|
||||
onClick={loadMaintenances}
|
||||
loading={loading}
|
||||
tooltip="Actualiser"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Métriques principales */}
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Total Maintenances</span>
|
||||
<div className="text-900 font-medium text-xl">{maintenances.length}</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-blue-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-wrench text-blue-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Coût Total</span>
|
||||
<div className="text-900 font-medium text-xl">
|
||||
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(totalCoutEstime)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-green-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-euro text-green-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Urgentes</span>
|
||||
<div className="text-900 font-medium text-xl">{maintenancesUrgentes}</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-red-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-exclamation-triangle text-red-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Taux Réalisation</span>
|
||||
<div className="text-900 font-medium text-xl">{tauxRealisation.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-cyan-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-check-circle text-cyan-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Graphique et Timeline */}
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card>
|
||||
<h6>Répartition par Type</h6>
|
||||
<Chart type="doughnut" data={chartData} options={chartOptions} className="w-full md:w-30rem" />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card>
|
||||
<h6>Prochaines Maintenances</h6>
|
||||
<Timeline
|
||||
value={timelineEvents}
|
||||
align="alternate"
|
||||
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 title={item.materiel} subTitle={item.date}>
|
||||
<p className="text-sm">
|
||||
<strong>Type:</strong> {item.type}<br/>
|
||||
<strong>Technicien:</strong> {item.technicien}
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tableau des maintenances */}
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<div className="flex justify-content-between align-items-center mb-4">
|
||||
<h6>Liste des Maintenances ({maintenances.length})</h6>
|
||||
<Badge value={`${maintenances.filter(m => m.statut === 'EN_COURS').length} en cours`} severity="warning" />
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
value={maintenances}
|
||||
loading={loading}
|
||||
responsiveLayout="scroll"
|
||||
paginator
|
||||
rows={10}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
emptyMessage="Aucune maintenance trouvée"
|
||||
sortMode="multiple"
|
||||
>
|
||||
<Column field="materiel" header="Matériel" sortable />
|
||||
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
|
||||
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
||||
<Column field="priorite" header="Priorité" body={prioriteBodyTemplate} sortable />
|
||||
<Column field="datePrevue" header="Date prévue" body={(rowData) => rowData.datePrevue.toLocaleDateString('fr-FR')} sortable />
|
||||
<Column field="technicien" header="Technicien" sortable />
|
||||
<Column field="coutEstime" header="Coût" body={coutBodyTemplate} sortable />
|
||||
<Column field="dureeEstimee" header="Durée" body={dureeBodyTemplate} sortable />
|
||||
<Column header="Actions" body={actionBodyTemplate} style={{ width: '120px' }} />
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardMaintenance;
|
||||
Reference in New Issue
Block a user