344 lines
14 KiB
TypeScript
344 lines
14 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 { InputNumber } from 'primereact/inputnumber';
|
|
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 { materielService, maintenanceService } from '../../../../services/api';
|
|
import { Materiel, MaintenanceMateriel, TypeMateriel, TypeMaintenance } from '../../../../types/btp';
|
|
import { formatDate } from '../../../../utils/formatters';
|
|
|
|
const MaintenancePrevuePage = () => {
|
|
const [materiels, setMateriels] = useState<Materiel[]>([]);
|
|
const [selectedMateriels, setSelectedMateriels] = useState<Materiel[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const [jours, setJours] = useState<number>(30);
|
|
const toast = useRef<Toast>(null);
|
|
const dt = useRef<DataTable<Materiel[]>>(null);
|
|
|
|
useEffect(() => {
|
|
loadMaterielsMaintenancePrevue();
|
|
}, [jours]);
|
|
|
|
const loadMaterielsMaintenancePrevue = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await materielService.getMaintenancePrevue(jours);
|
|
setMateriels(data);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les matériels nécessitant une maintenance',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const programmerMaintenance = async (materiel: Materiel) => {
|
|
try {
|
|
const maintenanceData: Partial<MaintenanceMateriel> = {
|
|
materiel: materiel,
|
|
type: TypeMaintenance.PREVENTIVE,
|
|
description: `Maintenance préventive programmée pour ${materiel.nom}`,
|
|
datePrevue: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // Dans 7 jours
|
|
};
|
|
|
|
await maintenanceService.create(maintenanceData);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: `Maintenance programmée pour ${materiel.nom}`,
|
|
life: 3000
|
|
});
|
|
|
|
// Recharger la liste
|
|
loadMaterielsMaintenancePrevue();
|
|
} catch (error: any) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: error?.userMessage || 'Erreur lors de la programmation',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const exportCSV = () => {
|
|
dt.current?.exportCSV();
|
|
};
|
|
|
|
// Templates pour les colonnes
|
|
const typeBodyTemplate = (rowData: Materiel) => {
|
|
return (
|
|
<Tag
|
|
value={rowData.type?.replace('_', ' ')}
|
|
severity={getTypeSeverity(rowData.type)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const urgenceBodyTemplate = (rowData: Materiel) => {
|
|
// Logique pour déterminer l'urgence basée sur les maintenances
|
|
const derniereMaintenance = rowData.maintenances?.[0];
|
|
if (!derniereMaintenance) {
|
|
return <Tag value="CRITIQUE" severity="danger" />;
|
|
}
|
|
|
|
const daysSinceLastMaintenance = Math.floor(
|
|
(Date.now() - new Date(derniereMaintenance.dateRealisation || derniereMaintenance.datePrevue).getTime()) / (1000 * 60 * 60 * 24)
|
|
);
|
|
|
|
if (daysSinceLastMaintenance > 90) {
|
|
return <Tag value="CRITIQUE" severity="danger" />;
|
|
} else if (daysSinceLastMaintenance > 60) {
|
|
return <Tag value="URGENT" severity="warning" />;
|
|
} else {
|
|
return <Tag value="NORMAL" severity="success" />;
|
|
}
|
|
};
|
|
|
|
const derniereMaintenanceBodyTemplate = (rowData: Materiel) => {
|
|
const derniereMaintenance = rowData.maintenances?.[0];
|
|
if (!derniereMaintenance) {
|
|
return <span className="text-red-500">Aucune</span>;
|
|
}
|
|
return formatDate(derniereMaintenance.dateRealisation || derniereMaintenance.datePrevue);
|
|
};
|
|
|
|
const actionBodyTemplate = (rowData: Materiel) => {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-calendar-plus"
|
|
label="Programmer"
|
|
size="small"
|
|
className="p-button-text p-button-rounded"
|
|
onClick={() => programmerMaintenance(rowData)}
|
|
/>
|
|
<Button
|
|
icon="pi pi-exclamation-triangle"
|
|
label="Signaler panne"
|
|
size="small"
|
|
severity="warning"
|
|
className="p-button-text p-button-rounded"
|
|
onClick={() => signalerPanne(rowData)}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const signalerPanne = async (materiel: Materiel) => {
|
|
try {
|
|
const maintenanceData: Partial<MaintenanceMateriel> = {
|
|
materiel: materiel,
|
|
type: TypeMaintenance.CORRECTIVE,
|
|
description: `Panne signalée sur ${materiel.nom}`,
|
|
datePrevue: new Date() // Immédiatement
|
|
};
|
|
|
|
await maintenanceService.create(maintenanceData);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: `Panne signalée pour ${materiel.nom}`,
|
|
life: 3000
|
|
});
|
|
} catch (error: any) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: error?.userMessage || 'Erreur lors du signalement',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const getTypeSeverity = (type?: TypeMateriel) => {
|
|
switch (type) {
|
|
case TypeMateriel.ENGIN_CHANTIER:
|
|
return 'danger';
|
|
case TypeMateriel.OUTILLAGE:
|
|
return 'warning';
|
|
case TypeMateriel.EQUIPEMENT_SECURITE:
|
|
return 'success';
|
|
case TypeMateriel.VEHICULE:
|
|
return 'info';
|
|
default:
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2 align-items-center">
|
|
<h4 className="m-0">Maintenance Prévue</h4>
|
|
<span className="text-500">({materiels.length} matériels)</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">
|
|
<label htmlFor="jours">Horizon (jours):</label>
|
|
<InputNumber
|
|
id="jours"
|
|
value={jours}
|
|
onValueChange={(e) => setJours(e.value || 30)}
|
|
min={1}
|
|
max={365}
|
|
showButtons
|
|
style={{ width: '120px' }}
|
|
/>
|
|
</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 }
|
|
};
|
|
|
|
// Calculs des statistiques
|
|
const materielsUrgents = materiels.filter(m => {
|
|
const derniereMaintenance = m.maintenances?.[0];
|
|
if (!derniereMaintenance) return true;
|
|
const daysSince = Math.floor(
|
|
(Date.now() - new Date(derniereMaintenance.dateRealisation || derniereMaintenance.datePrevue).getTime()) / (1000 * 60 * 60 * 24)
|
|
);
|
|
return daysSince > 60;
|
|
}).length;
|
|
|
|
const materielsCritiques = materiels.filter(m => {
|
|
const derniereMaintenance = m.maintenances?.[0];
|
|
if (!derniereMaintenance) return true;
|
|
const daysSince = Math.floor(
|
|
(Date.now() - new Date(derniereMaintenance.dateRealisation || derniereMaintenance.datePrevue).getTime()) / (1000 * 60 * 60 * 24)
|
|
);
|
|
return daysSince > 90;
|
|
}).length;
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Toast ref={toast} />
|
|
|
|
<Card>
|
|
<div className="mb-4">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<h5>Paramètres de recherche</h5>
|
|
<div className="flex flex-wrap gap-3 align-items-center">
|
|
<div className="flex flex-column gap-2">
|
|
<label htmlFor="joursInput">Horizon de maintenance (jours)</label>
|
|
<InputNumber
|
|
id="joursInput"
|
|
value={jours}
|
|
onValueChange={(e) => setJours(e.value || 30)}
|
|
min={1}
|
|
max={365}
|
|
showButtons
|
|
/>
|
|
<small className="text-500">
|
|
Matériels nécessitant une maintenance dans les {jours} prochains jours
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-6">
|
|
<h5>Urgences</h5>
|
|
<div className="grid text-center">
|
|
<div className="col-6">
|
|
<div className="surface-card p-3 border-round border-left-3 border-orange-500">
|
|
<div className="text-2xl font-semibold text-orange-500">{materielsUrgents}</div>
|
|
<div className="text-500">Urgents</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="surface-card p-3 border-round border-left-3 border-red-500">
|
|
<div className="text-2xl font-semibold text-red-500">{materielsCritiques}</div>
|
|
<div className="text-500">Critiques</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
<div className="card">
|
|
<Toolbar
|
|
className="mb-4"
|
|
left={leftToolbarTemplate}
|
|
right={rightToolbarTemplate}
|
|
/>
|
|
|
|
<DataTable
|
|
ref={dt}
|
|
value={materiels}
|
|
selection={selectedMateriels}
|
|
onSelectionChange={(e) => setSelectedMateriels(e.value)}
|
|
dataKey="id"
|
|
paginator
|
|
rows={10}
|
|
rowsPerPageOptions={[5, 10, 25, 50]}
|
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
|
currentPageReportTemplate="Affichage {first} à {last} de {totalRecords} matériels"
|
|
filters={filters}
|
|
filterDisplay="menu"
|
|
loading={loading}
|
|
globalFilterFields={['nom', 'marque', 'modele', 'type', 'localisation']}
|
|
header={header}
|
|
emptyMessage="Aucun matériel nécessitant une maintenance."
|
|
>
|
|
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
|
|
<Column field="nom" header="Nom" sortable />
|
|
<Column field="marque" header="Marque" sortable />
|
|
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
|
|
<Column field="localisation" header="Localisation" sortable />
|
|
<Column header="Urgence" body={urgenceBodyTemplate} />
|
|
<Column header="Dernière maintenance" body={derniereMaintenanceBodyTemplate} />
|
|
<Column body={actionBodyTemplate} exportable={false} style={{ minWidth: '12rem' }} />
|
|
</DataTable>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MaintenancePrevuePage;
|
|
|