1556 lines
72 KiB
TypeScript
1556 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
|
|
onClick={() => editMateriel(rowData)}
|
|
/>
|
|
<DeleteButton
|
|
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
|
|
onClick={() => editMaintenance(rowData)}
|
|
/>
|
|
<DeleteButton
|
|
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;
|
|
|
|
|