511 lines
20 KiB
TypeScript
511 lines
20 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 { Dialog } from 'primereact/dialog';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { InputNumber } from 'primereact/inputnumber';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { InputTextarea } from 'primereact/inputtextarea';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { ConfirmDialog } from 'primereact/confirmdialog';
|
|
import { Tag } from 'primereact/tag';
|
|
import { FilterMatchMode } from 'primereact/api';
|
|
import { maintenanceService, materielService } from '../../../services/api';
|
|
import { MaintenanceMateriel, Materiel, TypeMaintenance, StatutMaintenance } from '../../../types/btp';
|
|
import { formatDate, formatCurrency } from '../../../utils/formatters';
|
|
|
|
const MaintenancesPage = () => {
|
|
const [maintenances, setMaintenances] = useState<MaintenanceMateriel[]>([]);
|
|
const [maintenance, setMaintenance] = useState<Partial<MaintenanceMateriel>>({});
|
|
const [materiels, setMateriels] = useState<Materiel[]>([]);
|
|
const [selectedMaintenances, setSelectedMaintenances] = useState<MaintenanceMateriel[]>([]);
|
|
const [maintenanceDialog, setMaintenanceDialog] = useState(false);
|
|
const [loading, setLoading] = useState(true);
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const [submitted, setSubmitted] = useState(false);
|
|
const toast = useRef<Toast>(null);
|
|
const dt = useRef<DataTable<MaintenanceMateriel[]>>(null);
|
|
|
|
const typeOptions = Object.values(TypeMaintenance).map(type => ({
|
|
label: type.replace('_', ' '),
|
|
value: type
|
|
}));
|
|
|
|
const statutOptions = Object.values(StatutMaintenance).map(statut => ({
|
|
label: statut.replace('_', ' '),
|
|
value: statut
|
|
}));
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [maintenancesData, materielsData] = await Promise.all([
|
|
maintenanceService.getAll(),
|
|
materielService.getAll()
|
|
]);
|
|
|
|
setMaintenances(maintenancesData);
|
|
setMateriels(materielsData);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les données',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const openNew = () => {
|
|
setMaintenance({
|
|
type: TypeMaintenance.PREVENTIVE,
|
|
statut: StatutMaintenance.PLANIFIEE,
|
|
datePrevue: new Date().toISOString(),
|
|
cout: 0
|
|
});
|
|
setSubmitted(false);
|
|
setMaintenanceDialog(true);
|
|
};
|
|
|
|
const hideDialog = () => {
|
|
setSubmitted(false);
|
|
setMaintenanceDialog(false);
|
|
setMaintenance({});
|
|
};
|
|
|
|
const saveMaintenance = async () => {
|
|
setSubmitted(true);
|
|
|
|
if (maintenance.description?.trim() && maintenance.materiel) {
|
|
try {
|
|
let savedMaintenance: MaintenanceMateriel;
|
|
|
|
if (maintenance.id) {
|
|
savedMaintenance = await maintenanceService.update(maintenance.id, maintenance);
|
|
const updatedMaintenances = maintenances.map(m =>
|
|
m.id === maintenance.id ? savedMaintenance : m
|
|
);
|
|
setMaintenances(updatedMaintenances);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Maintenance mise à jour',
|
|
life: 3000
|
|
});
|
|
} else {
|
|
savedMaintenance = await maintenanceService.create(maintenance);
|
|
setMaintenances([...maintenances, savedMaintenance]);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Maintenance créée',
|
|
life: 3000
|
|
});
|
|
}
|
|
|
|
hideDialog();
|
|
} catch (error: any) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: error?.userMessage || 'Erreur lors de la sauvegarde',
|
|
life: 3000
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const editMaintenance = (maintenance: MaintenanceMateriel) => {
|
|
setMaintenance({ ...maintenance });
|
|
setMaintenanceDialog(true);
|
|
};
|
|
|
|
const confirmDeleteSelected = () => {
|
|
if (selectedMaintenances && selectedMaintenances.length > 0) {
|
|
// Implémenter la suppression en lot si nécessaire
|
|
}
|
|
};
|
|
|
|
const deleteMaintenance = async (maintenance: MaintenanceMateriel) => {
|
|
try {
|
|
await maintenanceService.delete(maintenance.id);
|
|
const updatedMaintenances = maintenances.filter(m => m.id !== maintenance.id);
|
|
setMaintenances(updatedMaintenances);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Maintenance supprimée',
|
|
life: 3000
|
|
});
|
|
} catch (error: any) {
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: error?.userMessage || 'Erreur lors de la suppression',
|
|
life: 3000
|
|
});
|
|
}
|
|
};
|
|
|
|
const exportCSV = () => {
|
|
dt.current?.exportCSV();
|
|
};
|
|
|
|
const onInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string) => {
|
|
const val = (e.target && e.target.value) || '';
|
|
const _maintenance = { ...maintenance };
|
|
(_maintenance as any)[name] = val;
|
|
setMaintenance(_maintenance);
|
|
};
|
|
|
|
const onInputNumberChange = (value: number | null, name: string) => {
|
|
const _maintenance = { ...maintenance };
|
|
(_maintenance as any)[name] = value;
|
|
setMaintenance(_maintenance);
|
|
};
|
|
|
|
const onDropdownChange = (e: any, name: string) => {
|
|
const _maintenance = { ...maintenance };
|
|
(_maintenance as any)[name] = e.value;
|
|
setMaintenance(_maintenance);
|
|
};
|
|
|
|
const onDateChange = (e: any, name: string) => {
|
|
const _maintenance = { ...maintenance };
|
|
(_maintenance as any)[name] = e.value;
|
|
setMaintenance(_maintenance);
|
|
};
|
|
|
|
// Templates pour les colonnes
|
|
const materielBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
return rowData.materiel?.nom || 'N/A';
|
|
};
|
|
|
|
const typeBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
return (
|
|
<Tag
|
|
value={rowData.type?.replace('_', ' ')}
|
|
severity={getTypeSeverity(rowData.type)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const statutBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
return (
|
|
<Tag
|
|
value={rowData.statut?.replace('_', ' ')}
|
|
severity={getStatutSeverity(rowData.statut)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const coutBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
return formatCurrency(rowData.cout);
|
|
};
|
|
|
|
const dateBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
return formatDate(rowData.datePrevue);
|
|
};
|
|
|
|
const dateRealisationBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
return formatDate(rowData.dateRealisee);
|
|
};
|
|
|
|
const actionBodyTemplate = (rowData: MaintenanceMateriel) => {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-pencil"
|
|
className="p-button-text p-button-rounded"
|
|
onClick={() => editMaintenance(rowData)}
|
|
/>
|
|
<Button
|
|
icon="pi pi-trash"
|
|
className="p-button-text p-button-rounded"
|
|
severity="danger"
|
|
onClick={() => deleteMaintenance(rowData)}
|
|
/>
|
|
</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 getStatutSeverity = (statut?: StatutMaintenance) => {
|
|
switch (statut) {
|
|
case StatutMaintenance.PLANIFIEE:
|
|
return 'info';
|
|
case StatutMaintenance.EN_COURS:
|
|
return 'warning';
|
|
case StatutMaintenance.TERMINEE:
|
|
return 'success';
|
|
case StatutMaintenance.ANNULEE:
|
|
return 'danger';
|
|
default:
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
label="Nouveau"
|
|
icon="pi pi-plus"
|
|
severity="success"
|
|
className="mr-2 p-button-text p-button-rounded"
|
|
onClick={openNew}
|
|
/>
|
|
<Button
|
|
label="Supprimer"
|
|
icon="pi pi-trash"
|
|
severity="danger"
|
|
className="p-button-text p-button-rounded"
|
|
onClick={confirmDeleteSelected}
|
|
disabled={!selectedMaintenances || selectedMaintenances.length === 0}
|
|
/>
|
|
</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">
|
|
<h4 className="m-0">Gestion des Maintenances</h4>
|
|
<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 maintenanceDialogFooter = (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
label="Annuler"
|
|
icon="pi pi-times"
|
|
className="p-button-text p-button-rounded"
|
|
onClick={hideDialog}
|
|
/>
|
|
<Button
|
|
label="Sauvegarder"
|
|
icon="pi pi-check"
|
|
className="p-button-text p-button-rounded"
|
|
onClick={saveMaintenance}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
const filters = {
|
|
global: { value: globalFilter, matchMode: FilterMatchMode.CONTAINS }
|
|
};
|
|
|
|
const materielOptions = materiels.map(materiel => ({
|
|
label: materiel.nom,
|
|
value: materiel
|
|
}));
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Toast ref={toast} />
|
|
<ConfirmDialog />
|
|
|
|
<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', 'type', 'statut', 'materiel.nom']}
|
|
header={header}
|
|
emptyMessage="Aucune maintenance trouvée."
|
|
>
|
|
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
|
|
<Column field="materiel.nom" header="Matériel" body={materielBodyTemplate} sortable />
|
|
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
|
|
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
|
<Column field="description" header="Description" sortable />
|
|
<Column field="datePrevue" header="Date prévue" body={dateBodyTemplate} sortable />
|
|
<Column field="dateRealisation" header="Date réalisation" body={dateRealisationBodyTemplate} sortable />
|
|
<Column field="cout" header="Coût" body={coutBodyTemplate} sortable />
|
|
<Column body={actionBodyTemplate} exportable={false} style={{ minWidth: '8rem' }} />
|
|
</DataTable>
|
|
</div>
|
|
|
|
<Dialog
|
|
visible={maintenanceDialog}
|
|
style={{ width: '800px' }}
|
|
header="Détails de la maintenance"
|
|
modal
|
|
className="p-fluid"
|
|
footer={maintenanceDialogFooter}
|
|
onHide={hideDialog}
|
|
>
|
|
<div className="formgrid grid">
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="materiel" className="font-bold">
|
|
Matériel <span className="text-red-500">*</span>
|
|
</label>
|
|
<Dropdown
|
|
id="materiel"
|
|
value={maintenance.materiel}
|
|
onChange={(e) => onDropdownChange(e, 'materiel')}
|
|
options={materielOptions}
|
|
placeholder="Sélectionner un matériel"
|
|
className={submitted && !maintenance.materiel ? 'p-invalid' : ''}
|
|
/>
|
|
{submitted && !maintenance.materiel && <small className="p-error">Le matériel est obligatoire.</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="type" className="font-bold">Type</label>
|
|
<Dropdown
|
|
id="type"
|
|
value={maintenance.type}
|
|
onChange={(e) => onDropdownChange(e, 'type')}
|
|
options={typeOptions}
|
|
placeholder="Sélectionner un type"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="statut" className="font-bold">Statut</label>
|
|
<Dropdown
|
|
id="statut"
|
|
value={maintenance.statut}
|
|
onChange={(e) => onDropdownChange(e, 'statut')}
|
|
options={statutOptions}
|
|
placeholder="Sélectionner un statut"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="datePrevue" className="font-bold">Date prévue</label>
|
|
<Calendar
|
|
id="datePrevue"
|
|
value={maintenance.datePrevue ? new Date(maintenance.datePrevue) : null}
|
|
onChange={(e) => onDateChange(e, 'datePrevue')}
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="dateRealisee" className="font-bold">Date de réalisation</label>
|
|
<Calendar
|
|
id="dateRealisee"
|
|
value={maintenance.dateRealisee ? new Date(maintenance.dateRealisee) : null}
|
|
onChange={(e) => onDateChange(e, 'dateRealisee')}
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="cout" className="font-bold">Coût</label>
|
|
<InputNumber
|
|
id="cout"
|
|
value={maintenance.cout}
|
|
onValueChange={(e) => onInputNumberChange(e.value, 'cout')}
|
|
mode="currency"
|
|
currency="EUR"
|
|
locale="fr-FR"
|
|
min={0}
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="description" className="font-bold">
|
|
Description <span className="text-red-500">*</span>
|
|
</label>
|
|
<InputTextarea
|
|
id="description"
|
|
value={maintenance.description || ''}
|
|
onChange={(e) => onInputChange(e, 'description')}
|
|
rows={3}
|
|
cols={20}
|
|
className={submitted && !maintenance.description ? 'p-invalid' : ''}
|
|
/>
|
|
{submitted && !maintenance.description && <small className="p-error">La description est obligatoire.</small>}
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="notes" className="font-bold">Notes</label>
|
|
<InputTextarea
|
|
id="notes"
|
|
value={maintenance.notes || ''}
|
|
onChange={(e) => onInputChange(e, 'notes')}
|
|
rows={2}
|
|
cols={20}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MaintenancesPage; |