Initial commit
This commit is contained in:
342
app/(main)/materiels/maintenance-prevue/page.tsx
Normal file
342
app/(main)/materiels/maintenance-prevue/page.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user