Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

View File

@@ -0,0 +1,313 @@
'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 { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Tag } from 'primereact/tag';
import { Card } from 'primereact/card';
import { ProgressBar } from 'primereact/progressbar';
import { FilterMatchMode } from 'primereact/api';
import { maintenanceService } from '../../../../services/api';
import { MaintenanceMateriel, StatutMaintenance } from '../../../../types/btp';
import { formatDate } from '../../../../utils/formatters';
const MaintenancesEnCoursPage = () => {
const [maintenances, setMaintenances] = useState<MaintenanceMateriel[]>([]);
const [selectedMaintenances, setSelectedMaintenances] = useState<MaintenanceMateriel[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<MaintenanceMateriel[]>>(null);
useEffect(() => {
loadMaintenancesEnCours();
}, []);
const loadMaintenancesEnCours = async () => {
try {
setLoading(true);
const data = await maintenanceService.getAll();
// Filtrer les maintenances en cours
const enCours = data.filter(m => m.statut === StatutMaintenance.EN_COURS);
setMaintenances(enCours);
} catch (error) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les maintenances en cours',
life: 3000
});
} finally {
setLoading(false);
}
};
const terminerMaintenance = async (maintenance: MaintenanceMateriel) => {
try {
const updatedMaintenance = {
...maintenance,
statut: StatutMaintenance.TERMINEE,
dateRealisee: new Date().toISOString()
};
await maintenanceService.update(maintenance.id, updatedMaintenance);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Maintenance terminée',
life: 3000
});
loadMaintenancesEnCours();
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors de la finalisation',
life: 3000
});
}
};
const exportCSV = () => {
dt.current?.exportCSV();
};
// Templates pour les colonnes
const materielBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<div>
<div className="font-semibold">{rowData.materiel?.nom}</div>
<div className="text-sm text-500">{rowData.materiel?.marque} {rowData.materiel?.modele}</div>
</div>
);
};
const dureeBodyTemplate = (rowData: MaintenanceMateriel) => {
if (!rowData.dateRealisee) return 'N/A';
const debut = new Date(rowData.dateRealisee);
const maintenant = new Date();
const diffHeures = Math.floor((maintenant.getTime() - debut.getTime()) / (1000 * 60 * 60));
return (
<div>
<div>{diffHeures}h</div>
<ProgressBar value={Math.min(100, (diffHeures / 24) * 100)} className="mt-1" style={{height: '4px'}} />
</div>
);
};
const prioriteBodyTemplate = (rowData: MaintenanceMateriel) => {
const dateDebut = new Date(rowData.dateRealisee || rowData.datePrevue);
const heures = Math.floor((Date.now() - dateDebut.getTime()) / (1000 * 60 * 60));
if (heures > 48) {
return <Tag value="URGENT" severity="danger" />;
} else if (heures > 24) {
return <Tag value="PRIORITAIRE" severity="warning" />;
} else {
return <Tag value="NORMAL" severity="success" />;
}
};
const actionBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-check"
label="Terminer"
size="small"
severity="success"
className="p-button-text p-button-rounded"
onClick={() => terminerMaintenance(rowData)}
/>
<Button
icon="pi pi-eye"
size="small"
severity="info"
className="p-button-text p-button-rounded"
tooltip="Voir détails"
/>
</div>
);
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2 align-items-center">
<h4 className="m-0">Maintenances En Cours</h4>
<span className="text-500">({maintenances.length} maintenances actives)</span>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
className="p-button-text p-button-rounded"
onClick={exportCSV}
/>
);
};
const header = (
<div className="flex flex-wrap gap-2 align-items-center justify-content-between">
<div className="flex align-items-center gap-2">
<Button
icon="pi pi-refresh"
className="p-button-text p-button-rounded"
onClick={loadMaintenancesEnCours}
loading={loading}
/>
<span className="text-sm text-500">Dernière mise à jour: {new Date().toLocaleTimeString('fr-FR')}</span>
</div>
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
</div>
);
const filters = {
global: { value: globalFilter, matchMode: FilterMatchMode.CONTAINS }
};
// Statistiques
const maintenancesUrgentes = maintenances.filter(m => {
const heures = Math.floor((Date.now() - new Date(m.dateRealisee || m.datePrevue).getTime()) / (1000 * 60 * 60));
return heures > 48;
}).length;
const maintenancesPrioritaires = maintenances.filter(m => {
const heures = Math.floor((Date.now() - new Date(m.dateRealisee || m.datePrevue).getTime()) / (1000 * 60 * 60));
return heures > 24 && heures <= 48;
}).length;
return (
<div className="grid">
<div className="col-12">
<Toast ref={toast} />
{/* Cards de statistiques */}
<div className="grid mb-4">
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="mr-3">
<i className="pi pi-play-circle text-blue-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenances.length}</div>
<div className="text-500">En cours</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="mr-3">
<i className="pi pi-exclamation-triangle text-orange-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenancesPrioritaires}</div>
<div className="text-500">Prioritaires</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="mr-3">
<i className="pi pi-times-circle text-red-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenancesUrgentes}</div>
<div className="text-500">Urgentes</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="mr-3">
<i className="pi pi-clock text-green-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">
{maintenances.length > 0
? Math.round(maintenances.reduce((acc, m) => {
const heures = Math.floor((Date.now() - new Date(m.dateRealisee || m.datePrevue).getTime()) / (1000 * 60 * 60));
return acc + heures;
}, 0) / maintenances.length)
: 0
}h
</div>
<div className="text-500">Durée moyenne</div>
</div>
</div>
</Card>
</div>
</div>
<div className="card">
<Toolbar
className="mb-4"
left={leftToolbarTemplate}
right={rightToolbarTemplate}
/>
<DataTable
ref={dt}
value={maintenances}
selection={selectedMaintenances}
onSelectionChange={(e) => setSelectedMaintenances(e.value)}
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage {first} à {last} de {totalRecords} maintenances"
filters={filters}
filterDisplay="menu"
loading={loading}
globalFilterFields={['description', 'materiel.nom', 'type']}
header={header}
emptyMessage="Aucune maintenance en cours."
sortField="dateRealisee"
sortOrder={-1}
>
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
<Column header="Matériel" body={materielBodyTemplate} />
<Column header="Priorité" body={prioriteBodyTemplate} />
<Column header="Durée" body={dureeBodyTemplate} />
<Column field="description" header="Description" />
<Column field="dateRealisee" header="Démarrée le" body={(data) => formatDate(data.dateRealisee)} sortable />
<Column body={actionBodyTemplate} exportable={false} style={{ minWidth: '8rem' }} />
</DataTable>
</div>
</div>
</div>
);
};
export default MaintenancesEnCoursPage;

