Initial commit
This commit is contained in:
223
app/(main)/materiels/by-type/page.tsx
Normal file
223
app/(main)/materiels/by-type/page.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
'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 { Dropdown } from 'primereact/dropdown';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { materielService } from '../../../../services/api';
|
||||
import { Materiel, TypeMateriel, StatutMateriel } from '../../../../types/btp';
|
||||
import { formatCurrency } from '../../../../utils/formatters';
|
||||
|
||||
const MaterielsByTypePage = () => {
|
||||
const [materiels, setMateriels] = useState<Materiel[]>([]);
|
||||
const [selectedType, setSelectedType] = useState<TypeMateriel>(TypeMateriel.VEHICULE);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const toast = useRef<Toast>(null);
|
||||
const dt = useRef<DataTable<Materiel[]>>(null);
|
||||
|
||||
const typeOptions = Object.values(TypeMateriel).map(type => ({
|
||||
label: type.replace('_', ' '),
|
||||
value: type
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedType) {
|
||||
loadMaterielsByType();
|
||||
}
|
||||
}, [selectedType]);
|
||||
|
||||
const loadMaterielsByType = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await materielService.getByType(selectedType);
|
||||
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',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const exportCSV = () => {
|
||||
dt.current?.exportCSV();
|
||||
};
|
||||
|
||||
const statutBodyTemplate = (rowData: Materiel) => {
|
||||
const getStatutSeverity = (statut?: StatutMateriel) => {
|
||||
switch (statut) {
|
||||
case StatutMateriel.DISPONIBLE:
|
||||
return 'success';
|
||||
case StatutMateriel.UTILISE:
|
||||
return 'warning';
|
||||
case StatutMateriel.MAINTENANCE:
|
||||
return 'info';
|
||||
case StatutMateriel.HORS_SERVICE:
|
||||
return 'danger';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
value={rowData.statut?.replace('_', ' ')}
|
||||
severity={getStatutSeverity(rowData.statut)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const valeurBodyTemplate = (rowData: Materiel) => {
|
||||
return formatCurrency(rowData.valeurActuelle || rowData.valeurAchat);
|
||||
};
|
||||
|
||||
const leftToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<h4 className="m-0">Matériels par Type</h4>
|
||||
<Dropdown
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.value)}
|
||||
options={typeOptions}
|
||||
placeholder="Sélectionner un type"
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Statistiques par statut
|
||||
const stats = materiels.reduce((acc, materiel) => {
|
||||
acc[materiel.statut] = (acc[materiel.statut] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<StatutMateriel, number>);
|
||||
|
||||
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-check-circle text-green-500 text-3xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">
|
||||
{stats[StatutMateriel.DISPONIBLE] || 0}
|
||||
</div>
|
||||
<div className="text-500">Disponibles</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-play-circle text-orange-500 text-3xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">
|
||||
{stats[StatutMateriel.UTILISE] || 0}
|
||||
</div>
|
||||
<div className="text-500">En utilisation</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-wrench text-blue-500 text-3xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">
|
||||
{stats[StatutMateriel.MAINTENANCE] || 0}
|
||||
</div>
|
||||
<div className="text-500">En maintenance</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">
|
||||
{stats[StatutMateriel.HORS_SERVICE] || 0}
|
||||
</div>
|
||||
<div className="text-500">Hors service</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<Toolbar
|
||||
className="mb-4"
|
||||
left={leftToolbarTemplate}
|
||||
right={rightToolbarTemplate}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
ref={dt}
|
||||
value={materiels}
|
||||
loading={loading}
|
||||
paginator
|
||||
rows={10}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
currentPageReportTemplate="Affichage {first} à {last} de {totalRecords} matériels"
|
||||
emptyMessage="Aucun matériel trouvé pour ce type."
|
||||
sortField="nom"
|
||||
sortOrder={1}
|
||||
>
|
||||
<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" />
|
||||
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
||||
<Column field="localisation" header="Localisation" sortable />
|
||||
<Column field="valeurActuelle" header="Valeur" body={valeurBodyTemplate} sortable />
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterielsByTypePage;
|
||||
327
app/(main)/materiels/disponibles/page.tsx
Normal file
327
app/(main)/materiels/disponibles/page.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
'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.OUTILLAGE:
|
||||
return 'warning';
|
||||
case TypeMateriel.EQUIPEMENT_SECURITE:
|
||||
return 'success';
|
||||
case TypeMateriel.VEHICULE:
|
||||
return 'info';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
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)}
|
||||
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;
|
||||
342
app/(main)/materiels/maintenance-prevue/page.tsx
Normal file
342
app/(main)/materiels/maintenance-prevue/page.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
'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 { 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) // 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.dateRealisation || 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.dateRealisation || 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() // 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.OUTILLAGE:
|
||||
return 'warning';
|
||||
case TypeMateriel.EQUIPEMENT_SECURITE:
|
||||
return 'success';
|
||||
case TypeMateriel.VEHICULE:
|
||||
return 'info';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
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.dateRealisation || 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.dateRealisation || 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)}
|
||||
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;
|
||||
294
app/(main)/materiels/nouveau/page.tsx
Normal file
294
app/(main)/materiels/nouveau/page.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
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 { materielService } from '../../../../services/api';
|
||||
import { TypeMateriel, StatutMateriel } from '../../../../types/btp';
|
||||
import { useApiCall } from '../../../../hooks/useApiCall';
|
||||
|
||||
const NouveauMaterielPage = () => {
|
||||
const router = useRouter();
|
||||
const [materiel, setMateriel] = useState({
|
||||
nom: '',
|
||||
marque: '',
|
||||
modele: '',
|
||||
numeroSerie: '',
|
||||
type: '' as TypeMateriel,
|
||||
description: '',
|
||||
dateAchat: null as Date | null,
|
||||
valeurAchat: 0,
|
||||
valeurActuelle: 0,
|
||||
statut: StatutMateriel.DISPONIBLE,
|
||||
localisation: '',
|
||||
proprietaire: '',
|
||||
quantiteStock: 0,
|
||||
seuilMinimum: 0,
|
||||
unite: 'unité',
|
||||
actif: true
|
||||
});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
// Utilisation du hook pour la création de matériel avec gestion d'erreurs automatique
|
||||
const createMaterielCall = useApiCall(
|
||||
(data: any) => materielService.create(data),
|
||||
{
|
||||
showSuccessToast: true,
|
||||
successMessage: 'Matériel créé avec succès',
|
||||
retryAttempts: 2,
|
||||
retryDelay: 2000,
|
||||
onSuccess: () => {
|
||||
// Rediriger vers la liste après succès
|
||||
setTimeout(() => {
|
||||
router.push('/materiels');
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const typeOptions = Object.values(TypeMateriel).map(type => ({
|
||||
label: type.replace('_', ' '),
|
||||
value: type
|
||||
}));
|
||||
|
||||
const statutOptions = Object.values(StatutMateriel).map(statut => ({
|
||||
label: statut.replace('_', ' '),
|
||||
value: statut
|
||||
}));
|
||||
|
||||
const onInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string) => {
|
||||
const val = (e.target && e.target.value) || '';
|
||||
setMateriel(prev => ({ ...prev, [name]: val }));
|
||||
};
|
||||
|
||||
const onInputNumberChange = (value: number | null, name: string) => {
|
||||
setMateriel(prev => ({ ...prev, [name]: value || 0 }));
|
||||
};
|
||||
|
||||
const onDropdownChange = (e: any, name: string) => {
|
||||
setMateriel(prev => ({ ...prev, [name]: e.value }));
|
||||
};
|
||||
|
||||
const onDateChange = (e: any, name: string) => {
|
||||
setMateriel(prev => ({ ...prev, [name]: e.value }));
|
||||
};
|
||||
|
||||
const saveMateriel = async () => {
|
||||
setSubmitted(true);
|
||||
|
||||
if (materiel.nom?.trim() && materiel.type) {
|
||||
await createMaterielCall.execute(materiel);
|
||||
}
|
||||
};
|
||||
|
||||
const annuler = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Toast ref={createMaterielCall.toast} />
|
||||
|
||||
<div className="flex align-items-center justify-content-between mb-4">
|
||||
<h2 className="m-0">Nouveau Matériel</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
label="Annuler"
|
||||
icon="pi pi-times"
|
||||
className="p-button-text p-button-rounded"
|
||||
onClick={annuler}
|
||||
/>
|
||||
<Button
|
||||
label="Sauvegarder"
|
||||
icon="pi pi-check"
|
||||
className="p-button-text p-button-rounded"
|
||||
onClick={saveMateriel}
|
||||
loading={createMaterielCall.loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="p-fluid 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}
|
||||
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"
|
||||
min={0}
|
||||
/>
|
||||
</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"
|
||||
min={0}
|
||||
/>
|
||||
</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 md:col-6">
|
||||
<label htmlFor="seuilMinimum" className="font-bold">Seuil minimum</label>
|
||||
<InputNumber
|
||||
id="seuilMinimum"
|
||||
value={materiel.seuilMinimum}
|
||||
onValueChange={(e) => onInputNumberChange(e.value, 'seuilMinimum')}
|
||||
min={0}
|
||||
showButtons
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="unite" className="font-bold">Unité</label>
|
||||
<InputText
|
||||
id="unite"
|
||||
value={materiel.unite}
|
||||
onChange={(e) => onInputChange(e, 'unite')}
|
||||
placeholder="ex: unité, kg, m, etc."
|
||||
/>
|
||||
</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}
|
||||
placeholder="Description détaillée du matériel..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NouveauMaterielPage;
|
||||
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;
|
||||
258
app/(main)/materiels/search/page.tsx
Normal file
258
app/(main)/materiels/search/page.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { FilterMatchMode } from 'primereact/api';
|
||||
import { materielService } from '../../../../services/api';
|
||||
import { Materiel, TypeMateriel, StatutMateriel } from '../../../../types/btp';
|
||||
import { formatCurrency } from '../../../../utils/formatters';
|
||||
|
||||
const RechercheMaterielsPage = () => {
|
||||
const [materiels, setMateriels] = useState<Materiel[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
nom: '',
|
||||
type: '',
|
||||
marque: '',
|
||||
statut: '',
|
||||
localisation: ''
|
||||
});
|
||||
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
|
||||
}))
|
||||
];
|
||||
|
||||
const statutOptions = [
|
||||
{ label: 'Tous les statuts', value: '' },
|
||||
...Object.values(StatutMateriel).map(statut => ({
|
||||
label: statut.replace('_', ' '),
|
||||
value: statut
|
||||
}))
|
||||
];
|
||||
|
||||
const rechercher = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await materielService.search(searchParams);
|
||||
setMateriels(data);
|
||||
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Recherche effectuée',
|
||||
detail: `${data.length} résultat(s) trouvé(s)`,
|
||||
life: 3000
|
||||
});
|
||||
} catch (error) {
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Erreur lors de la recherche',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const exportCSV = () => {
|
||||
dt.current?.exportCSV();
|
||||
};
|
||||
|
||||
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>, name: string) => {
|
||||
const val = (e.target && e.target.value) || '';
|
||||
setSearchParams(prev => ({ ...prev, [name]: val }));
|
||||
};
|
||||
|
||||
const onDropdownChange = (e: any, name: string) => {
|
||||
setSearchParams(prev => ({ ...prev, [name]: e.value }));
|
||||
};
|
||||
|
||||
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 getTypeSeverity = (type?: TypeMateriel) => {
|
||||
switch (type) {
|
||||
case TypeMateriel.ENGIN_CHANTIER:
|
||||
return 'danger';
|
||||
case TypeMateriel.OUTIL_ELECTRIQUE:
|
||||
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.UTILISE:
|
||||
return 'warning';
|
||||
case StatutMateriel.MAINTENANCE:
|
||||
return 'info';
|
||||
case StatutMateriel.HORS_SERVICE:
|
||||
return 'danger';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Toast ref={toast} />
|
||||
|
||||
<h2 className="mb-4">Recherche de Matériel</h2>
|
||||
|
||||
<Card className="mb-4">
|
||||
<h5>Critères de recherche</h5>
|
||||
<div className="formgrid grid">
|
||||
<div className="field col-12 md:col-4">
|
||||
<label htmlFor="nom">Nom</label>
|
||||
<InputText
|
||||
id="nom"
|
||||
value={searchParams.nom}
|
||||
onChange={(e) => onInputChange(e, 'nom')}
|
||||
placeholder="Nom du matériel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-4">
|
||||
<label htmlFor="marque">Marque</label>
|
||||
<InputText
|
||||
id="marque"
|
||||
value={searchParams.marque}
|
||||
onChange={(e) => onInputChange(e, 'marque')}
|
||||
placeholder="Marque"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-4">
|
||||
<label htmlFor="localisation">Localisation</label>
|
||||
<InputText
|
||||
id="localisation"
|
||||
value={searchParams.localisation}
|
||||
onChange={(e) => onInputChange(e, 'localisation')}
|
||||
placeholder="Localisation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="type">Type</label>
|
||||
<Dropdown
|
||||
id="type"
|
||||
value={searchParams.type}
|
||||
onChange={(e) => onDropdownChange(e, 'type')}
|
||||
options={typeOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field col-12 md:col-6">
|
||||
<label htmlFor="statut">Statut</label>
|
||||
<Dropdown
|
||||
id="statut"
|
||||
value={searchParams.statut}
|
||||
onChange={(e) => onDropdownChange(e, 'statut')}
|
||||
options={statutOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
label="Rechercher"
|
||||
icon="pi pi-search"
|
||||
className="p-button-text p-button-rounded"
|
||||
onClick={rechercher}
|
||||
loading={loading}
|
||||
/>
|
||||
<Button
|
||||
label="Effacer"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
className="p-button-text p-button-rounded"
|
||||
onClick={() => setSearchParams({
|
||||
nom: '',
|
||||
type: '',
|
||||
marque: '',
|
||||
statut: '',
|
||||
localisation: ''
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{materiels.length > 0 && (
|
||||
<Card>
|
||||
<div className="flex justify-content-between align-items-center mb-4">
|
||||
<h5>Résultats ({materiels.length})</h5>
|
||||
<Button
|
||||
label="Exporter"
|
||||
icon="pi pi-upload"
|
||||
severity="help"
|
||||
className="p-button-text p-button-rounded"
|
||||
onClick={exportCSV}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
ref={dt}
|
||||
value={materiels}
|
||||
paginator
|
||||
rows={10}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
currentPageReportTemplate="Affichage {first} à {last} de {totalRecords} matériels"
|
||||
emptyMessage="Aucun résultat trouvé."
|
||||
>
|
||||
<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 />
|
||||
</DataTable>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RechercheMaterielsPage;
|
||||
374
app/(main)/materiels/stats/page.tsx
Normal file
374
app/(main)/materiels/stats/page.tsx
Normal file
@@ -0,0 +1,374 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Chart } from 'primereact/chart';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Divider } from 'primereact/divider';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { materielService, maintenanceService } from '../../../../services/api';
|
||||
import { Materiel, MaintenanceMateriel, TypeMateriel, StatutMateriel } from '../../../../types/btp';
|
||||
import { formatCurrency } from '../../../../utils/formatters';
|
||||
|
||||
const MaterielsStatsPage = () => {
|
||||
const [materiels, setMateriels] = useState<Materiel[]>([]);
|
||||
const [maintenances, setMaintenances] = useState<MaintenanceMateriel[]>([]);
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [valeurTotale, setValeurTotale] = useState<number>(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const toast = useRef<Toast>(null);
|
||||
|
||||
// Options des graphiques
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [materielsData, maintenancesData, statsData, valeurData] = await Promise.all([
|
||||
materielService.getAll(),
|
||||
maintenanceService.getAll(),
|
||||
materielService.getStats(),
|
||||
materielService.getValeurTotale()
|
||||
]);
|
||||
|
||||
setMateriels(materielsData);
|
||||
setMaintenances(maintenancesData);
|
||||
setStats(statsData);
|
||||
setValeurTotale(valeurData);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Impossible de charger les statistiques',
|
||||
life: 3000
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Calculs des statistiques
|
||||
const getStatutStats = () => {
|
||||
const statutCounts = materiels.reduce((acc, materiel) => {
|
||||
acc[materiel.statut] = (acc[materiel.statut] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<StatutMateriel, number>);
|
||||
|
||||
return {
|
||||
labels: Object.keys(statutCounts).map(s => s.replace('_', ' ')),
|
||||
datasets: [{
|
||||
label: 'Matériels par statut',
|
||||
data: Object.values(statutCounts),
|
||||
backgroundColor: [
|
||||
'#4CAF50', // DISPONIBLE
|
||||
'#FF9800', // EN_UTILISATION
|
||||
'#2196F3', // EN_MAINTENANCE
|
||||
'#f44336' // HORS_SERVICE
|
||||
]
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const getTypeStats = () => {
|
||||
const typeCounts = materiels.reduce((acc, materiel) => {
|
||||
acc[materiel.type] = (acc[materiel.type] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<TypeMateriel, number>);
|
||||
|
||||
return {
|
||||
labels: Object.keys(typeCounts).map(t => t.replace('_', ' ')),
|
||||
datasets: [{
|
||||
label: 'Matériels par type',
|
||||
data: Object.values(typeCounts),
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56',
|
||||
'#4BC0C0',
|
||||
'#9966FF',
|
||||
'#FF9F40'
|
||||
]
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const getValeurStats = () => {
|
||||
const valeurParType = materiels.reduce((acc, materiel) => {
|
||||
const valeur = materiel.valeurActuelle || materiel.valeurAchat || 0;
|
||||
acc[materiel.type] = (acc[materiel.type] || 0) + valeur;
|
||||
return acc;
|
||||
}, {} as Record<TypeMateriel, number>);
|
||||
|
||||
return {
|
||||
labels: Object.keys(valeurParType).map(t => t.replace('_', ' ')),
|
||||
datasets: [{
|
||||
label: 'Valeur par type (€)',
|
||||
data: Object.values(valeurParType),
|
||||
backgroundColor: '#42A5F5',
|
||||
borderColor: '#1976D2',
|
||||
borderWidth: 1
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const getMaintenanceStats = () => {
|
||||
const monthlyMaintenance = new Array(12).fill(0);
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
maintenances.forEach(maintenance => {
|
||||
const date = new Date(maintenance.dateRealisation || maintenance.datePrevue);
|
||||
if (date.getFullYear() === currentYear) {
|
||||
monthlyMaintenance[date.getMonth()]++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
labels: [
|
||||
'Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun',
|
||||
'Jul', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'
|
||||
],
|
||||
datasets: [{
|
||||
label: 'Maintenances par mois',
|
||||
data: monthlyMaintenance,
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.2)',
|
||||
borderColor: '#FFC107',
|
||||
borderWidth: 2,
|
||||
fill: true
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
// Template pour les matériels les plus coûteux
|
||||
const valeurBodyTemplate = (rowData: Materiel) => {
|
||||
return formatCurrency(rowData.valeurActuelle || rowData.valeurAchat);
|
||||
};
|
||||
|
||||
const typeBodyTemplate = (rowData: Materiel) => {
|
||||
return (
|
||||
<Tag
|
||||
value={rowData.type?.replace('_', ' ')}
|
||||
severity={getTypeSeverity(rowData.type)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// Matériels triés par valeur décroissante
|
||||
const materielsCouteux = [...materiels]
|
||||
.sort((a, b) => (b.valeurActuelle || b.valeurAchat || 0) - (a.valeurActuelle || a.valeurAchat || 0))
|
||||
.slice(0, 5);
|
||||
|
||||
// Calcul des KPI
|
||||
const tauxDisponibilite = materiels.length > 0
|
||||
? (materiels.filter(m => m.statut === StatutMateriel.DISPONIBLE).length / materiels.length) * 100
|
||||
: 0;
|
||||
|
||||
const tauxMaintenance = materiels.length > 0
|
||||
? (materiels.filter(m => m.statut === StatutMateriel.EN_MAINTENANCE).length / materiels.length) * 100
|
||||
: 0;
|
||||
|
||||
const tauxUtilisation = materiels.length > 0
|
||||
? (materiels.filter(m => m.statut === StatutMateriel.EN_UTILISATION).length / materiels.length) * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Toast ref={toast} />
|
||||
|
||||
<div className="flex align-items-center justify-content-between mb-4">
|
||||
<h2 className="m-0">Statistiques du Parc Matériel</h2>
|
||||
<Button
|
||||
label="Actualiser"
|
||||
icon="pi pi-refresh"
|
||||
className="p-button-text p-button-rounded"
|
||||
onClick={loadData}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<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-cog text-blue-500 text-3xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">{materiels.length}</div>
|
||||
<div className="text-500">Total matériels</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-euro text-green-500 text-3xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">{formatCurrency(valeurTotale)}</div>
|
||||
<div className="text-500">Valeur totale</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-check-circle text-green-500 text-3xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">{tauxDisponibilite.toFixed(1)}%</div>
|
||||
<div className="text-500">Disponibilité</div>
|
||||
<ProgressBar value={tauxDisponibilite} showValue={false} style={{height: '6px'}} />
|
||||
</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-wrench text-orange-500 text-3xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-semibold">{maintenances.length}</div>
|
||||
<div className="text-500">Maintenances</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indicateurs de performance */}
|
||||
<div className="grid mb-4">
|
||||
<div className="col-12 md:col-4">
|
||||
<Card title="Taux de Disponibilité">
|
||||
<div className="text-center">
|
||||
<div className="text-6xl font-bold text-green-500 mb-2">
|
||||
{tauxDisponibilite.toFixed(0)}%
|
||||
</div>
|
||||
<ProgressBar value={tauxDisponibilite} showValue={false} className="mb-3" />
|
||||
<div className="text-500">
|
||||
{materiels.filter(m => m.statut === StatutMateriel.DISPONIBLE).length} / {materiels.length} disponibles
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-4">
|
||||
<Card title="Taux d'Utilisation">
|
||||
<div className="text-center">
|
||||
<div className="text-6xl font-bold text-blue-500 mb-2">
|
||||
{tauxUtilisation.toFixed(0)}%
|
||||
</div>
|
||||
<ProgressBar value={tauxUtilisation} showValue={false} className="mb-3" />
|
||||
<div className="text-500">
|
||||
{materiels.filter(m => m.statut === StatutMateriel.EN_UTILISATION).length} / {materiels.length} en utilisation
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-4">
|
||||
<Card title="Taux de Maintenance">
|
||||
<div className="text-center">
|
||||
<div className="text-6xl font-bold text-orange-500 mb-2">
|
||||
{tauxMaintenance.toFixed(0)}%
|
||||
</div>
|
||||
<ProgressBar value={tauxMaintenance} showValue={false} className="mb-3" />
|
||||
<div className="text-500">
|
||||
{materiels.filter(m => m.statut === StatutMateriel.EN_MAINTENANCE).length} / {materiels.length} en maintenance
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Graphiques */}
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-6">
|
||||
<Card title="Répartition par Statut">
|
||||
<Chart type="doughnut" data={getStatutStats()} options={chartOptions} style={{ height: '400px' }} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<Card title="Répartition par Type">
|
||||
<Chart type="pie" data={getTypeStats()} options={chartOptions} style={{ height: '400px' }} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<Card title="Valeur par Type de Matériel">
|
||||
<Chart type="bar" data={getValeurStats()} options={chartOptions} style={{ height: '400px' }} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<Card title="Évolution des Maintenances">
|
||||
<Chart type="line" data={getMaintenanceStats()} options={chartOptions} style={{ height: '400px' }} />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top matériels les plus coûteux */}
|
||||
<div className="col-12">
|
||||
<Card title="Top 5 - Matériels les plus coûteux">
|
||||
<DataTable
|
||||
value={materielsCouteux}
|
||||
showGridlines
|
||||
emptyMessage="Aucun matériel trouvé."
|
||||
>
|
||||
<Column field="nom" header="Nom" />
|
||||
<Column field="marque" header="Marque" />
|
||||
<Column field="type" header="Type" body={typeBodyTemplate} />
|
||||
<Column field="valeurActuelle" header="Valeur" body={valeurBodyTemplate} />
|
||||
<Column field="localisation" header="Localisation" />
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterielsStatsPage;
|
||||
Reference in New Issue
Block a user