Files
btpxpress-frontend/app/(main)/maintenance/calendrier/page.tsx
2025-10-01 01:39:07 +00:00

566 lines
25 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { Calendar } from 'primereact/calendar';
import { Dropdown } from 'primereact/dropdown';
import { Toolbar } from 'primereact/toolbar';
import { Badge } from 'primereact/badge';
import { Tag } from 'primereact/tag';
import { Dialog } from 'primereact/dialog';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { useRouter } from 'next/navigation';
import { apiClient } from '../../../../services/api-client';
interface EvenementMaintenance {
id: number;
title: string;
start: Date;
end?: Date;
type: 'PREVENTIVE' | 'CORRECTIVE' | 'PLANIFIEE' | 'URGENTE';
statut: 'PLANIFIEE' | 'EN_COURS' | 'TERMINEE' | 'ANNULEE';
priorite: 'BASSE' | 'NORMALE' | 'HAUTE' | 'CRITIQUE';
materielNom: string;
technicienNom?: string;
description: string;
dureeEstimee: number;
color: string;
}
interface VueCalendrier {
date: Date;
evenements: EvenementMaintenance[];
conflits: Array<{
technicienId: number;
technicienNom: string;
maintenances: EvenementMaintenance[];
}>;
}
const CalendrierMaintenancePage = () => {
const [vueCalendrier, setVueCalendrier] = useState<VueCalendrier>({
date: new Date(),
evenements: [],
conflits: []
});
const [loading, setLoading] = useState(true);
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [filterType, setFilterType] = useState<string | null>(null);
const [filterTechnicien, setFilterTechnicien] = useState<string | null>(null);
const [detailDialog, setDetailDialog] = useState(false);
const [selectedEvenement, setSelectedEvenement] = useState<EvenementMaintenance | null>(null);
const [techniciens, setTechniciens] = useState<any[]>([]);
const [vueMode, setVueMode] = useState<'mois' | 'semaine' | 'jour'>('mois');
const router = useRouter();
const typeOptions = [
{ label: 'Tous', value: null },
{ label: 'Préventive', value: 'PREVENTIVE' },
{ label: 'Corrective', value: 'CORRECTIVE' },
{ label: 'Planifiée', value: 'PLANIFIEE' },
{ label: 'Urgente', value: 'URGENTE' }
];
const vueModeOptions = [
{ label: 'Mois', value: 'mois' },
{ label: 'Semaine', value: 'semaine' },
{ label: 'Jour', value: 'jour' }
];
useEffect(() => {
loadCalendrierMaintenance();
loadTechniciens();
}, [selectedDate, filterType, filterTechnicien, vueMode]);
const loadCalendrierMaintenance = async () => {
try {
setLoading(true);
console.log('🔄 Chargement du calendrier de maintenance...');
const params = new URLSearchParams();
params.append('date', selectedDate.toISOString().split('T')[0]);
params.append('vue', vueMode);
if (filterType) params.append('type', filterType);
if (filterTechnicien) params.append('technicienId', filterTechnicien);
const response = await apiClient.get(`/api/maintenances/calendrier?${params.toString()}`);
console.log('✅ Calendrier chargé:', response.data);
// Transformer les données pour le calendrier
const evenements = response.data.maintenances?.map((m: any) => ({
...m,
start: new Date(m.datePlanifiee),
end: m.dateFin ? new Date(m.dateFin) : new Date(new Date(m.datePlanifiee).getTime() + m.dureeEstimee * 60 * 60 * 1000),
title: `${m.materielNom} - ${m.typeMaintenance}`,
color: getTypeColor(m.typeMaintenance, m.statut)
})) || [];
setVueCalendrier({
date: selectedDate,
evenements,
conflits: response.data.conflits || []
});
} catch (error) {
console.error('❌ Erreur lors du chargement du calendrier:', error);
setVueCalendrier({ date: selectedDate, evenements: [], conflits: [] });
} finally {
setLoading(false);
}
};
const loadTechniciens = async () => {
try {
const response = await apiClient.get('/api/employes/techniciens');
setTechniciens(response.data || []);
} catch (error) {
console.error('❌ Erreur lors du chargement des techniciens:', error);
}
};
const getTypeColor = (type: string, statut: string) => {
if (statut === 'TERMINEE') return '#10b981'; // vert
if (statut === 'ANNULEE') return '#6b7280'; // gris
switch (type) {
case 'PREVENTIVE': return '#3b82f6'; // bleu
case 'CORRECTIVE': return '#f59e0b'; // orange
case 'PLANIFIEE': return '#8b5cf6'; // violet
case 'URGENTE': return '#ef4444'; // rouge
default: return '#6b7280';
}
};
const getStatutSeverity = (statut: string) => {
switch (statut) {
case 'PLANIFIEE': return 'info';
case 'EN_COURS': return 'warning';
case 'TERMINEE': return 'success';
case 'ANNULEE': return 'danger';
default: return 'secondary';
}
};
const getTypeSeverity = (type: string) => {
switch (type) {
case 'PREVENTIVE': return 'info';
case 'CORRECTIVE': return 'warning';
case 'PLANIFIEE': return 'success';
case 'URGENTE': return 'danger';
default: return 'secondary';
}
};
const ouvrirDetail = (evenement: EvenementMaintenance) => {
setSelectedEvenement(evenement);
setDetailDialog(true);
};
const planifierMaintenance = () => {
router.push(`/maintenance/nouveau?date=${selectedDate.toISOString().split('T')[0]}`);
};
const resoudreConflit = async (conflitId: number) => {
try {
await apiClient.post(`/api/maintenances/conflits/${conflitId}/resoudre`);
console.log('✅ Conflit résolu');
loadCalendrierMaintenance();
} catch (error) {
console.error('❌ Erreur lors de la résolution du conflit:', error);
}
};
const genererJoursCalendrier = () => {
const debut = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1);
const fin = new Date(selectedDate.getFullYear(), selectedDate.getMonth() + 1, 0);
const jours = [];
// Ajouter les jours du mois précédent pour compléter la première semaine
const premierJourSemaine = debut.getDay();
for (let i = premierJourSemaine - 1; i >= 0; i--) {
const jour = new Date(debut);
jour.setDate(jour.getDate() - i - 1);
jours.push(jour);
}
// Ajouter tous les jours du mois
for (let jour = 1; jour <= fin.getDate(); jour++) {
jours.push(new Date(selectedDate.getFullYear(), selectedDate.getMonth(), jour));
}
// Ajouter les jours du mois suivant pour compléter la dernière semaine
const dernierJourSemaine = fin.getDay();
for (let i = 1; i <= 6 - dernierJourSemaine; i++) {
const jour = new Date(fin);
jour.setDate(jour.getDate() + i);
jours.push(jour);
}
return jours;
};
const getEvenementsJour = (jour: Date) => {
return vueCalendrier.evenements.filter(evt => {
const dateEvt = new Date(evt.start);
return dateEvt.toDateString() === jour.toDateString();
});
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Retour Maintenance"
icon="pi pi-arrow-left"
className="p-button-outlined"
onClick={() => router.push('/maintenance')}
/>
<Button
label="Nouvelle Maintenance"
icon="pi pi-plus"
className="p-button-success"
onClick={planifierMaintenance}
/>
<Button
label="Optimiser Planning"
icon="pi pi-cog"
className="p-button-info"
onClick={() => router.push('/maintenance/optimiser-planning')}
/>
<Button
label="Exporter"
icon="pi pi-download"
className="p-button-secondary"
onClick={() => router.push('/maintenance/export-calendrier')}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex gap-2">
<Dropdown
value={vueMode}
options={vueModeOptions}
onChange={(e) => setVueMode(e.value)}
placeholder="Vue"
/>
<Button
icon="pi pi-chevron-left"
className="p-button-outlined"
onClick={() => {
const nouvellDate = new Date(selectedDate);
if (vueMode === 'mois') {
nouvellDate.setMonth(nouvellDate.getMonth() - 1);
} else if (vueMode === 'semaine') {
nouvellDate.setDate(nouvellDate.getDate() - 7);
} else {
nouvellDate.setDate(nouvellDate.getDate() - 1);
}
setSelectedDate(nouvellDate);
}}
/>
<Button
label="Aujourd'hui"
className="p-button-outlined"
onClick={() => setSelectedDate(new Date())}
/>
<Button
icon="pi pi-chevron-right"
className="p-button-outlined"
onClick={() => {
const nouvellDate = new Date(selectedDate);
if (vueMode === 'mois') {
nouvellDate.setMonth(nouvellDate.getMonth() + 1);
} else if (vueMode === 'semaine') {
nouvellDate.setDate(nouvellDate.getDate() + 7);
} else {
nouvellDate.setDate(nouvellDate.getDate() + 1);
}
setSelectedDate(nouvellDate);
}}
/>
<Button
icon="pi pi-refresh"
className="p-button-outlined"
onClick={loadCalendrierMaintenance}
tooltip="Actualiser"
/>
</div>
);
};
const conflitBodyTemplate = (rowData: any) => {
return (
<div className="flex gap-2">
<Button
label="Résoudre"
icon="pi pi-check"
className="p-button-success p-button-sm"
onClick={() => resoudreConflit(rowData.id)}
/>
<Button
label="Reporter"
icon="pi pi-calendar"
className="p-button-warning p-button-sm"
onClick={() => router.push(`/maintenance/reporter/${rowData.id}`)}
/>
</div>
);
};
return (
<div className="grid">
<div className="col-12">
<Toolbar
className="mb-4"
left={leftToolbarTemplate}
right={rightToolbarTemplate}
/>
</div>
{/* Filtres */}
<div className="col-12">
<Card className="mb-4">
<div className="grid">
<div className="col-12 md:col-3">
<label className="font-medium mb-2 block">Type de maintenance</label>
<Dropdown
value={filterType}
options={typeOptions}
onChange={(e) => setFilterType(e.value)}
placeholder="Tous les types"
/>
</div>
<div className="col-12 md:col-3">
<label className="font-medium mb-2 block">Technicien</label>
<Dropdown
value={filterTechnicien}
options={techniciens}
onChange={(e) => setFilterTechnicien(e.value)}
optionLabel="nom"
optionValue="id"
placeholder="Tous les techniciens"
filter
/>
</div>
<div className="col-12 md:col-3">
<label className="font-medium mb-2 block">Date</label>
<Calendar
value={selectedDate}
onChange={(e) => setSelectedDate(e.value as Date)}
showIcon
dateFormat="dd/mm/yy"
placeholder="Sélectionner une date"
/>
</div>
<div className="col-12 md:col-3">
<label className="font-medium mb-2 block">Actions</label>
<Button
label="Réinitialiser"
icon="pi pi-filter-slash"
className="p-button-outlined w-full"
onClick={() => {
setFilterType(null);
setFilterTechnicien(null);
setSelectedDate(new Date());
}}
/>
</div>
</div>
</Card>
</div>
{/* Calendrier */}
<div className="col-12 lg:col-8">
<Card title={`Calendrier de Maintenance - ${selectedDate.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}`}>
{vueMode === 'mois' && (
<div className="grid">
{/* En-têtes des jours */}
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map(jour => (
<div key={jour} className="col-12 md:col text-center font-medium p-2 bg-gray-100">
{jour}
</div>
))}
{/* Jours du calendrier */}
{genererJoursCalendrier().map((jour, index) => {
const evenementsJour = getEvenementsJour(jour);
const estAujourdhui = jour.toDateString() === new Date().toDateString();
const estMoisCourant = jour.getMonth() === selectedDate.getMonth();
return (
<div
key={index}
className={`col-12 md:col border-1 border-gray-200 p-2 min-h-6rem cursor-pointer
${estAujourdhui ? 'bg-blue-50' : ''}
${!estMoisCourant ? 'text-gray-400' : ''}
`}
onClick={() => setSelectedDate(jour)}
>
<div className={`font-medium mb-1 ${estAujourdhui ? 'text-blue-600' : ''}`}>
{jour.getDate()}
</div>
<div className="flex flex-column gap-1">
{evenementsJour.slice(0, 3).map(evt => (
<div
key={evt.id}
className="text-xs p-1 border-round cursor-pointer"
style={{ backgroundColor: evt.color, color: 'white' }}
onClick={(e) => {
e.stopPropagation();
ouvrirDetail(evt);
}}
>
{evt.title.length > 20 ? `${evt.title.substring(0, 20)}...` : evt.title}
</div>
))}
{evenementsJour.length > 3 && (
<div className="text-xs text-500">
+{evenementsJour.length - 3} autres
</div>
)}
</div>
</div>
);
})}
</div>
)}
{vueMode === 'jour' && (
<div>
<h3>{selectedDate.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}</h3>
<div className="flex flex-column gap-2">
{getEvenementsJour(selectedDate).map(evt => (
<div
key={evt.id}
className="p-3 border-round cursor-pointer"
style={{ backgroundColor: evt.color + '20', borderLeft: `4px solid ${evt.color}` }}
onClick={() => ouvrirDetail(evt)}
>
<div className="flex justify-content-between align-items-center">
<div>
<div className="font-medium">{evt.title}</div>
<div className="text-sm text-500">{evt.description}</div>
<div className="text-xs text-500 mt-1">
{evt.start.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
{evt.end && ` - ${evt.end.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`}
</div>
</div>
<div className="flex flex-column gap-1">
<Tag value={evt.type} severity={getTypeSeverity(evt.type)} />
<Tag value={evt.statut} severity={getStatutSeverity(evt.statut)} />
</div>
</div>
</div>
))}
{getEvenementsJour(selectedDate).length === 0 && (
<div className="text-center text-500 p-4">
Aucune maintenance planifiée pour cette date
</div>
)}
</div>
</div>
)}
</Card>
</div>
{/* Panneau latéral */}
<div className="col-12 lg:col-4">
{/* Conflits */}
{vueCalendrier.conflits.length > 0 && (
<Card title="⚠️ Conflits de Planning" className="mb-4">
<DataTable value={vueCalendrier.conflits} responsiveLayout="scroll">
<Column field="technicienNom" header="Technicien" />
<Column
field="maintenances"
header="Conflits"
body={(rowData) => <Badge value={rowData.maintenances.length} severity="danger" />}
/>
<Column body={conflitBodyTemplate} header="Actions" />
</DataTable>
</Card>
)}
{/* Statistiques */}
<Card title="📊 Statistiques">
<div className="flex flex-column gap-3">
<div className="flex justify-content-between">
<span>Total maintenances:</span>
<Badge value={vueCalendrier.evenements.length} severity="info" />
</div>
<div className="flex justify-content-between">
<span>Urgentes:</span>
<Badge value={vueCalendrier.evenements.filter(e => e.type === 'URGENTE').length} severity="danger" />
</div>
<div className="flex justify-content-between">
<span>Préventives:</span>
<Badge value={vueCalendrier.evenements.filter(e => e.type === 'PREVENTIVE').length} severity="info" />
</div>
<div className="flex justify-content-between">
<span>En cours:</span>
<Badge value={vueCalendrier.evenements.filter(e => e.statut === 'EN_COURS').length} severity="warning" />
</div>
<div className="flex justify-content-between">
<span>Terminées:</span>
<Badge value={vueCalendrier.evenements.filter(e => e.statut === 'TERMINEE').length} severity="success" />
</div>
</div>
</Card>
</div>
{/* Dialog Détail Événement */}
<Dialog
visible={detailDialog}
style={{ width: '50vw' }}
header="Détail de la Maintenance"
modal
onHide={() => setDetailDialog(false)}
footer={
<div>
<Button
label="Fermer"
icon="pi pi-times"
className="p-button-outlined"
onClick={() => setDetailDialog(false)}
/>
<Button
label="Voir Détails"
icon="pi pi-eye"
className="p-button-info"
onClick={() => {
if (selectedEvenement) {
router.push(`/maintenance/${selectedEvenement.id}`);
}
}}
/>
</div>
}
>
{selectedEvenement && (
<div className="grid">
<div className="col-12">
<h4>{selectedEvenement.title}</h4>
<div className="flex gap-2 mb-3">
<Tag value={selectedEvenement.type} severity={getTypeSeverity(selectedEvenement.type)} />
<Tag value={selectedEvenement.statut} severity={getStatutSeverity(selectedEvenement.statut)} />
</div>
</div>
<div className="col-12">
<p><strong>Matériel:</strong> {selectedEvenement.materielNom}</p>
<p><strong>Technicien:</strong> {selectedEvenement.technicienNom || 'Non assigné'}</p>
<p><strong>Date:</strong> {selectedEvenement.start.toLocaleDateString('fr-FR')}</p>
<p><strong>Durée estimée:</strong> {selectedEvenement.dureeEstimee}h</p>
<p><strong>Description:</strong> {selectedEvenement.description}</p>
</div>
</div>
)}
</Dialog>
</div>
);
};
export default CalendrierMaintenancePage;