View File

@@ -0,0 +1,314 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { InputNumber } from 'primereact/inputnumber';
import { Dropdown } from 'primereact/dropdown';
import { Calendar } from 'primereact/calendar';
import { InputTextarea } from 'primereact/inputtextarea';
import { Toast } from 'primereact/toast';
import { maintenanceService, materielService } from '../../../../services/api';
import { Materiel, TypeMaintenance, StatutMaintenance } from '../../../../types/btp';
const NouvelleMaintenancePage = () => {
const router = useRouter();
const [maintenance, setMaintenance] = useState({
materiel: null as Materiel | null,
type: TypeMaintenance.PREVENTIVE,
statut: StatutMaintenance.PLANIFIEE,
description: '',
datePrevue: new Date(),
dateRealisee: null as Date | null,
cout: 0,
notes: ''
});
const [materiels, setMateriels] = useState<Materiel[]>([]);
const [submitted, setSubmitted] = useState(false);
const [saving, setSaving] = useState(false);
const toast = useRef<Toast>(null);
const typeOptions = Object.values(TypeMaintenance).map(type => ({
label: type.replace('_', ' '),
value: type
}));
const statutOptions = Object.values(StatutMaintenance).map(statut => ({
label: statut.replace('_', ' '),
value: statut
}));
useEffect(() => {
loadMateriels();
}, []);
const loadMateriels = async () => {
try {
const data = await materielService.getAll();
setMateriels(data);
} catch (error) {
console.error('Erreur lors du chargement des matériels:', error);
}
};
const onInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>, name: string) => {
const val = (e.target && e.target.value) || '';
setMaintenance(prev => ({ ...prev, [name]: val }));
};
const onInputNumberChange = (value: number | null, name: string) => {
setMaintenance(prev => ({ ...prev, [name]: value || 0 }));
};
const onDropdownChange = (e: any, name: string) => {
setMaintenance(prev => ({ ...prev, [name]: e.value }));
};
const onDateChange = (e: any, name: string) => {
setMaintenance(prev => ({ ...prev, [name]: e.value }));
};
const saveMaintenance = async () => {
setSubmitted(true);
if (maintenance.description?.trim() && maintenance.materiel) {
try {
setSaving(true);
const savedMaintenance = await maintenanceService.create(maintenance);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Maintenance créée avec succès',
life: 3000
});
// Rediriger vers la liste après 2 secondes
setTimeout(() => {
router.push('/maintenances');
}, 2000);
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors de la création',
life: 3000
});
} finally {
setSaving(false);
}
}
};
const annuler = () => {
router.back();
};
const materielOptions = materiels.map(materiel => ({
label: `${materiel.nom} - ${materiel.marque} ${materiel.modele}`,
value: materiel
}));
// Templates prédéfinis selon le type
const onTypeChange = (newType: TypeMaintenance) => {
let description = '';
let cout = 0;
switch (newType) {
case TypeMaintenance.PREVENTIVE:
description = 'Maintenance préventive programmée selon planning';
cout = 100;
break;
case TypeMaintenance.CORRECTIVE:
description = 'Réparation suite à dysfonctionnement';
cout = 300;
break;
case TypeMaintenance.REVISION:
description = 'Révision complète du matériel';
cout = 200;
break;
case TypeMaintenance.CONTROLE_TECHNIQUE:
description = 'Contrôle technique réglementaire';
cout = 150;
break;
case TypeMaintenance.NETTOYAGE:
description = 'Nettoyage et entretien courant';
cout = 50;
break;
}
setMaintenance(prev => ({ ...prev, type: newType, description, cout }));
};
return (
<div className="grid">
<div className="col-12">
<Toast ref={toast} />
<div className="flex align-items-center justify-content-between mb-4">
<h2 className="m-0">Nouvelle Maintenance</h2>
<div className="flex gap-2">
<Button
label="Annuler"
icon="pi pi-times"
className="p-button-text p-button-rounded"
onClick={annuler}
/>
<Button
label="Sauvegarder"
icon="pi pi-check"
className="p-button-text p-button-rounded"
onClick={saveMaintenance}
loading={saving}
/>
</div>
</div>
<Card>
<div className="p-fluid formgrid grid">
<div className="field col-12 md:col-6">
<label htmlFor="materiel" className="font-bold">
Matériel <span className="text-red-500">*</span>
</label>
<Dropdown
id="materiel"
value={maintenance.materiel}
onChange={(e) => onDropdownChange(e, 'materiel')}
options={materielOptions}
placeholder="Sélectionner un matériel"
filter
className={submitted && !maintenance.materiel ? 'p-invalid' : ''}
/>
{submitted && !maintenance.materiel && <small className="p-error">Le matériel est obligatoire.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="type" className="font-bold">Type</label>
<Dropdown
id="type"
value={maintenance.type}
onChange={(e) => onTypeChange(e.value)}
options={typeOptions}
placeholder="Sélectionner un type"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="statut" className="font-bold">Statut</label>
<Dropdown
id="statut"
value={maintenance.statut}
onChange={(e) => onDropdownChange(e, 'statut')}
options={statutOptions}
placeholder="Sélectionner un statut"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="datePrevue" className="font-bold">Date prévue</label>
<Calendar
id="datePrevue"
value={maintenance.datePrevue}
onChange={(e) => onDateChange(e, 'datePrevue')}
dateFormat="dd/mm/yy"
showIcon
showTime
hourFormat="24"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="dateRealisee" className="font-bold">Date de réalisation</label>
<Calendar
id="dateRealisee"
value={maintenance.dateRealisee}
onChange={(e) => onDateChange(e, 'dateRealisee')}
dateFormat="dd/mm/yy"
showIcon
showTime
hourFormat="24"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="cout" className="font-bold">Coût estimé</label>
<InputNumber
id="cout"
value={maintenance.cout}
onValueChange={(e) => onInputNumberChange(e.value, 'cout')}
mode="currency"
currency="EUR"
locale="fr-FR"
min={0}
/>
</div>
<div className="field col-12">
<label htmlFor="description" className="font-bold">
Description <span className="text-red-500">*</span>
</label>
<InputTextarea
id="description"
value={maintenance.description}
onChange={(e) => onInputChange(e, 'description')}
rows={3}
cols={20}
placeholder="Description détaillée de la maintenance..."
className={submitted && !maintenance.description ? 'p-invalid' : ''}
/>
{submitted && !maintenance.description && <small className="p-error">La description est obligatoire.</small>}
</div>
<div className="field col-12">
<label htmlFor="notes" className="font-bold">Notes</label>
<InputTextarea
id="notes"
value={maintenance.notes}
onChange={(e) => onInputChange(e, 'notes')}
rows={2}
cols={20}
placeholder="Notes et observations..."
/>
</div>
</div>
<div className="mt-4 p-3 surface-100 border-round">
<h5>Informations sur le matériel sélectionné</h5>
{maintenance.materiel ? (
<div className="grid">
<div className="col-12 md:col-6">
<strong>Nom:</strong> {maintenance.materiel.nom}
</div>
<div className="col-12 md:col-6">
<strong>Marque:</strong> {maintenance.materiel.marque}
</div>
<div className="col-12 md:col-6">
<strong>Modèle:</strong> {maintenance.materiel.modele}
</div>
<div className="col-12 md:col-6">
<strong>Statut:</strong> {maintenance.materiel.statut?.replace('_', ' ')}
</div>
<div className="col-12 md:col-6">
<strong>Localisation:</strong> {maintenance.materiel.localisation}
</div>
<div className="col-12 md:col-6">
<strong>Dernière maintenance:</strong>
{maintenance.materiel.maintenances?.length ?
new Date(maintenance.materiel.maintenances[0].dateRealisee || maintenance.materiel.maintenances[0].datePrevue).toLocaleDateString('fr-FR') :
'Aucune'
}
</div>
</div>
) : (
<p className="text-500">Sélectionnez un matériel pour voir ses informations</p>
)}
</div>
</Card>
</div>
</div>
);
};
export default NouvelleMaintenancePage;

