'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([]); const [maintenances, setMaintenances] = useState([]); const [selectedMateriels, setSelectedMateriels] = useState([]); const [selectedMaintenances, setSelectedMaintenances] = useState([]); 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>({}); const [maintenance, setMaintenance] = useState>({}); const [selectedMateriel, setSelectedMateriel] = useState(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({}); const [chartOptions, setChartOptions] = useState({}); const toast = useRef(null); const dt = useRef>(null); const op = useRef(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); 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, 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 ; }; 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 (
{getLabel(rowData.type)}
); }; const valeurBodyTemplate = (rowData: Materiel) => { const depreciationRate = rowData.valeurAchat && rowData.valeurActuelle ? ((rowData.valeurAchat - rowData.valeurActuelle) / rowData.valeurAchat) * 100 : 0; return (
{formatCurrency(rowData.valeurActuelle)} Achat: {formatCurrency(rowData.valeurAchat)} {depreciationRate > 0 && ` (-${depreciationRate.toFixed(1)}%)`}
); }; 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 (
{maintenancesPrevues > 0 && ( )} {maintenancesEnCours > 0 && ( )}
); }; const actionBodyTemplate = (rowData: Materiel) => { return ( { setSelectedMateriel(rowData); setPlanningDialog(true); }} />