Files
btpxpress-frontend/app/(main)/materiels/maintenance-prevue/page.tsx
dahoud a8825a058b Fix: Corriger toutes les erreurs de build du frontend
- 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>
2025-10-18 13:23:08 +00:00

351 lines
14 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 { 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).toISOString() // 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.dateRealisee || 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.dateRealisee || 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().toISOString() // 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.OUTIL_ELECTRIQUE:
case TypeMateriel.OUTIL_MANUEL:
return 'warning';
case TypeMateriel.EQUIPEMENT_SECURITE:
return 'success';
case TypeMateriel.VEHICULE:
return 'info';
case TypeMateriel.GRUE:
case TypeMateriel.BETONIERE:
return 'danger';
default:
return 'secondary';
}
};
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.dateRealisee || 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.dateRealisee || 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)}
selectionMode="checkbox"
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;