View File

@@ -0,0 +1,508 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { InputNumber } from 'primereact/inputnumber';
import { Dropdown } from 'primereact/dropdown';
import { Calendar } from 'primereact/calendar';
import { InputTextarea } from 'primereact/inputtextarea';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { ConfirmDialog } from 'primereact/confirmdialog';
import { Tag } from 'primereact/tag';
import { FilterMatchMode } from 'primereact/api';
import { maintenanceService, materielService } from '../../../services/api';
import { MaintenanceMateriel, Materiel, TypeMaintenance, StatutMaintenance } from '../../../types/btp';
import { formatDate, formatCurrency } from '../../../utils/formatters';
const MaintenancesPage = () => {
const [maintenances, setMaintenances] = useState<MaintenanceMateriel[]>([]);
const [maintenance, setMaintenance] = useState<Partial<MaintenanceMateriel>>({});
const [materiels, setMateriels] = useState<Materiel[]>([]);
const [selectedMaintenances, setSelectedMaintenances] = useState<MaintenanceMateriel[]>([]);
const [maintenanceDialog, setMaintenanceDialog] = useState(false);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [submitted, setSubmitted] = useState(false);
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<MaintenanceMateriel[]>>(null);
const typeOptions = Object.values(TypeMaintenance).map(type => ({
label: type.replace('_', ' '),
value: type
}));
const statutOptions = Object.values(StatutMaintenance).map(statut => ({
label: statut.replace('_', ' '),
value: statut
}));
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
const [maintenancesData, materielsData] = await Promise.all([
maintenanceService.getAll(),
materielService.getAll()
]);
setMaintenances(maintenancesData);
setMateriels(materielsData);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données',
life: 3000
});
} finally {
setLoading(false);
}
};
const openNew = () => {
setMaintenance({
type: TypeMaintenance.PREVENTIVE,
statut: StatutMaintenance.PLANIFIEE,
datePrevue: new Date(),
cout: 0
});
setSubmitted(false);
setMaintenanceDialog(true);
};
const hideDialog = () => {
setSubmitted(false);
setMaintenanceDialog(false);
setMaintenance({});
};
const saveMaintenance = async () => {
setSubmitted(true);
if (maintenance.description?.trim() && maintenance.materiel) {
try {
let savedMaintenance: MaintenanceMateriel;
if (maintenance.id) {
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 {
savedMaintenance = await maintenanceService.create(maintenance);
setMaintenances([...maintenances, savedMaintenance]);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Maintenance créée',
life: 3000
});
}
hideDialog();
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors de la sauvegarde',
life: 3000
});
}
}
};
const editMaintenance = (maintenance: MaintenanceMateriel) => {
setMaintenance({ ...maintenance });
setMaintenanceDialog(true);
};
const confirmDeleteSelected = () => {
if (selectedMaintenances && selectedMaintenances.length > 0) {
// Implémenter la suppression en lot si nécessaire
}
};
const deleteMaintenance = async (maintenance: MaintenanceMateriel) => {
try {
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
});
}
};
const exportCSV = () => {
dt.current?.exportCSV();
};
const onInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string) => {
const val = (e.target && e.target.value) || '';
const _maintenance = { ...maintenance };
(_maintenance as any)[name] = val;
setMaintenance(_maintenance);
};
const onInputNumberChange = (value: number | null, name: string) => {
const _maintenance = { ...maintenance };
(_maintenance as any)[name] = value;
setMaintenance(_maintenance);
};
const onDropdownChange = (e: any, name: string) => {
const _maintenance = { ...maintenance };
(_maintenance as any)[name] = e.value;
setMaintenance(_maintenance);
};
const onDateChange = (e: any, name: string) => {
const _maintenance = { ...maintenance };
(_maintenance as any)[name] = e.value;
setMaintenance(_maintenance);
};
// Templates pour les colonnes
const materielBodyTemplate = (rowData: MaintenanceMateriel) => {
return rowData.materiel?.nom || 'N/A';
};
const typeBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<Tag
value={rowData.type?.replace('_', ' ')}
severity={getTypeSeverity(rowData.type)}
/>
);
};
const statutBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<Tag
value={rowData.statut?.replace('_', ' ')}
severity={getStatutSeverity(rowData.statut)}
/>
);
};
const coutBodyTemplate = (rowData: MaintenanceMateriel) => {
return formatCurrency(rowData.cout);
};
const dateBodyTemplate = (rowData: MaintenanceMateriel) => {
return formatDate(rowData.datePrevue);
};
const dateRealisationBodyTemplate = (rowData: MaintenanceMateriel) => {
return formatDate(rowData.dateRealisation);
};
const actionBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-pencil"
className="p-button-text p-button-rounded"
onClick={() => editMaintenance(rowData)}
/>
<Button
icon="pi pi-trash"
className="p-button-text p-button-rounded"
severity="danger"
onClick={() => deleteMaintenance(rowData)}
/>
</div>
);
};
const getTypeSeverity = (type?: TypeMaintenance) => {
switch (type) {
case TypeMaintenance.PREVENTIVE:
return 'success';
case TypeMaintenance.CORRECTIVE:
return 'danger';
case TypeMaintenance.PREDICTIVE:
return 'info';
case TypeMaintenance.CURATIVE:
return 'warning';
default:
return undefined;
}
};
const getStatutSeverity = (statut?: StatutMaintenance) => {
switch (statut) {
case StatutMaintenance.PLANIFIEE:
return 'info';
case StatutMaintenance.EN_COURS:
return 'warning';
case StatutMaintenance.TERMINEE:
return 'success';
case StatutMaintenance.ANNULEE:
return 'danger';
default:
return undefined;
}
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouveau"
icon="pi pi-plus"
severity="success"
className="mr-2 p-button-text p-button-rounded"
onClick={openNew}
/>
<Button
label="Supprimer"
icon="pi pi-trash"
severity="danger"
className="p-button-text p-button-rounded"
onClick={confirmDeleteSelected}
disabled={!selectedMaintenances || selectedMaintenances.length === 0}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
className="p-button-text p-button-rounded"
onClick={exportCSV}
/>
);
};
const header = (
<div className="flex flex-wrap gap-2 align-items-center justify-content-between">
<h4 className="m-0">Gestion des Maintenances</h4>
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
</div>
);
const maintenanceDialogFooter = (
<div className="flex gap-2">
<Button
label="Annuler"
icon="pi pi-times"
className="p-button-text p-button-rounded"
onClick={hideDialog}
/>
<Button
label="Sauvegarder"
icon="pi pi-check"
className="p-button-text p-button-rounded"
onClick={saveMaintenance}
/>
</div>
);
const filters = {
global: { value: globalFilter, matchMode: FilterMatchMode.CONTAINS }
};
const materielOptions = materiels.map(materiel => ({
label: materiel.nom,
value: materiel
}));
return (
<div className="grid">
<div className="col-12">
<Toast ref={toast} />
<ConfirmDialog />
<div className="card">
<Toolbar
className="mb-4"
left={leftToolbarTemplate}
right={rightToolbarTemplate}
/>
<DataTable
ref={dt}
value={maintenances}
selection={selectedMaintenances}
onSelectionChange={(e) => setSelectedMaintenances(e.value)}
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage {first} à {last} de {totalRecords} maintenances"
filters={filters}
filterDisplay="menu"
loading={loading}
globalFilterFields={['description', 'type', 'statut', 'materiel.nom']}
header={header}
emptyMessage="Aucune maintenance trouvée."
>
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
<Column field="materiel.nom" header="Matériel" body={materielBodyTemplate} sortable />
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
<Column field="description" header="Description" sortable />
<Column field="datePrevue" header="Date prévue" body={dateBodyTemplate} sortable />
<Column field="dateRealisation" header="Date réalisation" body={dateRealisationBodyTemplate} sortable />
<Column field="cout" header="Coût" body={coutBodyTemplate} sortable />
<Column body={actionBodyTemplate} exportable={false} style={{ minWidth: '8rem' }} />
</DataTable>
</div>
<Dialog
visible={maintenanceDialog}
style={{ width: '800px' }}
header="Détails de la maintenance"
modal
className="p-fluid"
footer={maintenanceDialogFooter}
onHide={hideDialog}
>
<div className="formgrid grid">
<div className="field col-12 md:col-6">
<label htmlFor="materiel" className="font-bold">
Matériel <span className="text-red-500">*</span>
</label>
<Dropdown
id="materiel"
value={maintenance.materiel}
onChange={(e) => onDropdownChange(e, 'materiel')}
options={materielOptions}
placeholder="Sélectionner un matériel"
className={submitted && !maintenance.materiel ? 'p-invalid' : ''}
/>
{submitted && !maintenance.materiel && <small className="p-error">Le matériel est obligatoire.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="type" className="font-bold">Type</label>
<Dropdown
id="type"
value={maintenance.type}
onChange={(e) => onDropdownChange(e, 'type')}
options={typeOptions}
placeholder="Sélectionner un type"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="statut" className="font-bold">Statut</label>
<Dropdown
id="statut"
value={maintenance.statut}
onChange={(e) => onDropdownChange(e, 'statut')}
options={statutOptions}
placeholder="Sélectionner un statut"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="datePrevue" className="font-bold">Date prévue</label>
<Calendar
id="datePrevue"
value={maintenance.datePrevue ? new Date(maintenance.datePrevue) : null}
onChange={(e) => onDateChange(e, 'datePrevue')}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="dateRealisation" className="font-bold">Date de réalisation</label>
<Calendar
id="dateRealisation"
value={maintenance.dateRealisation ? new Date(maintenance.dateRealisation) : null}
onChange={(e) => onDateChange(e, 'dateRealisation')}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="cout" className="font-bold">Coût</label>
<InputNumber
id="cout"
value={maintenance.cout}
onValueChange={(e) => onInputNumberChange(e.value, 'cout')}
mode="currency"
currency="EUR"
locale="fr-FR"
min={0}
/>
</div>
<div className="field col-12">
<label htmlFor="description" className="font-bold">
Description <span className="text-red-500">*</span>
</label>
<InputTextarea
id="description"
value={maintenance.description || ''}
onChange={(e) => onInputChange(e, 'description')}
rows={3}
cols={20}
className={submitted && !maintenance.description ? 'p-invalid' : ''}
/>
{submitted && !maintenance.description && <small className="p-error">La description est obligatoire.</small>}
</div>
<div className="field col-12">
<label htmlFor="notes" className="font-bold">Notes</label>
<InputTextarea
id="notes"
value={maintenance.notes || ''}
onChange={(e) => onInputChange(e, 'notes')}
rows={2}
cols={20}
/>
</div>
</div>
</Dialog>
</div>
</div>
);
};
export default MaintenancesPage;

