- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts - Ajout des propriétés manquantes aux objets User mockés - Conversion des dates de string vers objets Date - Correction des appels asynchrones et des types incompatibles - Ajout de dynamic rendering pour résoudre les erreurs useSearchParams - Enveloppement de useSearchParams dans Suspense boundary - Configuration de force-dynamic au niveau du layout principal Build réussi: 126 pages générées avec succès 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
453 lines
18 KiB
TypeScript
453 lines
18 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
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;
|
|
|