Files
btpxpress-frontend/app/(main)/planning/materiel/page.tsx

1560 lines
72 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
import { Card } from 'primereact/card';
import { Dialog } from 'primereact/dialog';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Tag } from 'primereact/tag';
import { Dropdown } from 'primereact/dropdown';
import { Calendar } from 'primereact/calendar';
import { InputTextarea } from 'primereact/inputtextarea';
import { InputNumber } from 'primereact/inputnumber';
import { Panel } from 'primereact/panel';
import { TabView, TabPanel } from 'primereact/tabview';
import { ProgressBar } from 'primereact/progressbar';
import { Timeline } from 'primereact/timeline';
import { Badge } from 'primereact/badge';
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
import { Splitter, SplitterPanel } from 'primereact/splitter';
import { OverlayPanel } from 'primereact/overlaypanel';
import { Chart } from 'primereact/chart';
import { Image } from 'primereact/image';
import { formatDate, formatCurrency } from '../../../../utils/formatters';
import type {
Materiel,
MaintenanceMateriel,
TypeMateriel,
StatutMateriel,
TypeMaintenance,
StatutMaintenance
} from '../../../../types/btp';
import {
ActionButtonGroup,
ViewButton,
EditButton,
DeleteButton,
ActionButton
} from '../../../../components/ui/ActionButton';
import { materielService, maintenanceService } from '../../../../services/api';
const MaterielPage = () => {
const [materiels, setMateriels] = useState<Materiel[]>([]);
const [maintenances, setMaintenances] = useState<MaintenanceMateriel[]>([]);
const [selectedMateriels, setSelectedMateriels] = useState<Materiel[]>([]);
const [selectedMaintenances, setSelectedMaintenances] = useState<MaintenanceMateriel[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [activeTab, setActiveTab] = useState(0);
// Dialogs
const [materielDialog, setMaterielDialog] = useState(false);
const [maintenanceDialog, setMaintenanceDialog] = useState(false);
const [planningDialog, setPlanningDialog] = useState(false);
const [deleteDialog, setDeleteDialog] = useState(false);
const [historyDialog, setHistoryDialog] = useState(false);
// Current entities
const [materiel, setMateriel] = useState<Partial<Materiel>>({});
const [maintenance, setMaintenance] = useState<Partial<MaintenanceMateriel>>({});
const [selectedMateriel, setSelectedMateriel] = useState<Materiel | null>(null);
const [submitted, setSubmitted] = useState(false);
// Stats
const [stats, setStats] = useState({
totalMateriels: 0,
materielsDisponibles: 0,
materielsEnMaintenance: 0,
materielsHorsService: 0,
valeurTotale: 0,
coutMaintenanceAnnuel: 0,
tauxDisponibilite: 0,
maintenancesPrevues: 0
});
// Chart data
const [chartData, setChartData] = useState<any>({});
const [chartOptions, setChartOptions] = useState<any>({});
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<Materiel[]>>(null);
const op = useRef<OverlayPanel>(null);
const typeOptions = [
{ label: 'Véhicule', value: 'VEHICULE' },
{ label: 'Outil électrique', value: 'OUTIL_ELECTRIQUE' },
{ label: 'Outil manuel', value: 'OUTIL_MANUEL' },
{ label: 'Échafaudage', value: 'ECHAFAUDAGE' },
{ label: 'Bétonnière', value: 'BETONIERE' },
{ label: 'Grue', value: 'GRUE' },
{ label: 'Compresseur', value: 'COMPRESSEUR' },
{ label: 'Générateur', value: 'GENERATEUR' },
{ label: 'Engin de chantier', value: 'ENGIN_CHANTIER' },
{ label: 'Matériel de mesure', value: 'MATERIEL_MESURE' },
{ label: 'Équipement sécurité', value: 'EQUIPEMENT_SECURITE' },
{ label: 'Autre', value: 'AUTRE' }
];
const statutOptions = [
{ label: 'Disponible', value: 'DISPONIBLE' },
{ label: 'Utilisé', value: 'UTILISE' },
{ label: 'Maintenance', value: 'MAINTENANCE' },
{ label: 'Hors service', value: 'HORS_SERVICE' },
{ label: 'Réservé', value: 'RESERVE' },
{ label: 'En réparation', value: 'EN_REPARATION' }
];
const typeMaintenanceOptions = [
{ label: 'Préventive', value: 'PREVENTIVE' },
{ label: 'Corrective', value: 'CORRECTIVE' },
{ label: 'Révision', value: 'REVISION' },
{ label: 'Contrôle technique', value: 'CONTROLE_TECHNIQUE' },
{ label: 'Nettoyage', value: 'NETTOYAGE' }
];
const statutMaintenanceOptions = [
{ label: 'Planifiée', value: 'PLANIFIEE' },
{ label: 'En cours', value: 'EN_COURS' },
{ label: 'Terminée', value: 'TERMINEE' },
{ label: 'Reportée', value: 'REPORTEE' },
{ label: 'Annulée', value: 'ANNULEE' }
];
useEffect(() => {
loadData();
initChart();
}, []);
useEffect(() => {
calculateStats();
updateChartData();
}, [materiels, maintenances]);
const loadData = async () => {
try {
setLoading(true);
// Charger les matériels et maintenances en parallèle
const [materielsData, maintenancesData] = await Promise.all([
materielService.getAll(),
maintenanceService.getAll()
]);
setMateriels(materielsData);
setMaintenances(maintenancesData);
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données',
life: 3000
});
// Fallback vers les données mock en cas d'erreur
generateMockData();
} finally {
setLoading(false);
}
};
const generateMockData = () => {
const mockMateriels: Materiel[] = [
{
id: '1',
nom: 'Camion benne Mercedes',
marque: 'Mercedes',
modele: 'Actros 2635',
numeroSerie: 'MB2635001',
type: 'VEHICULE' as TypeMateriel,
description: 'Camion benne 6x4 pour transport de matériaux',
dateAchat: '2020-03-15',
valeurAchat: 85000,
valeurActuelle: 65000,
statut: 'DISPONIBLE' as StatutMateriel,
localisation: 'Dépôt principal',
proprietaire: 'BTP Express',
coutUtilisation: 120,
maintenances: [],
dateCreation: '2020-03-15T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '2',
nom: 'Grue mobile Liebherr',
marque: 'Liebherr',
modele: 'LTM 1030',
numeroSerie: 'LH1030001',
type: 'GRUE' as TypeMateriel,
description: 'Grue mobile 30 tonnes sur pneumatiques',
dateAchat: '2019-06-20',
valeurAchat: 250000,
valeurActuelle: 180000,
statut: 'UTILISE' as StatutMateriel,
localisation: 'Chantier A',
proprietaire: 'BTP Express',
coutUtilisation: 350,
maintenances: [],
dateCreation: '2019-06-20T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '3',
nom: 'Bétonnière Altrad',
marque: 'Altrad',
modele: 'B160',
numeroSerie: 'ALT160001',
type: 'BETONIERE' as TypeMateriel,
description: 'Bétonnière électrique 160L',
dateAchat: '2021-02-10',
valeurAchat: 1200,
valeurActuelle: 900,
statut: 'MAINTENANCE' as StatutMateriel,
localisation: 'Atelier maintenance',
proprietaire: 'BTP Express',
coutUtilisation: 15,
maintenances: [],
dateCreation: '2021-02-10T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '4',
nom: 'Compresseur Atlas Copco',
marque: 'Atlas Copco',
modele: 'XAS 67',
numeroSerie: 'AC67001',
type: 'COMPRESSEUR' as TypeMateriel,
description: 'Compresseur portable diesel 67 CFM',
dateAchat: '2022-08-15',
valeurAchat: 8500,
valeurActuelle: 7200,
statut: 'DISPONIBLE' as StatutMateriel,
localisation: 'Dépôt principal',
proprietaire: 'BTP Express',
coutUtilisation: 45,
maintenances: [],
dateCreation: '2022-08-15T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '5',
nom: 'Échafaudage Layher',
marque: 'Layher',
modele: 'Allround',
numeroSerie: 'LAY001',
type: 'ECHAFAUDAGE' as TypeMateriel,
description: 'Système d\'échafaudage modulaire - 50m²',
dateAchat: '2023-01-20',
valeurAchat: 15000,
valeurActuelle: 14000,
statut: 'RESERVE' as StatutMateriel,
localisation: 'Chantier B',
proprietaire: 'BTP Express',
coutUtilisation: 25,
maintenances: [],
dateCreation: '2023-01-20T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '6',
nom: 'Perceuse Hilti',
marque: 'Hilti',
modele: 'TE 30-C AVR',
numeroSerie: 'HI30001',
type: 'OUTIL_ELECTRIQUE' as TypeMateriel,
description: 'Perforateur SDS-Plus avec AVR',
dateAchat: '2023-09-10',
valeurAchat: 450,
valeurActuelle: 400,
statut: 'HORS_SERVICE' as StatutMateriel,
localisation: 'Atelier réparation',
proprietaire: 'BTP Express',
coutUtilisation: 8,
maintenances: [],
dateCreation: '2023-09-10T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
}
];
const mockMaintenances: MaintenanceMateriel[] = [
{
id: '1',
materiel: mockMateriels[0], // Camion
type: 'PREVENTIVE' as TypeMaintenance,
description: 'Révision 20 000 km - Vidange moteur, filtres, freins',
datePrevue: '2024-02-15',
cout: 850,
statut: 'PLANIFIEE' as StatutMaintenance,
technicien: 'Garage Dupont',
prochaineMaintenance: '2024-08-15',
dateCreation: '2024-01-10T09:00:00',
dateModification: '2024-01-10T09:00:00'
},
{
id: '2',
materiel: mockMateriels[1], // Grue
type: 'CONTROLE_TECHNIQUE' as TypeMaintenance,
description: 'Contrôle technique annuel obligatoire',
datePrevue: '2024-06-20',
cout: 1200,
statut: 'PLANIFIEE' as StatutMaintenance,
technicien: 'Liebherr Service',
prochaineMaintenance: '2025-06-20',
dateCreation: '2024-01-05T09:00:00',
dateModification: '2024-01-05T09:00:00'
},
{
id: '3',
materiel: mockMateriels[2], // Bétonnière
type: 'CORRECTIVE' as TypeMaintenance,
description: 'Réparation moteur électrique - Remplacement charbons',
datePrevue: '2024-01-20',
dateRealisee: '2024-01-22',
cout: 180,
statut: 'TERMINEE' as StatutMaintenance,
technicien: 'Atelier interne',
notes: 'Charbons remplacés, moteur testé OK',
dateCreation: '2024-01-18T09:00:00',
dateModification: '2024-01-22T16:00:00'
},
{
id: '4',
materiel: mockMateriels[3], // Compresseur
type: 'PREVENTIVE' as TypeMaintenance,
description: 'Entretien trimestriel - Filtres air/huile, niveau',
datePrevue: '2024-03-01',
cout: 120,
statut: 'PLANIFIEE' as StatutMaintenance,
technicien: 'Technicien interne',
prochaineMaintenance: '2024-06-01',
dateCreation: '2024-01-01T09:00:00',
dateModification: '2024-01-01T09:00:00'
},
{
id: '5',
materiel: mockMateriels[5], // Perceuse
type: 'CORRECTIVE' as TypeMaintenance,
description: 'Panne - Réparation système de percussion',
datePrevue: '2024-01-25',
statut: 'EN_COURS' as StatutMaintenance,
technicien: 'Service Hilti',
notes: 'En attente diagnostic complet',
dateCreation: '2024-01-24T14:00:00',
dateModification: '2024-01-25T10:00:00'
}
];
// Associer les maintenances aux matériels
mockMateriels.forEach(materiel => {
materiel.maintenances = mockMaintenances.filter(m => m.materiel.id === materiel.id);
});
setMateriels(mockMateriels);
setMaintenances(mockMaintenances);
};
const calculateStats = () => {
const totalMateriels = materiels.length;
const materielsDisponibles = materiels.filter(m => m.statut === 'DISPONIBLE').length;
const materielsEnMaintenance = materiels.filter(m => m.statut === 'MAINTENANCE' || m.statut === 'EN_REPARATION').length;
const materielsHorsService = materiels.filter(m => m.statut === 'HORS_SERVICE').length;
const valeurTotale = materiels.reduce((total, m) => total + (m.valeurActuelle || 0), 0);
const coutMaintenanceAnnuel = maintenances
.filter(m => m.dateRealisee && new Date(m.dateRealisee).getFullYear() === new Date().getFullYear())
.reduce((total, m) => total + (m.cout || 0), 0);
const tauxDisponibilite = totalMateriels > 0 ? Math.round((materielsDisponibles / totalMateriels) * 100) : 0;
const maintenancesPrevues = maintenances.filter(m => m.statut === 'PLANIFIEE').length;
setStats({
totalMateriels,
materielsDisponibles,
materielsEnMaintenance,
materielsHorsService,
valeurTotale,
coutMaintenanceAnnuel,
tauxDisponibilite,
maintenancesPrevues
});
};
const initChart = () => {
const documentStyle = getComputedStyle(document.documentElement);
const textColor = documentStyle.getPropertyValue('--text-color');
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
setChartOptions({
maintainAspectRatio: false,
aspectRatio: 0.8,
plugins: {
legend: {
labels: {
color: textColor
}
}
},
scales: {
x: {
ticks: {
color: textColorSecondary,
font: {
weight: 500
}
},
grid: {
color: surfaceBorder,
drawBorder: false
}
},
y: {
ticks: {
color: textColorSecondary
},
grid: {
color: surfaceBorder,
drawBorder: false
}
}
}
});
};
const updateChartData = () => {
const statutCounts = statutOptions.reduce((acc, statut) => {
acc[statut.label] = materiels.filter(m => m.statut === statut.value).length;
return acc;
}, {} as Record<string, number>);
const data = {
labels: Object.keys(statutCounts),
datasets: [
{
data: Object.values(statutCounts),
backgroundColor: [
'#42A5F5',
'#FFA726',
'#EF5350',
'#AB47BC',
'#26A69A',
'#78909C'
]
}
]
};
setChartData(data);
};
const openNewMateriel = () => {
setMateriel({
nom: '',
type: 'AUTRE' as TypeMateriel,
statut: 'DISPONIBLE' as StatutMateriel,
proprietaire: 'BTP Express',
maintenances: [],
actif: true
});
setSubmitted(false);
setMaterielDialog(true);
};
const openNewMaintenance = () => {
setMaintenance({
type: 'PREVENTIVE' as TypeMaintenance,
description: '',
datePrevue: new Date().toISOString().split('T')[0],
statut: 'PLANIFIEE' as StatutMaintenance
});
setSubmitted(false);
setMaintenanceDialog(true);
};
const editMateriel = (materiel: Materiel) => {
setMateriel({ ...materiel });
setMaterielDialog(true);
};
const editMaintenance = (maintenance: MaintenanceMateriel) => {
setMaintenance({ ...maintenance });
setMaintenanceDialog(true);
};
const confirmDelete = (item: Materiel | MaintenanceMateriel, type: 'materiel' | 'maintenance') => {
if (type === 'materiel') {
setMateriel(item as Materiel);
} else {
setMaintenance(item as MaintenanceMateriel);
}
setDeleteDialog(true);
};
const deleteItem = async () => {
try {
if (materiel.id) {
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
});
} else if (maintenance.id) {
await maintenanceService.delete(maintenance.id);
const updatedMaintenances = maintenances.filter(m => m.id !== maintenance.id);
setMaintenances(updatedMaintenances);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Maintenance supprimée',
life: 3000
});
}
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors de la suppression',
life: 3000
});
} finally {
setDeleteDialog(false);
setMateriel({});
setMaintenance({});
}
};
const saveMateriel = async () => {
setSubmitted(true);
if (materiel.nom?.trim()) {
try {
let savedMateriel: Materiel;
if (materiel.id) {
// Mise à jour
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 {
// Création
savedMateriel = await materielService.create(materiel);
setMateriels([...materiels, savedMateriel]);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Matériel créé',
life: 3000
});
}
setMaterielDialog(false);
setMateriel({});
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors de la sauvegarde',
life: 3000
});
}
}
};
const saveMaintenance = async () => {
setSubmitted(true);
if (maintenance.description?.trim() && maintenance.materiel) {
try {
let savedMaintenance: MaintenanceMateriel;
if (maintenance.id) {
// Mise à jour
savedMaintenance = await maintenanceService.update(maintenance.id, maintenance);
const updatedMaintenances = maintenances.map(m =>
m.id === maintenance.id ? savedMaintenance : m
);
setMaintenances(updatedMaintenances);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Maintenance mise à jour',
life: 3000
});
} else {
// Création
savedMaintenance = await maintenanceService.create(maintenance);
setMaintenances([...maintenances, savedMaintenance]);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Maintenance créée',
life: 3000
});
}
setMaintenanceDialog(false);
setMaintenance({});
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors de la sauvegarde',
life: 3000
});
}
}
};
const findIndexById = (id: string, array: any[]) => {
return array.findIndex(item => item.id === id);
};
const exportCSV = () => {
dt.current?.exportCSV();
};
const onInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string, entity: 'materiel' | 'maintenance') => {
const val = (e.target && e.target.value) || '';
if (entity === 'materiel') {
setMateriel(prev => ({ ...prev, [name]: val }));
} else {
setMaintenance(prev => ({ ...prev, [name]: val }));
}
};
const onDropdownChange = (e: any, name: string, entity: 'materiel' | 'maintenance') => {
if (entity === 'materiel') {
setMateriel(prev => ({ ...prev, [name]: e.value }));
} else {
setMaintenance(prev => ({ ...prev, [name]: e.value }));
}
};
const onNumberChange = (e: any, name: string, entity: 'materiel' | 'maintenance') => {
if (entity === 'materiel') {
setMateriel(prev => ({ ...prev, [name]: e.value }));
} else {
setMaintenance(prev => ({ ...prev, [name]: e.value }));
}
};
const onDateChange = (e: any, name: string, entity: 'materiel' | 'maintenance') => {
const dateValue = e.value ? e.value.toISOString().split('T')[0] : '';
if (entity === 'materiel') {
setMateriel(prev => ({ ...prev, [name]: dateValue }));
} else {
setMaintenance(prev => ({ ...prev, [name]: dateValue }));
}
};
// Templates pour les matériels
const statutBodyTemplate = (rowData: Materiel) => {
const getSeverity = (statut: StatutMateriel) => {
switch (statut) {
case 'DISPONIBLE': return 'success';
case 'UTILISE': return 'info';
case 'RESERVE': return 'warning';
case 'MAINTENANCE': return 'secondary';
case 'EN_REPARATION': return 'warning';
case 'HORS_SERVICE': return 'danger';
default: return 'secondary';
}
};
const getLabel = (statut: StatutMateriel) => {
switch (statut) {
case 'DISPONIBLE': return 'Disponible';
case 'UTILISE': return 'Utilisé';
case 'RESERVE': return 'Réservé';
case 'MAINTENANCE': return 'Maintenance';
case 'EN_REPARATION': return 'En réparation';
case 'HORS_SERVICE': return 'Hors service';
default: return statut;
}
};
return <Tag value={getLabel(rowData.statut)} severity={getSeverity(rowData.statut)} />;
};
const typeBodyTemplate = (rowData: Materiel) => {
const getIcon = (type: TypeMateriel) => {
switch (type) {
case 'VEHICULE': return 'pi pi-car';
case 'GRUE': return 'pi pi-building';
case 'OUTIL_ELECTRIQUE': return 'pi pi-bolt';
case 'OUTIL_MANUEL': return 'pi pi-wrench';
case 'COMPRESSEUR': return 'pi pi-cog';
case 'BETONIERE': return 'pi pi-circle';
case 'ECHAFAUDAGE': return 'pi pi-th-large';
default: return 'pi pi-box';
}
};
const getLabel = (type: TypeMateriel) => {
const option = typeOptions.find(opt => opt.value === type);
return option ? option.label : type;
};
return (
<div className="flex align-items-center gap-2">
<i className={getIcon(rowData.type)} />
<span>{getLabel(rowData.type)}</span>
</div>
);
};
const valeurBodyTemplate = (rowData: Materiel) => {
const depreciationRate = rowData.valeurAchat && rowData.valeurActuelle ?
((rowData.valeurAchat - rowData.valeurActuelle) / rowData.valeurAchat) * 100 : 0;
return (
<div className="flex flex-column">
<span className="font-semibold">{formatCurrency(rowData.valeurActuelle)}</span>
<small className="text-color-secondary">
Achat: {formatCurrency(rowData.valeurAchat)}
{depreciationRate > 0 && ` (-${depreciationRate.toFixed(1)}%)`}
</small>
</div>
);
};
const maintenanceBodyTemplate = (rowData: Materiel) => {
const maintenancesMatériel = maintenances.filter(m => m.materiel.id === rowData.id);
const maintenancesPrevues = maintenancesMatériel.filter(m => m.statut === 'PLANIFIEE').length;
const maintenancesEnCours = maintenancesMatériel.filter(m => m.statut === 'EN_COURS').length;
return (
<div className="flex align-items-center gap-2">
<Badge value={maintenancesMatériel.length} />
{maintenancesPrevues > 0 && (
<Tag value={`${maintenancesPrevues} prévue(s)`} severity="info" />
)}
{maintenancesEnCours > 0 && (
<Tag value={`${maintenancesEnCours} en cours`} severity="warning" />
)}
</div>
);
};
const actionBodyTemplate = (rowData: Materiel) => {
return (
<ActionButtonGroup>
<ActionButton
icon="pi pi-calendar"
color="blue"
tooltip="Planning"
onClick={() => {
setSelectedMateriel(rowData);
setPlanningDialog(true);
}}
/>
<Button
icon="pi pi-wrench"
className="p-button-rounded p-button-text p-button-sm"
tooltip="Maintenance"
tooltipOptions={{ position: 'top' }}
onClick={(e) => {
setSelectedMateriel(rowData);
op.current?.toggle(e);
}}
/>
<EditButton
tooltip="Modifier"
onClick={() => editMateriel(rowData)}
/>
<DeleteButton
tooltip="Supprimer"
onClick={() => confirmDelete(rowData, 'materiel')}
/>
</ActionButtonGroup>
);
};
// Templates pour les maintenances
const maintenanceStatutBodyTemplate = (rowData: MaintenanceMateriel) => {
const getSeverity = (statut: StatutMaintenance) => {
switch (statut) {
case 'PLANIFIEE': return 'info';
case 'EN_COURS': return 'warning';
case 'TERMINEE': return 'success';
case 'REPORTEE': return 'secondary';
case 'ANNULEE': return 'danger';
default: return 'secondary';
}
};
const getLabel = (statut: StatutMaintenance) => {
const option = statutMaintenanceOptions.find(opt => opt.value === statut);
return option ? option.label : statut;
};
return <Tag value={getLabel(rowData.statut)} severity={getSeverity(rowData.statut)} />;
};
const maintenanceTypeBodyTemplate = (rowData: MaintenanceMateriel) => {
const getLabel = (type: TypeMaintenance) => {
const option = typeMaintenanceOptions.find(opt => opt.value === type);
return option ? option.label : type;
};
return getLabel(rowData.type);
};
const maintenanceDateBodyTemplate = (rowData: MaintenanceMateriel) => {
const isOverdue = rowData.statut === 'PLANIFIEE' && new Date(rowData.datePrevue) < new Date();
return (
<div className={`flex flex-column ${isOverdue ? 'text-red-500' : ''}`}>
<span className="font-semibold">
{isOverdue && <i className="pi pi-exclamation-triangle mr-1" />}
Prévue: {formatDate(rowData.datePrevue)}
</span>
{rowData.dateRealisee && (
<small className="text-color-secondary">
Réalisée: {formatDate(rowData.dateRealisee)}
</small>
)}
</div>
);
};
const maintenanceActionBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<ActionButtonGroup>
<EditButton
tooltip="Modifier"
onClick={() => editMaintenance(rowData)}
/>
<DeleteButton
tooltip="Supprimer"
onClick={() => confirmDelete(rowData, 'maintenance')}
/>
</ActionButtonGroup>
);
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
{activeTab === 0 ? (
<>
<Button
label="Nouveau matériel"
icon="pi pi-plus"
severity="success"
className="p-button-text p-button-rounded mr-2"
onClick={openNewMateriel}
/>
<Button
label="Supprimer"
icon="pi pi-trash"
severity="danger"
className="p-button-text p-button-rounded"
onClick={() => {
confirmDialog({
message: 'Êtes-vous sûr de vouloir supprimer les matériels sélectionnés ?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Matériels supprimés',
life: 3000
});
}
});
}}
disabled={!selectedMateriels || selectedMateriels.length === 0}
/>
</>
) : (
<>
<Button
label="Nouvelle maintenance"
icon="pi pi-plus"
severity="success"
className="p-button-text p-button-rounded mr-2"
onClick={openNewMaintenance}
/>
<Button
label="Supprimer"
icon="pi pi-trash"
severity="danger"
className="p-button-text p-button-rounded"
onClick={() => {
confirmDialog({
message: 'Êtes-vous sûr de vouloir supprimer les maintenances sélectionnées ?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Maintenances supprimées',
life: 3000
});
}
});
}}
disabled={!selectedMaintenances || selectedMaintenances.length === 0}
/>
</>
)}
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
className="p-button-text p-button-rounded"
onClick={exportCSV}
/>
</div>
);
};
const renderStatsCards = () => {
return (
<div className="grid mb-4">
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="bg-blue-100 p-3 border-round mr-3">
<i className="pi pi-box text-blue-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.totalMateriels}</div>
<div className="text-color-secondary">Matériels total</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="bg-green-100 p-3 border-round mr-3">
<i className="pi pi-check-circle text-green-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.tauxDisponibilite}%</div>
<div className="text-color-secondary">Taux disponibilité</div>
<ProgressBar value={stats.tauxDisponibilite} className="mt-1" style={{ height: '4px' }} />
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="bg-orange-100 p-3 border-round mr-3">
<i className="pi pi-euro text-orange-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{formatCurrency(stats.valeurTotale)}</div>
<div className="text-color-secondary">Valeur totale</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="bg-purple-100 p-3 border-round mr-3">
<i className="pi pi-wrench text-purple-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.maintenancesPrevues}</div>
<div className="text-color-secondary">Maintenances prévues</div>
</div>
</div>
</Card>
</div>
</div>
);
};
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<ConfirmDialog />
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
{renderStatsCards()}
<TabView activeIndex={activeTab} onTabChange={(e) => setActiveTab(e.index)}>
<TabPanel header="Matériels">
<Splitter style={{ height: '600px' }}>
<SplitterPanel size={75}>
<div className="p-3 h-full">
<DataTable
ref={dt}
value={materiels}
selection={selectedMateriels}
onSelectionChange={(e) => setSelectedMateriels(e.value)}
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
className="datatable-responsive"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} matériels"
globalFilter={globalFilter}
emptyMessage="Aucun matériel trouvé."
loading={loading}
scrollable
scrollHeight="500px"
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="nom" header="Nom" sortable style={{ minWidth: '200px' }} />
<Column field="type" header="Type" body={typeBodyTemplate} sortable style={{ minWidth: '150px' }} />
<Column field="marque" header="Marque" sortable style={{ minWidth: '100px' }} />
<Column field="modele" header="Modèle" sortable style={{ minWidth: '120px' }} />
<Column field="localisation" header="Localisation" sortable style={{ minWidth: '150px' }} />
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable style={{ minWidth: '120px' }} />
<Column field="valeur" header="Valeur" body={valeurBodyTemplate} sortable style={{ minWidth: '120px' }} />
<Column field="maintenance" header="Maintenance" body={maintenanceBodyTemplate} style={{ minWidth: '150px' }} />
<Column body={actionBodyTemplate} style={{ minWidth: '150px' }} />
</DataTable>
</div>
</SplitterPanel>
<SplitterPanel size={25}>
<div className="p-3 h-full">
<Panel header="Répartition par statut" className="h-full">
<Chart type="doughnut" data={chartData} options={chartOptions} className="w-full" />
</Panel>
</div>
</SplitterPanel>
</Splitter>
</TabPanel>
<TabPanel header="Maintenances">
<DataTable
value={maintenances}
selection={selectedMaintenances}
onSelectionChange={(e) => setSelectedMaintenances(e.value)}
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
className="datatable-responsive"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} maintenances"
globalFilter={globalFilter}
emptyMessage="Aucune maintenance trouvée."
loading={loading}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="materiel.nom" header="Matériel" sortable style={{ minWidth: '200px' }} />
<Column field="type" header="Type" body={maintenanceTypeBodyTemplate} sortable style={{ minWidth: '120px' }} />
<Column field="description" header="Description" sortable style={{ minWidth: '250px' }} />
<Column field="dates" header="Dates" body={maintenanceDateBodyTemplate} sortable style={{ minWidth: '180px' }} />
<Column field="technicien" header="Technicien" sortable style={{ minWidth: '150px' }} />
<Column field="cout" header="Coût" body={(rowData) => formatCurrency(rowData.cout)} sortable style={{ minWidth: '100px' }} />
<Column field="statut" header="Statut" body={maintenanceStatutBodyTemplate} sortable style={{ minWidth: '120px' }} />
<Column body={maintenanceActionBodyTemplate} style={{ minWidth: '100px' }} />
</DataTable>
</TabPanel>
</TabView>
{/* Overlay Panel pour maintenance rapide */}
<OverlayPanel ref={op} showCloseIcon>
{selectedMateriel && (
<div className="flex flex-column gap-3" style={{ minWidth: '300px' }}>
<div className="font-semibold text-lg">{selectedMateriel.nom}</div>
<div className="flex flex-column gap-2">
<Button
label="Programmer maintenance"
icon="pi pi-calendar-plus"
severity="info"
size="small"
className="p-button-text p-button-rounded"
onClick={() => {
setMaintenance({
materiel: selectedMateriel,
type: 'PREVENTIVE' as TypeMaintenance,
description: '',
datePrevue: new Date().toISOString().split('T')[0],
statut: 'PLANIFIEE' as StatutMaintenance
});
setMaintenanceDialog(true);
op.current?.hide();
}}
/>
<Button
label="Signaler panne"
icon="pi pi-exclamation-triangle"
severity="warning"
size="small"
className="p-button-text p-button-rounded"
onClick={() => {
setMaintenance({
materiel: selectedMateriel,
type: 'CORRECTIVE' as TypeMaintenance,
description: 'Panne signalée',
datePrevue: new Date().toISOString().split('T')[0],
statut: 'PLANIFIEE' as StatutMaintenance
});
setMaintenanceDialog(true);
op.current?.hide();
}}
/>
<Button
label="Historique"
icon="pi pi-history"
severity={"secondary" as any}
size="small"
className="p-button-text p-button-rounded"
onClick={() => {
setHistoryDialog(true);
op.current?.hide();
}}
/>
</div>
</div>
)}
</OverlayPanel>
{/* Dialog Matériel */}
<Dialog
visible={materielDialog}
style={{ width: '800px' }}
header="Détails du matériel"
modal
className="p-fluid"
footer={
<div>
<Button label="Annuler" icon="pi pi-times" className="p-button-text p-button-rounded mr-2" onClick={() => setMaterielDialog(false)} />
<Button label="Sauvegarder" icon="pi pi-check" className="p-button-text p-button-rounded" onClick={saveMateriel} />
</div>
}
onHide={() => setMaterielDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="nom">Nom du matériel *</label>
<InputText
id="nom"
value={materiel.nom || ''}
onChange={(e) => onInputChange(e, 'nom', 'materiel')}
required
className={submitted && !materiel.nom ? 'p-invalid' : ''}
/>
{submitted && !materiel.nom && <small className="p-invalid">Le nom est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="type">Type</label>
<Dropdown
id="type"
value={materiel.type}
options={typeOptions}
onChange={(e) => onDropdownChange(e, 'type', 'materiel')}
placeholder="Sélectionnez un type"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="statut">Statut</label>
<Dropdown
id="statut"
value={materiel.statut}
options={statutOptions}
onChange={(e) => onDropdownChange(e, 'statut', 'materiel')}
placeholder="Sélectionnez un statut"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="marque">Marque</label>
<InputText
id="marque"
value={materiel.marque || ''}
onChange={(e) => onInputChange(e, 'marque', 'materiel')}
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="modele">Modèle</label>
<InputText
id="modele"
value={materiel.modele || ''}
onChange={(e) => onInputChange(e, 'modele', 'materiel')}
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="numeroSerie">Numéro de série</label>
<InputText
id="numeroSerie"
value={materiel.numeroSerie || ''}
onChange={(e) => onInputChange(e, 'numeroSerie', 'materiel')}
/>
</div>
<div className="field col-12">
<label htmlFor="description">Description</label>
<InputTextarea
id="description"
value={materiel.description || ''}
onChange={(e) => onInputChange(e, 'description', 'materiel')}
rows={3}
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="dateAchat">Date d'achat</label>
<Calendar
id="dateAchat"
value={materiel.dateAchat ? new Date(materiel.dateAchat) : null}
onChange={(e) => onDateChange(e, 'dateAchat', 'materiel')}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="localisation">Localisation</label>
<InputText
id="localisation"
value={materiel.localisation || ''}
onChange={(e) => onInputChange(e, 'localisation', 'materiel')}
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="valeurAchat">Valeur d'achat ()</label>
<InputNumber
id="valeurAchat"
value={materiel.valeurAchat}
onValueChange={(e) => onNumberChange(e, 'valeurAchat', 'materiel')}
mode="currency"
currency="EUR"
locale="fr-FR"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="valeurActuelle">Valeur actuelle ()</label>
<InputNumber
id="valeurActuelle"
value={materiel.valeurActuelle}
onValueChange={(e) => onNumberChange(e, 'valeurActuelle', 'materiel')}
mode="currency"
currency="EUR"
locale="fr-FR"
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="coutUtilisation">Coût d'utilisation/jour (€)</label>
<InputNumber
id="coutUtilisation"
value={materiel.coutUtilisation}
onValueChange={(e) => onNumberChange(e, 'coutUtilisation', 'materiel')}
mode="currency"
currency="EUR"
locale="fr-FR"
/>
</div>
<div className="field col-12">
<label htmlFor="proprietaire">Propriétaire</label>
<InputText
id="proprietaire"
value={materiel.proprietaire || ''}
onChange={(e) => onInputChange(e, 'proprietaire', 'materiel')}
/>
</div>
</div>
</Dialog>
{/* Dialog Maintenance */}
<Dialog
visible={maintenanceDialog}
style={{ width: '700px' }}
header="Détails de la maintenance"
modal
className="p-fluid"
footer={
<div>
<Button label="Annuler" icon="pi pi-times" className="p-button-text p-button-rounded mr-2" onClick={() => setMaintenanceDialog(false)} />
<Button label="Sauvegarder" icon="pi pi-check" className="p-button-text p-button-rounded" onClick={saveMaintenance} />
</div>
}
onHide={() => setMaintenanceDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="materielMaintenance">Matériel *</label>
<Dropdown
id="materielMaintenance"
value={maintenance.materiel?.id}
options={materiels.map(m => ({
label: `${m.nom} - ${m.marque} ${m.modele}`,
value: m.id
}))}
onChange={(e) => {
const selectedMateriel = materiels.find(m => m.id === e.value);
setMaintenance(prev => ({ ...prev, materiel: selectedMateriel }));
}}
placeholder="Sélectionnez un matériel"
required
className={submitted && !maintenance.materiel ? 'p-invalid' : ''}
/>
{submitted && !maintenance.materiel && <small className="p-invalid">Le matériel est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="typeMaintenance">Type</label>
<Dropdown
id="typeMaintenance"
value={maintenance.type}
options={typeMaintenanceOptions}
onChange={(e) => onDropdownChange(e, 'type', 'maintenance')}
placeholder="Sélectionnez un type"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="statutMaintenance">Statut</label>
<Dropdown
id="statutMaintenance"
value={maintenance.statut}
options={statutMaintenanceOptions}
onChange={(e) => onDropdownChange(e, 'statut', 'maintenance')}
placeholder="Sélectionnez un statut"
/>
</div>
<div className="field col-12">
<label htmlFor="descriptionMaintenance">Description *</label>
<InputTextarea
id="descriptionMaintenance"
value={maintenance.description || ''}
onChange={(e) => onInputChange(e, 'description', 'maintenance')}
rows={3}
required
className={submitted && !maintenance.description ? 'p-invalid' : ''}
/>
{submitted && !maintenance.description && <small className="p-invalid">La description est requise.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="datePrevueMaintenance">Date prévue *</label>
<Calendar
id="datePrevueMaintenance"
value={maintenance.datePrevue ? new Date(maintenance.datePrevue) : null}
onChange={(e) => onDateChange(e, 'datePrevue', 'maintenance')}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="dateRealisee">Date réalisée</label>
<Calendar
id="dateRealisee"
value={maintenance.dateRealisee ? new Date(maintenance.dateRealisee) : null}
onChange={(e) => onDateChange(e, 'dateRealisee', 'maintenance')}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="coutMaintenance">Coût (€)</label>
<InputNumber
id="coutMaintenance"
value={maintenance.cout}
onValueChange={(e) => onNumberChange(e, 'cout', 'maintenance')}
mode="currency"
currency="EUR"
locale="fr-FR"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="technicienMaintenance">Technicien</label>
<InputText
id="technicienMaintenance"
value={maintenance.technicien || ''}
onChange={(e) => onInputChange(e, 'technicien', 'maintenance')}
/>
</div>
<div className="field col-12">
<label htmlFor="notesMaintenance">Notes</label>
<InputTextarea
id="notesMaintenance"
value={maintenance.notes || ''}
onChange={(e) => onInputChange(e, 'notes', 'maintenance')}
rows={2}
/>
</div>
<div className="field col-12">
<label htmlFor="prochaineMaintenance">Prochaine maintenance</label>
<Calendar
id="prochaineMaintenance"
value={maintenance.prochaineMaintenance ? new Date(maintenance.prochaineMaintenance) : null}
onChange={(e) => onDateChange(e, 'prochaineMaintenance', 'maintenance')}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
</div>
</Dialog>
{/* Dialog Planning Matériel */}
<Dialog
visible={planningDialog}
style={{ width: '900px' }}
header={`Planning - ${selectedMateriel?.nom}`}
modal
onHide={() => setPlanningDialog(false)}
>
{selectedMateriel && (
<div className="flex flex-column gap-4">
<div className="grid">
<div className="col-12 md:col-6">
<Panel header="Informations générales">
<div className="flex flex-column gap-2">
<div><strong>Type:</strong> {typeBodyTemplate(selectedMateriel)}</div>
<div><strong>Marque/Modèle:</strong> {selectedMateriel.marque} {selectedMateriel.modele}</div>
<div><strong>Statut:</strong> {statutBodyTemplate(selectedMateriel)}</div>
<div><strong>Localisation:</strong> {selectedMateriel.localisation}</div>
<div><strong>Coût/jour:</strong> {formatCurrency(selectedMateriel.coutUtilisation)}</div>
</div>
</Panel>
</div>
<div className="col-12 md:col-6">
<Panel header="Utilisation de la semaine">
<Timeline
value={[
{ status: 'Chantier A - Fondations', date: 'Lundi 08:00-17:00', icon: 'pi pi-building', color: '#9C27B0' },
{ status: 'Maintenance préventive', date: 'Mardi 14:00-16:00', icon: 'pi pi-wrench', color: '#FF9800' },
{ status: 'Chantier B - Gros œuvre', date: 'Mercredi 08:00-17:00', icon: 'pi pi-building', color: '#9C27B0' },
{ status: 'Disponible', date: 'Jeudi', icon: 'pi pi-check', color: '#4CAF50' },
{ status: 'Chantier C - Finitions', date: 'Vendredi 08:00-17:00', icon: 'pi pi-building', color: '#9C27B0' }
]}
content={(item) => (
<div className="flex flex-column">
<small className="text-color-secondary">{item.date}</small>
<div className="font-medium">{item.status}</div>
</div>
)}
/>
</Panel>
</div>
</div>
<Panel header="Historique des maintenances">
<Timeline
value={selectedMateriel.maintenances?.map(maintenance => ({
status: maintenanceTypeBodyTemplate(maintenance),
date: formatDate(maintenance.datePrevue),
icon: 'pi pi-wrench',
color: maintenance.statut === 'TERMINEE' ? '#4CAF50' : '#FF9800',
description: maintenance.description,
cost: maintenance.cout
})) || []}
content={(item) => (
<div className="flex flex-column">
<small className="text-color-secondary">{item.date}</small>
<div className="font-medium">{item.status}</div>
<div className="text-sm mt-1">{item.description}</div>
{item.cost && <div className="text-sm font-semibold">{formatCurrency(item.cost)}</div>}
</div>
)}
/>
</Panel>
</div>
)}
</Dialog>
{/* Dialog Suppression */}
<Dialog
visible={deleteDialog}
style={{ width: '450px' }}
header="Confirmer"
modal
footer={
<div>
<Button label="Non" icon="pi pi-times" className="p-button-text p-button-rounded mr-2" onClick={() => setDeleteDialog(false)} />
<Button label="Oui" icon="pi pi-check" className="p-button-text p-button-rounded" onClick={deleteItem} />
</div>
}
onHide={() => setDeleteDialog(false)}
>
<div className="flex align-items-center justify-content-center">
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
{materiel.nom && (
<span>
Êtes-vous sûr de vouloir supprimer le matériel <b>{materiel.nom}</b> ?
</span>
)}
{maintenance.description && (
<span>
Êtes-vous sûr de vouloir supprimer cette maintenance ?
</span>
)}
</div>
</Dialog>
</Card>
</div>
</div>
);
};
export default MaterielPage;