View File

@@ -0,0 +1,448 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
import { Calendar } from 'primereact/calendar';
import { Dropdown } from 'primereact/dropdown';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Tag } from 'primereact/tag';
import { Card } from 'primereact/card';
import { FilterMatchMode } from 'primereact/api';
import { Timeline } from 'primereact/timeline';
import { maintenanceService } from '../../../../services/api';
import { MaintenanceMateriel, TypeMaintenance, StatutMaintenance } from '../../../../types/btp';
import { formatDate } from '../../../../utils/formatters';
const MaintenancesPlanifieesPage = () => {
const [maintenances, setMaintenances] = useState<MaintenanceMateriel[]>([]);
const [selectedMaintenances, setSelectedMaintenances] = useState<MaintenanceMateriel[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [dateDebut, setDateDebut] = useState<Date | null>(new Date());
const [dateFin, setDateFin] = useState<Date | null>(new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)); // +30 jours
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<MaintenanceMateriel[]>>(null);
useEffect(() => {
loadMaintenancesPlanifiees();
}, [dateDebut, dateFin]);
const loadMaintenancesPlanifiees = async () => {
try {
setLoading(true);
const data = await maintenanceService.getAll();
// Filtrer les maintenances planifiées
const planifiees = data.filter(m =>
m.statut === StatutMaintenance.PLANIFIEE &&
new Date(m.datePrevue) >= (dateDebut || new Date()) &&
new Date(m.datePrevue) <= (dateFin || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000))
);
setMaintenances(planifiees);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les maintenances planifiées',
life: 3000
});
} finally {
setLoading(false);
}
};
const demarrerMaintenance = async (maintenance: MaintenanceMateriel) => {
try {
const updatedMaintenance = {
...maintenance,
statut: StatutMaintenance.EN_COURS,
dateRealisee: new Date().toISOString()
};
await maintenanceService.update(maintenance.id, updatedMaintenance);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Maintenance démarrée',
life: 3000
});
loadMaintenancesPlanifiees();
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors du démarrage',
life: 3000
});
}
};
const reporterMaintenance = async (maintenance: MaintenanceMateriel, nouveauJours: number = 7) => {
try {
const nouvelleDate = new Date();
nouvelleDate.setDate(nouvelleDate.getDate() + nouveauJours);
const updatedMaintenance = {
...maintenance,
datePrevue: nouvelleDate,
notes: `${maintenance.notes || ''}\nReportée le ${formatDate(new Date())}`
};
await maintenanceService.update(maintenance.id, updatedMaintenance);
toast.current?.show({
severity: 'info',
summary: 'Maintenance reportée',
detail: `Nouvelle date: ${formatDate(nouvelleDate)}`,
life: 3000
});
loadMaintenancesPlanifiees();
} catch (error: any) {
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: error?.userMessage || 'Erreur lors du report',
life: 3000
});
}
};
const exportCSV = () => {
dt.current?.exportCSV();
};
// Templates pour les colonnes
const materielBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<div>
<div className="font-semibold">{rowData.materiel?.nom}</div>
<div className="text-sm text-500">{rowData.materiel?.marque} {rowData.materiel?.modele}</div>
</div>
);
};
const typeBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<Tag
value={rowData.type?.replace('_', ' ')}
severity={getTypeSeverity(rowData.type)}
/>
);
};
const prioriteBodyTemplate = (rowData: MaintenanceMateriel) => {
const daysDiff = Math.ceil((new Date(rowData.datePrevue).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
let severity;
let label;
if (daysDiff < 0) {
severity = 'danger';
label = 'EN RETARD';
} else if (daysDiff <= 3) {
severity = 'warning';
label = 'URGENT';
} else if (daysDiff <= 7) {
severity = 'info';
label = 'PRIORITAIRE';
} else {
severity = 'success';
label = 'NORMAL';
}
return <Tag value={label} severity={severity} />;
};
const dateBodyTemplate = (rowData: MaintenanceMateriel) => {
const date = new Date(rowData.datePrevue);
const today = new Date();
const daysDiff = Math.ceil((date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
return (
<div>
<div className={daysDiff < 0 ? 'text-red-500 font-semibold' : daysDiff <= 3 ? 'text-orange-500' : ''}>
{formatDate(rowData.datePrevue)}
</div>
<div className="text-xs text-500">
{daysDiff < 0 ? `${Math.abs(daysDiff)} jour(s) de retard` :
daysDiff === 0 ? 'Aujourd\'hui' :
`Dans ${daysDiff} jour(s)`}
</div>
</div>
);
};
const actionBodyTemplate = (rowData: MaintenanceMateriel) => {
return (
<div className="flex gap-1">
<Button
icon="pi pi-play"
size="small"
severity="success"
className="p-button-text p-button-rounded"
tooltip="Démarrer"
onClick={() => demarrerMaintenance(rowData)}
/>
<Button
icon="pi pi-calendar-plus"
size="small"
severity="info"
className="p-button-text p-button-rounded"
tooltip="Reporter (+7 jours)"
onClick={() => reporterMaintenance(rowData, 7)}
/>
<Button
icon="pi pi-calendar-times"
size="small"
severity="warning"
className="p-button-text p-button-rounded"
tooltip="Reporter (+14 jours)"
onClick={() => reporterMaintenance(rowData, 14)}
/>
</div>
);
};
const getTypeSeverity = (type?: TypeMaintenance) => {
switch (type) {
case TypeMaintenance.PREVENTIVE:
return 'success';
case TypeMaintenance.CORRECTIVE:
return 'danger';
case TypeMaintenance.REVISION:
return 'info';
case TypeMaintenance.CONTROLE_TECHNIQUE:
return 'warning';
case TypeMaintenance.NETTOYAGE:
return 'secondary';
default:
return undefined;
}
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2 align-items-center">
<h4 className="m-0">Maintenances Planifiées</h4>
<span className="text-500">({maintenances.length} maintenances)</span>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
className="p-button-text p-button-rounded"
onClick={exportCSV}
/>
);
};
const header = (
<div className="flex flex-wrap gap-2 align-items-center justify-content-between">
<div className="flex flex-wrap gap-2 align-items-center">
<Calendar
placeholder="Date début"
value={dateDebut}
onChange={(e) => setDateDebut(e.value)}
dateFormat="dd/mm/yy"
showIcon
/>
<Calendar
placeholder="Date fin"
value={dateFin}
onChange={(e) => setDateFin(e.value)}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
</div>
);
const filters = {
global: { value: globalFilter, matchMode: FilterMatchMode.CONTAINS }
};
// Statistiques pour les cards
const maintenancesUrgentes = maintenances.filter(m => {
const daysDiff = Math.ceil((new Date(m.datePrevue).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
return daysDiff <= 3;
}).length;
const maintenancesRetard = maintenances.filter(m => {
const daysDiff = Math.ceil((new Date(m.datePrevue).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
return daysDiff < 0;
}).length;
const maintenancesSemaine = maintenances.filter(m => {
const daysDiff = Math.ceil((new Date(m.datePrevue).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
return daysDiff >= 0 && daysDiff <= 7;
}).length;
// Timeline des prochaines maintenances
const timelineEvents = maintenances
.sort((a, b) => new Date(a.datePrevue).getTime() - new Date(b.datePrevue).getTime())
.slice(0, 5)
.map(m => ({
date: formatDate(m.datePrevue),
icon: 'pi pi-wrench',
color: new Date(m.datePrevue) < new Date() ? '#e74c3c' : '#3498db',
content: (
<div>
<div className="font-semibold">{m.materiel?.nom}</div>
<div className="text-sm text-500">{m.description}</div>
<Tag value={m.type?.replace('_', ' ')} severity={getTypeSeverity(m.type)} size="small" className="mt-1" />
</div>
)
}));
return (
<div className="grid">
<div className="col-12">
<Toast ref={toast} />
{/* Cards de statistiques */}
<div className="grid mb-4">
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="mr-3">
<i className="pi pi-clock text-blue-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenances.length}</div>
<div className="text-500">Planifiées</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="mr-3">
<i className="pi pi-exclamation-triangle text-orange-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenancesUrgentes}</div>
<div className="text-500">Urgentes</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="mr-3">
<i className="pi pi-times-circle text-red-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenancesRetard}</div>
<div className="text-500">En retard</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="mr-3">
<i className="pi pi-calendar text-green-500 text-3xl"></i>
</div>
<div>
<div className="text-2xl font-semibold">{maintenancesSemaine}</div>
<div className="text-500">Cette semaine</div>
</div>
</div>
</Card>
</div>
</div>
<div className="grid">
<div className="col-12 lg:col-8">
<div className="card">
<Toolbar
className="mb-4"
left={leftToolbarTemplate}
right={rightToolbarTemplate}
/>
<DataTable
ref={dt}
value={maintenances}
selection={selectedMaintenances}
onSelectionChange={(e) => setSelectedMaintenances(e.value)}
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage {first} à {last} de {totalRecords} maintenances"
filters={filters}
filterDisplay="menu"
loading={loading}
globalFilterFields={['description', 'materiel.nom', 'type']}
header={header}
emptyMessage="Aucune maintenance planifiée."
sortField="datePrevue"
sortOrder={1}
>
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
<Column header="Matériel" body={materielBodyTemplate} />
<Column field="type" header="Type" body={typeBodyTemplate} />
<Column header="Priorité" body={prioriteBodyTemplate} />
<Column field="datePrevue" header="Date prévue" body={dateBodyTemplate} sortable />
<Column field="description" header="Description" />
<Column body={actionBodyTemplate} exportable={false} style={{ minWidth: '8rem' }} />
</DataTable>
</div>
</div>
<div className="col-12 lg:col-4">
<Card title="Prochaines maintenances">
<Timeline
value={timelineEvents}
align="left"
className="customized-timeline"
marker={(item) => (
<span
className="flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1 shadow-1"
style={{ backgroundColor: item.color }}
>
<i className={item.icon}></i>
</span>
)}
content={(item) => item.content}
opposite={(item) => <small className="text-color-secondary">{item.date}</small>}
/>
</Card>
</div>
</div>
</div>
</div>
);
};
export default MaintenancesPlanifieesPage;