Files
btpxpress-frontend/app/(main)/maintenances/planifiees/page.tsx

451 lines
18 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 { InputText } from 'primereact/inputtext';
import { Calendar } from 'primereact/calendar';
import { Dropdown } from 'primereact/dropdown';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Tag } from 'primereact/tag';
import { Card } from 'primereact/card';
import { FilterMatchMode } from 'primereact/api';
import { Timeline } from 'primereact/timeline';
import { maintenanceService } from '../../../../services/api';
import { MaintenanceMateriel, TypeMaintenance, StatutMaintenance } from '../../../../types/btp';
import { formatDate } from '../../../../utils/formatters';
const MaintenancesPlanifieesPage = () => {
const [maintenances, setMaintenances] = useState<MaintenanceMateriel[]>([]);
const [selectedMaintenances, setSelectedMaintenances] = useState<MaintenanceMateriel[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [dateDebut, setDateDebut] = useState<Date | null>(new Date());
const [dateFin, setDateFin] = useState<Date | null>(new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)); // +30 jours
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<MaintenanceMateriel[]>>(null);
useEffect(() => {
loadMaintenancesPlanifiees();
}, [dateDebut, dateFin]);
const loadMaintenancesPlanifiees = async () => {
try {
setLoading(true);
const data = await maintenanceService.getAll();
// Filtrer les maintenances planifiées
const planifiees = data.filter(m =>
m.statut === StatutMaintenance.PLANIFIEE &&
new Date(m.datePrevue) >= (dateDebut || new Date()) &&
new Date(m.datePrevue) <= (dateFin || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000))
);
setMaintenances(planifiees);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les maintenances planifiées',
life: 3000
});
} finally {
setLoading(false);
}
};
const demarrerMaintenance = async (maintenance: MaintenanceMateriel) => {
try {
const updatedMaintenance = {
...maintenance,
statut: StatutMaintenance.EN_COURS,
dateRealisee: new Date().toISOString()
};
await maintenanceService.update(maintenance.id, updatedMaintenance);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Maintenance démarrée',
life: 3000
});
loadMaintenancesPlanifiees();
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors du démarrage',
life: 3000
});
}
};
const reporterMaintenance = async (maintenance: MaintenanceMateriel, nouveauJours: number = 7) => {
try {
const nouvelleDate = new Date();
nouvelleDate.setDate(nouvelleDate.getDate() + nouveauJours);
const updatedMaintenance = {
...maintenance,
datePrevue: nouvelleDate.toISOString(),
notes: `${maintenance.notes || ''}\nReportée le ${formatDate(new Date())}`
};
await maintenanceService.update(maintenance.id, updatedMaintenance);
toast.current?.show({
severity: 'info',
summary: 'Maintenance reportée',
detail: `Nouvelle date: ${formatDate(nouvelleDate)}`,
life: 3000
});
loadMaintenancesPlanifiees();
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors du report',
life: 3000
});
}
};
const exportCSV = () => {
dt.current?.exportCSV();
};
// Templates pour les colonnes
const materielBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<div>
<div className="font-semibold">{rowData.materiel?.nom}</div>
<div className="text-sm text-500">{rowData.materiel?.marque} {rowData.materiel?.modele}</div>
</div>
);
};
const typeBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<Tag
value={rowData.type?.replace('_', ' ')}
severity={getTypeSeverity(rowData.type)}
/>
);
};
const prioriteBodyTemplate = (rowData: MaintenanceMateriel) => {
const daysDiff = Math.ceil((new Date(rowData.datePrevue).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
let severity;
let label;
if (daysDiff < 0) {
severity = 'danger';
label = 'EN RETARD';
} else if (daysDiff <= 3) {
severity = 'warning';
label = 'URGENT';
} else if (daysDiff <= 7) {
severity = 'info';
label = 'PRIORITAIRE';
} else {
severity = 'success';
label = 'NORMAL';
}
return <Tag value={label} severity={severity} />;
};
const dateBodyTemplate = (rowData: MaintenanceMateriel) => {
const date = new Date(rowData.datePrevue);
const today = new Date();
const daysDiff = Math.ceil((date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
return (
<div>
<div className={daysDiff < 0 ? 'text-red-500 font-semibold' : daysDiff <= 3 ? 'text-orange-500' : ''}>
{formatDate(rowData.datePrevue)}
</div>
<div className="text-xs text-500">
{daysDiff < 0 ? `${Math.abs(daysDiff)} jour(s) de retard` :
daysDiff === 0 ? 'Aujourd\'hui' :
`Dans ${daysDiff} jour(s)`}
</div>
</div>
);
};
const actionBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<div className="flex gap-1">
<Button
icon="pi pi-play"
size="small"
severity="success"
className="p-button-text p-button-rounded"
tooltip="Démarrer"
onClick={() => demarrerMaintenance(rowData)}
/>
<Button
icon="pi pi-calendar-plus"
size="small"
severity="info"
className="p-button-text p-button-rounded"
tooltip="Reporter (+7 jours)"
onClick={() => reporterMaintenance(rowData, 7)}
/>
<Button
icon="pi pi-calendar-times"
size="small"
severity="warning"
className="p-button-text p-button-rounded"
tooltip="Reporter (+14 jours)"
onClick={() => reporterMaintenance(rowData, 14)}
/>
</div>
);
};
const getTypeSeverity = (type?: TypeMaintenance) => {
switch (type) {
case TypeMaintenance.PREVENTIVE:
return 'success';
case TypeMaintenance.CORRECTIVE:
return 'danger';
case TypeMaintenance.REVISION:
return 'info';
case TypeMaintenance.CONTROLE_TECHNIQUE:
return 'warning';
case TypeMaintenance.NETTOYAGE:
return 'secondary';
default:
return undefined;
}
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2 align-items-center">
<h4 className="m-0">Maintenances Planifiées</h4>
<span className="text-500">({maintenances.length} maintenances)</span>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
className="p-button-text p-button-rounded"
onClick={exportCSV}
/>
);
};
const header = (
<div className="flex flex-wrap gap-2 align-items-center justify-content-between">
<div className="flex flex-wrap gap-2 align-items-center">
<Calendar
placeholder="Date début"
value={dateDebut}
onChange={(e) => setDateDebut(e.value)}
dateFormat="dd/mm/yy"
showIcon
/>
<Calendar
placeholder="Date fin"
value={dateFin}
onChange={(e) => setDateFin(e.value)}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
</div>
);
const filters = {
global: { value: globalFilter, matchMode: FilterMatchMode.CONTAINS }
};
// Statistiques pour les cards
const maintenancesUrgentes = maintenances.filter(m => {
const daysDiff = Math.ceil((new Date(m.datePrevue).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
return daysDiff <= 3;
}).length;
const maintenancesRetard = maintenances.filter(m => {
const daysDiff = Math.ceil((new Date(m.datePrevue).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
return daysDiff < 0;
}).length;
const maintenancesSemaine = maintenances.filter(m => {
const daysDiff = Math.ceil((new Date(m.datePrevue).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
return daysDiff >= 0 && daysDiff <= 7;
}).length;
// Timeline des prochaines maintenances
const timelineEvents = maintenances
.sort((a, b) => new Date(a.datePrevue).getTime() - new Date(b.datePrevue).getTime())
.slice(0, 5)
.map(m => ({
date: formatDate(m.datePrevue),
icon: 'pi pi-wrench',
color: new Date(m.datePrevue) < new Date() ? '#e74c3c' : '#3498db',
content: (
<div>
<div className="font-semibold">{m.materiel?.nom}</div>
<div className="text-sm text-500">{m.description}</div>
<Tag value={m.type?.replace('_', ' ')} severity={getTypeSeverity(m.type)} className="mt-1" />
</div>
)
}));
return (
<div className="grid">
<div className="col-12">
<Toast ref={toast} />
{/* Cards de statistiques */}
<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-clock text-blue-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenances.length}</div>
<div className="text-500">Planifiées</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-exclamation-triangle text-orange-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenancesUrgentes}</div>
<div className="text-500">Urgentes</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-times-circle text-red-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenancesRetard}</div>
<div className="text-500">En retard</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-calendar text-green-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenancesSemaine}</div>
<div className="text-500">Cette semaine</div>
</div>
</div>
</Card>
</div>
</div>
<div className="grid">
<div className="col-12 lg:col-8">
<div className="card">
<Toolbar
className="mb-4"
left={leftToolbarTemplate}
right={rightToolbarTemplate}
/>
<DataTable
ref={dt}
value={maintenances}
selection={selectedMaintenances}
onSelectionChange={(e) => setSelectedMaintenances(e.value)}
selectionMode="checkbox"
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage {first} à {last} de {totalRecords} maintenances"
filters={filters}
filterDisplay="menu"
loading={loading}
globalFilterFields={['description', 'materiel.nom', 'type']}
header={header}
emptyMessage="Aucune maintenance planifiée."
sortField="datePrevue"
sortOrder={1}
>
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
<Column header="Matériel" body={materielBodyTemplate} />
<Column field="type" header="Type" body={typeBodyTemplate} />
<Column header="Priorité" body={prioriteBodyTemplate} />
<Column field="datePrevue" header="Date prévue" body={dateBodyTemplate} sortable />
<Column field="description" header="Description" />
<Column body={actionBodyTemplate} exportable={false} style={{ minWidth: '8rem' }} />
</DataTable>
</div>
</div>
<div className="col-12 lg:col-4">
<Card title="Prochaines maintenances">
<Timeline
value={timelineEvents}
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) => item.content}
opposite={(item) => <small className="text-color-secondary">{item.date}</small>}
/>
</Card>
</div>
</div>
</div>
</div>
);
};
export default MaintenancesPlanifieesPage;