Initial commit
This commit is contained in:
536
app/(main)/materiels/page.tsx
Normal file
536
app/(main)/materiels/page.tsx
Normal file
@@ -0,0 +1,536 @@
|
||||
'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 { materielService } from '../../../services/api';
|
||||
import { Materiel, TypeMateriel, StatutMateriel } from '../../../types/btp';
|
||||
import { formatDate, formatCurrency } from '../../../utils/formatters';
|
||||
|
||||
const MaterielsPage = () => {
|
||||
const [materiels, setMateriels] = useState<Materiel[]>([]);
|
||||
const [materiel, setMateriel] = useState<Partial<Materiel>>({});
|
||||
const [selectedMateriels, setSelectedMateriels] = useState<Materiel[]>([]);
|
||||
const [materielDialog, setMaterielDialog] = 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<Materiel[]>>(null);
|
||||
|
||||
const typeOptions = Object.values(TypeMateriel).map(type => ({
|
||||
label: type.replace('_', ' '),
|
||||
value: type
|
||||
}));
|
||||
|
||||
const statutOptions = Object.values(StatutMateriel).map(statut => ({
|
||||
label: statut.replace('_', ' '),
|
||||
value: statut
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
loadMateriels();
|
||||
}, []);
|
||||
|
||||
const loadMateriels = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await materielService.getAll();
|
||||
setMateriels(data);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des matériels:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Impossible de charger les matériels',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openNew = () => {
|
||||
setMateriel({
|
||||
statut: StatutMateriel.DISPONIBLE,
|
||||
actif: true,
|
||||
quantiteStock: 0,
|
||||
seuilMinimum: 0
|
||||
});
|
||||
setSubmitted(false);
|
||||
setMaterielDialog(true);
|
||||
};
|
||||
|
||||
const hideDialog = () => {
|
||||
setSubmitted(false);
|
||||
setMaterielDialog(false);
|
||||
setMateriel({});
|
||||
};
|
||||
|
||||
const saveMateriel = async () => {
|
||||
setSubmitted(true);
|
||||
|
||||
if (materiel.nom?.trim()) {
|
||||
try {
|
||||
let savedMateriel: Materiel;
|
||||
|
||||
if (materiel.id) {
|
||||
savedMateriel = await materielService.update(materiel.id, materiel);
|
||||
const updatedMateriels = materiels.map(m =>
|
||||
m.id === materiel.id ? savedMateriel : m
|
||||
);
|
||||
setMateriels(updatedMateriels);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Succès',
|
||||
detail: 'Matériel mis à jour',
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
savedMateriel = await materielService.create(materiel);
|
||||
setMateriels([...materiels, savedMateriel]);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Succès',
|
||||
detail: 'Matériel créé',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
|
||||
hideDialog();
|
||||
} catch (error: any) {
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: error?.userMessage || 'Erreur lors de la sauvegarde',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const editMateriel = (materiel: Materiel) => {
|
||||
setMateriel({ ...materiel });
|
||||
setMaterielDialog(true);
|
||||
};
|
||||
|
||||
const confirmDeleteSelected = () => {
|
||||
if (selectedMateriels && selectedMateriels.length > 0) {
|
||||
// Implémenter la suppression en lot si nécessaire
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMateriel = async (materiel: Materiel) => {
|
||||
try {
|
||||
await materielService.delete(materiel.id);
|
||||
const updatedMateriels = materiels.filter(m => m.id !== materiel.id);
|
||||
setMateriels(updatedMateriels);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Succès',
|
||||
detail: 'Matériel supprimé',
|
||||
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 _materiel = { ...materiel };
|
||||
(_materiel as any)[name] = val;
|
||||
setMateriel(_materiel);
|
||||
};
|
||||
|
||||
const onInputNumberChange = (value: number | null, name: string) => {
|
||||
const _materiel = { ...materiel };
|
||||
(_materiel as any)[name] = value;
|
||||
setMateriel(_materiel);
|
||||
};
|
||||
|
||||
const onDropdownChange = (e: any, name: string) => {
|
||||
const _materiel = { ...materiel };
|
||||
(_materiel as any)[name] = e.value;
|
||||
setMateriel(_materiel);
|
||||
};
|
||||
|
||||
const onDateChange = (e: any, name: string) => {
|
||||
const _materiel = { ...materiel };
|
||||
(_materiel as any)[name] = e.value;
|
||||
setMateriel(_materiel);
|
||||
};
|
||||
|
||||
// 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={getStatutSeverity(rowData.statut)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const valeurBodyTemplate = (rowData: Materiel) => {
|
||||
return formatCurrency(rowData.valeurActuelle || rowData.valeurAchat);
|
||||
};
|
||||
|
||||
const dateBodyTemplate = (rowData: Materiel) => {
|
||||
return formatDate(rowData.dateAchat);
|
||||
};
|
||||
|
||||
const actionBodyTemplate = (rowData: Materiel) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
className="p-button-text p-button-rounded"
|
||||
onClick={() => editMateriel(rowData)}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
className="p-button-text p-button-rounded"
|
||||
severity="danger"
|
||||
onClick={() => deleteMateriel(rowData)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 getStatutSeverity = (statut?: StatutMateriel) => {
|
||||
switch (statut) {
|
||||
case StatutMateriel.DISPONIBLE:
|
||||
return 'success';
|
||||
case StatutMateriel.EN_UTILISATION:
|
||||
return 'warning';
|
||||
case StatutMateriel.EN_MAINTENANCE:
|
||||
return 'info';
|
||||
case StatutMateriel.HORS_SERVICE:
|
||||
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={!selectedMateriels || selectedMateriels.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 du Matériel</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 materielDialogFooter = (
|
||||
<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={saveMateriel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const filters = {
|
||||
global: { value: globalFilter, matchMode: FilterMatchMode.CONTAINS }
|
||||
};
|
||||
|
||||
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={materiels}
|
||||
selection={selectedMateriels}
|
||||
onSelectionChange={(e) => setSelectedMateriels(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} matériels"
|
||||
filters={filters}
|
||||
filterDisplay="menu"
|
||||
loading={loading}
|
||||
globalFilterFields={['nom', 'marque', 'modele', 'numeroSerie', 'type', 'statut']}
|
||||
header={header}
|
||||
emptyMessage="Aucun matériel trouvé."
|
||||
>
|
||||
<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="numeroSerie" header="N° Série" sortable />
|
||||
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
|
||||
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
||||
<Column field="valeurActuelle" header="Valeur" body={valeurBodyTemplate} sortable />
|
||||
<Column field="dateAchat" header="Date Achat" body={dateBodyTemplate} sortable />
|
||||
<Column field="localisation" header="Localisation" sortable />
|
||||
<Column body={actionBodyTemplate} exportable={false} style={{ minWidth: '8rem' }} />
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
visible={materielDialog}
|
||||
style={{ width: '800px' }}
|
||||
header="Détails du matériel"
|
||||
modal
|
||||
className="p-fluid"
|
||||
footer={materielDialogFooter}
|
||||
onHide={hideDialog}
|
||||
>
|
||||
<div className="formgrid grid">
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="nom" className="font-bold">
|
||||
Nom <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
id="nom"
|
||||
value={materiel.nom || ''}
|
||||
onChange={(e) => onInputChange(e, 'nom')}
|
||||
required
|
||||
autoFocus
|
||||
className={submitted && !materiel.nom ? 'p-invalid' : ''}
|
||||
/>
|
||||
{submitted && !materiel.nom && <small className="p-error">Le nom est obligatoire.</small>}
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="type" className="font-bold">
|
||||
Type <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Dropdown
|
||||
id="type"
|
||||
value={materiel.type}
|
||||
onChange={(e) => onDropdownChange(e, 'type')}
|
||||
options={typeOptions}
|
||||
placeholder="Sélectionner un type"
|
||||
className={submitted && !materiel.type ? 'p-invalid' : ''}
|
||||
/>
|
||||
{submitted && !materiel.type && <small className="p-error">Le type est obligatoire.</small>}
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="marque" className="font-bold">Marque</label>
|
||||
<InputText
|
||||
id="marque"
|
||||
value={materiel.marque || ''}
|
||||
onChange={(e) => onInputChange(e, 'marque')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="modele" className="font-bold">Modèle</label>
|
||||
<InputText
|
||||
id="modele"
|
||||
value={materiel.modele || ''}
|
||||
onChange={(e) => onInputChange(e, 'modele')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="numeroSerie" className="font-bold">Numéro de série</label>
|
||||
<InputText
|
||||
id="numeroSerie"
|
||||
value={materiel.numeroSerie || ''}
|
||||
onChange={(e) => onInputChange(e, 'numeroSerie')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="statut" className="font-bold">Statut</label>
|
||||
<Dropdown
|
||||
id="statut"
|
||||
value={materiel.statut}
|
||||
onChange={(e) => onDropdownChange(e, 'statut')}
|
||||
options={statutOptions}
|
||||
placeholder="Sélectionner un statut"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="dateAchat" className="font-bold">Date d'achat</label>
|
||||
<Calendar
|
||||
id="dateAchat"
|
||||
value={materiel.dateAchat ? new Date(materiel.dateAchat) : null}
|
||||
onChange={(e) => onDateChange(e, 'dateAchat')}
|
||||
dateFormat="dd/mm/yy"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="valeurAchat" className="font-bold">Valeur d'achat</label>
|
||||
<InputNumber
|
||||
id="valeurAchat"
|
||||
value={materiel.valeurAchat}
|
||||
onValueChange={(e) => onInputNumberChange(e.value, 'valeurAchat')}
|
||||
mode="currency"
|
||||
currency="EUR"
|
||||
locale="fr-FR"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="valeurActuelle" className="font-bold">Valeur actuelle</label>
|
||||
<InputNumber
|
||||
id="valeurActuelle"
|
||||
value={materiel.valeurActuelle}
|
||||
onValueChange={(e) => onInputNumberChange(e.value, 'valeurActuelle')}
|
||||
mode="currency"
|
||||
currency="EUR"
|
||||
locale="fr-FR"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="localisation" className="font-bold">Localisation</label>
|
||||
<InputText
|
||||
id="localisation"
|
||||
value={materiel.localisation || ''}
|
||||
onChange={(e) => onInputChange(e, 'localisation')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="proprietaire" className="font-bold">Propriétaire</label>
|
||||
<InputText
|
||||
id="proprietaire"
|
||||
value={materiel.proprietaire || ''}
|
||||
onChange={(e) => onInputChange(e, 'proprietaire')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="quantiteStock" className="font-bold">Quantité en stock</label>
|
||||
<InputNumber
|
||||
id="quantiteStock"
|
||||
value={materiel.quantiteStock}
|
||||
onValueChange={(e) => onInputNumberChange(e.value, 'quantiteStock')}
|
||||
min={0}
|
||||
showButtons
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12">
|
||||
<label htmlFor="description" className="font-bold">Description</label>
|
||||
<InputTextarea
|
||||
id="description"
|
||||
value={materiel.description || ''}
|
||||
onChange={(e) => onInputChange(e, 'description')}
|
||||
rows={3}
|
||||
cols={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterielsPage;
|
||||
Reference in New Issue
Block a user