315 lines
13 KiB
TypeScript
315 lines
13 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 { Toast } from 'primereact/toast';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Card } from 'primereact/card';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { FilterMatchMode } from 'primereact/api';
|
|
import { maintenanceService } from '../../../../services/api';
|
|
import { MaintenanceMateriel, StatutMaintenance } from '../../../../types/btp';
|
|
import { formatDate } from '../../../../utils/formatters';
|
|
|
|
const MaintenancesEnCoursPage = () => {
|
|
const [maintenances, setMaintenances] = useState<MaintenanceMateriel[]>([]);
|
|
const [selectedMaintenances, setSelectedMaintenances] = useState<MaintenanceMateriel[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const toast = useRef<Toast>(null);
|
|
const dt = useRef<DataTable<MaintenanceMateriel[]>>(null);
|
|
|
|
useEffect(() => {
|
|
loadMaintenancesEnCours();
|
|
}, []);
|
|
|
|
const loadMaintenancesEnCours = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await maintenanceService.getAll();
|
|
|
|
// Filtrer les maintenances en cours
|
|
const enCours = data.filter(m => m.statut === StatutMaintenance.EN_COURS);
|
|
setMaintenances(enCours);
|
|
} catch (error) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les maintenances en cours',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const terminerMaintenance = async (maintenance: MaintenanceMateriel) => {
|
|
try {
|
|
const updatedMaintenance = {
|
|
...maintenance,
|
|
statut: StatutMaintenance.TERMINEE,
|
|
dateRealisee: new Date().toISOString()
|
|
};
|
|
|
|
await maintenanceService.update(maintenance.id, updatedMaintenance);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Maintenance terminée',
|
|
life: 3000
|
|
});
|
|
|
|
loadMaintenancesEnCours();
|
|
} catch (error: any) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: error?.userMessage || 'Erreur lors de la finalisation',
|
|
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 dureeBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
if (!rowData.dateRealisee) return 'N/A';
|
|
|
|
const debut = new Date(rowData.dateRealisee);
|
|
const maintenant = new Date();
|
|
const diffHeures = Math.floor((maintenant.getTime() - debut.getTime()) / (1000 * 60 * 60));
|
|
|
|
return (
|
|
<div>
|
|
<div>{diffHeures}h</div>
|
|
<ProgressBar value={Math.min(100, (diffHeures / 24) * 100)} className="mt-1" style={{height: '4px'}} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const prioriteBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
const dateDebut = new Date(rowData.dateRealisee || rowData.datePrevue);
|
|
const heures = Math.floor((Date.now() - dateDebut.getTime()) / (1000 * 60 * 60));
|
|
|
|
if (heures > 48) {
|
|
return <Tag value="URGENT" severity="danger" />;
|
|
} else if (heures > 24) {
|
|
return <Tag value="PRIORITAIRE" severity="warning" />;
|
|
} else {
|
|
return <Tag value="NORMAL" severity="success" />;
|
|
}
|
|
};
|
|
|
|
const actionBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-check"
|
|
label="Terminer"
|
|
size="small"
|
|
severity="success"
|
|
className="p-button-text p-button-rounded"
|
|
onClick={() => terminerMaintenance(rowData)}
|
|
/>
|
|
<Button
|
|
icon="pi pi-eye"
|
|
size="small"
|
|
severity="info"
|
|
className="p-button-text p-button-rounded"
|
|
tooltip="Voir détails"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2 align-items-center">
|
|
<h4 className="m-0">Maintenances En Cours</h4>
|
|
<span className="text-500">({maintenances.length} maintenances actives)</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 align-items-center gap-2">
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
className="p-button-text p-button-rounded"
|
|
onClick={loadMaintenancesEnCours}
|
|
loading={loading}
|
|
/>
|
|
<span className="text-sm text-500">Dernière mise à jour: {new Date().toLocaleTimeString('fr-FR')}</span>
|
|
</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
|
|
const maintenancesUrgentes = maintenances.filter(m => {
|
|
const heures = Math.floor((Date.now() - new Date(m.dateRealisee || m.datePrevue).getTime()) / (1000 * 60 * 60));
|
|
return heures > 48;
|
|
}).length;
|
|
|
|
const maintenancesPrioritaires = maintenances.filter(m => {
|
|
const heures = Math.floor((Date.now() - new Date(m.dateRealisee || m.datePrevue).getTime()) / (1000 * 60 * 60));
|
|
return heures > 24 && heures <= 48;
|
|
}).length;
|
|
|
|
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-play-circle text-blue-500 text-3xl"></i>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-semibold">{maintenances.length}</div>
|
|
<div className="text-500">En cours</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">{maintenancesPrioritaires}</div>
|
|
<div className="text-500">Prioritaires</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">{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-clock text-green-500 text-3xl"></i>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-semibold">
|
|
{maintenances.length > 0
|
|
? Math.round(maintenances.reduce((acc, m) => {
|
|
const heures = Math.floor((Date.now() - new Date(m.dateRealisee || m.datePrevue).getTime()) / (1000 * 60 * 60));
|
|
return acc + heures;
|
|
}, 0) / maintenances.length)
|
|
: 0
|
|
}h
|
|
</div>
|
|
<div className="text-500">Durée moyenne</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card">
|
|
<Toolbar
|
|
className="mb-4"
|
|
left={leftToolbarTemplate}
|
|
right={rightToolbarTemplate}
|
|
/>
|
|
|
|
<DataTable
|
|
ref={dt}
|
|
value={maintenances}
|
|
selection={selectedMaintenances}
|
|
onSelectionChange={(e) => setSelectedMaintenances(e.value)}
|
|
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 en cours."
|
|
sortField="dateRealisee"
|
|
sortOrder={-1}
|
|
>
|
|
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
|
|
<Column header="Matériel" body={materielBodyTemplate} />
|
|
<Column header="Priorité" body={prioriteBodyTemplate} />
|
|
<Column header="Durée" body={dureeBodyTemplate} />
|
|
<Column field="description" header="Description" />
|
|
<Column field="dateRealisee" header="Démarrée le" body={(data) => formatDate(data.dateRealisee)} sortable />
|
|
<Column body={actionBodyTemplate} exportable={false} style={{ minWidth: '8rem' }} />
|
|
</DataTable>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MaintenancesEnCoursPage;
|
|
|