Files
btpxpress-frontend/app/(main)/materiels/disponibles/page.tsx

334 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 { Calendar } from 'primereact/calendar';
import { Dropdown } from 'primereact/dropdown';
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 } from '../../../../services/api';
import { Materiel, TypeMateriel, StatutMateriel } from '../../../../types/btp';
import { formatDate, formatCurrency } from '../../../../utils/formatters';
const MaterielsDisponiblesPage = () => {
const [materiels, setMateriels] = useState<Materiel[]>([]);
const [selectedMateriels, setSelectedMateriels] = useState<Materiel[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [dateDebut, setDateDebut] = useState<Date | null>(new Date());
const [dateFin, setDateFin] = useState<Date | null>(new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)); // +7 jours
const [typeFilter, setTypeFilter] = useState<string>('');
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<Materiel[]>>(null);
const typeOptions = [
{ label: 'Tous les types', value: '' },
...Object.values(TypeMateriel).map(type => ({
label: type.replace('_', ' '),
value: type
}))
];
useEffect(() => {
loadMaterielsDisponibles();
}, [dateDebut, dateFin, typeFilter]);
const loadMaterielsDisponibles = async () => {
try {
setLoading(true);
const dateDebutStr = dateDebut ? dateDebut.toISOString().split('T')[0] : undefined;
const dateFinStr = dateFin ? dateFin.toISOString().split('T')[0] : undefined;
const data = await materielService.getDisponibles(dateDebutStr, dateFinStr, typeFilter || undefined);
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 disponibles',
life: 3000
});
} finally {
setLoading(false);
}
};
const reserverMateriel = async (materiel: Materiel) => {
if (!dateDebut || !dateFin) {
toast.current?.show({
severity: 'warn',
summary: 'Attention',
detail: 'Veuillez sélectionner une période',
life: 3000
});
return;
}
try {
await materielService.reserver(
materiel.id,
dateDebut.toISOString().split('T')[0],
dateFin.toISOString().split('T')[0]
);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: `Matériel ${materiel.nom} réservé`,
life: 3000
});
// Recharger la liste
loadMaterielsDisponibles();
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors de la réservation',
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 statutBodyTemplate = (rowData: Materiel) => {
return (
<Tag
value={rowData.statut?.replace('_', ' ')}
severity="success"
/>
);
};
const valeurBodyTemplate = (rowData: Materiel) => {
return formatCurrency(rowData.valeurActuelle || rowData.valeurAchat);
};
const actionBodyTemplate = (rowData: Materiel) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-calendar-plus"
label="Réserver"
size="small"
className="p-button-text p-button-rounded"
onClick={() => reserverMateriel(rowData)}
/>
</div>
);
};
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">Matériels Disponibles</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">
<Calendar
placeholder="Date début"
value={dateDebut}
onChange={(e) => setDateDebut(e.value)}
dateFormat="dd/mm/yy"
showIcon
/>
<Calendar
placeholder="Date fin"
value={dateFin}
onChange={(e) => setDateFin(e.value)}
dateFormat="dd/mm/yy"
showIcon
/>
<Dropdown
value={typeFilter}
onChange={(e) => setTypeFilter(e.value)}
options={typeOptions}
placeholder="Tous les types"
style={{ minWidth: '150px' }}
/>
</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 }
};
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>Critères de disponibilité</h5>
<div className="flex flex-wrap gap-3 align-items-center">
<div className="flex flex-column gap-2">
<label htmlFor="dateDebut">Date début</label>
<Calendar
id="dateDebut"
value={dateDebut}
onChange={(e) => setDateDebut(e.value)}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<div className="flex flex-column gap-2">
<label htmlFor="dateFin">Date fin</label>
<Calendar
id="dateFin"
value={dateFin}
onChange={(e) => setDateFin(e.value)}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<div className="flex flex-column gap-2">
<label htmlFor="typeFilter">Type</label>
<Dropdown
id="typeFilter"
value={typeFilter}
onChange={(e) => setTypeFilter(e.value)}
options={typeOptions}
style={{ minWidth: '150px' }}
/>
</div>
</div>
</div>
<div className="col-12 md:col-6">
<h5>Statistiques</h5>
<div className="grid text-center">
<div className="col-6">
<div className="surface-card p-3 border-round">
<div className="text-2xl font-semibold text-green-500">{materiels.length}</div>
<div className="text-500">Disponibles</div>
</div>
</div>
<div className="col-6">
<div className="surface-card p-3 border-round">
<div className="text-2xl font-semibold text-blue-500">
{materiels.reduce((sum, m) => sum + (m.valeurActuelle || m.valeurAchat || 0), 0).toLocaleString('fr-FR', {
style: 'currency',
currency: 'EUR'
})}
</div>
<div className="text-500">Valeur totale</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 disponibles"
filters={filters}
filterDisplay="menu"
loading={loading}
globalFilterFields={['nom', 'marque', 'modele', 'type', 'localisation']}
header={header}
emptyMessage="Aucun matériel disponible pour cette période."
>
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
<Column field="nom" header="Nom" sortable />
<Column field="marque" header="Marque" sortable />
<Column field="modele" header="Modèle" sortable />
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
<Column field="localisation" header="Localisation" sortable />
<Column field="valeurActuelle" header="Valeur" body={valeurBodyTemplate} sortable />
<Column body={actionBodyTemplate} exportable={false} style={{ minWidth: '8rem' }} />
</DataTable>
</div>
</div>
</div>
);
};
export default MaterielsDisponiblesPage;