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

448 lines
17 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { Calendar } from 'primereact/calendar';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Toast } from 'primereact/toast';
import { Tag } from 'primereact/tag';
import { Toolbar } from 'primereact/toolbar';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { Dialog } from 'primereact/dialog';
import { Timeline } from 'primereact/timeline';
import { chantierService } from '../../../services/api';
import { formatDate } from '../../../utils/formatters';
import type { Chantier } from '../../../types/btp';
interface PlanningEvent {
id: string;
title: string;
date: Date;
type: 'debut' | 'fin' | 'milestone';
chantier: Chantier;
status: string;
}
const PlanningPage = () => {
const [chantiers, setChantiers] = useState<Chantier[]>([]);
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [planningEvents, setPlanningEvents] = useState<PlanningEvent[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [viewMode, setViewMode] = useState<'calendar' | 'table' | 'timeline'>('calendar');
const [filteredChantiers, setFilteredChantiers] = useState<Chantier[]>([]);
const [eventDialog, setEventDialog] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<PlanningEvent | null>(null);
const toast = useRef<Toast>(null);
const viewModeOptions = [
{ label: 'Calendrier', value: 'calendar' },
{ label: 'Tableau', value: 'table' },
{ label: 'Timeline', value: 'timeline' }
];
const statutOptions = [
{ label: 'Tous', value: '' },
{ label: 'Planifié', value: 'PLANIFIE' },
{ label: 'En cours', value: 'EN_COURS' },
{ label: 'Terminé', value: 'TERMINE' },
{ label: 'Suspendu', value: 'SUSPENDU' }
];
useEffect(() => {
loadChantiers();
}, []);
useEffect(() => {
generatePlanningEvents();
}, [chantiers, selectedDate]);
const loadChantiers = async () => {
try {
setLoading(true);
const data = await chantierService.getAll();
setChantiers(data);
setFilteredChantiers(data);
} catch (error) {
console.error('Erreur lors du chargement des chantiers:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les chantiers',
life: 3000
});
} finally {
setLoading(false);
}
};
const generatePlanningEvents = () => {
const events: PlanningEvent[] = [];
chantiers.forEach(chantier => {
// Événement de début
events.push({
id: `${chantier.id}-debut`,
title: `Début: ${chantier.nom}`,
date: new Date(chantier.dateDebut),
type: 'debut',
chantier,
status: chantier.statut
});
// Événement de fin prévue
if (chantier.dateFinPrevue) {
events.push({
id: `${chantier.id}-fin-prevue`,
title: `Fin prévue: ${chantier.nom}`,
date: new Date(chantier.dateFinPrevue),
type: 'fin',
chantier,
status: chantier.statut
});
}
// Événement de fin réelle
if (chantier.dateFinReelle) {
events.push({
id: `${chantier.id}-fin-reelle`,
title: `Fin réelle: ${chantier.nom}`,
date: new Date(chantier.dateFinReelle),
type: 'fin',
chantier,
status: chantier.statut
});
}
});
setPlanningEvents(events.sort((a, b) => a.date.getTime() - b.date.getTime()));
};
const filterChantiersForDate = (date: Date) => {
return chantiers.filter(chantier => {
const dateDebut = new Date(chantier.dateDebut);
const dateFin = chantier.dateFinPrevue ? new Date(chantier.dateFinPrevue) : new Date();
return date >= dateDebut && date <= dateFin;
});
};
const getEventsForDate = (date: Date) => {
return planningEvents.filter(event => {
const eventDate = new Date(event.date);
return eventDate.toDateString() === date.toDateString();
});
};
const onDateSelect = (e: any) => {
setSelectedDate(e.value);
const chantiersForDate = filterChantiersForDate(e.value);
setFilteredChantiers(chantiersForDate);
};
const onViewModeChange = (e: any) => {
setViewMode(e.value);
};
const onEventClick = (event: PlanningEvent) => {
setSelectedEvent(event);
setEventDialog(true);
};
const hideEventDialog = () => {
setEventDialog(false);
setSelectedEvent(null);
};
const statusBodyTemplate = (rowData: Chantier) => {
const getSeverity = (status: string) => {
switch (status) {
case 'PLANIFIE': return 'info';
case 'EN_COURS': return 'success';
case 'TERMINE': return 'secondary';
case 'ANNULE': return 'danger';
case 'SUSPENDU': return 'warning';
default: return 'info';
}
};
const getLabel = (status: string) => {
switch (status) {
case 'PLANIFIE': return 'Planifié';
case 'EN_COURS': return 'En cours';
case 'TERMINE': return 'Terminé';
case 'ANNULE': return 'Annulé';
case 'SUSPENDU': return 'Suspendu';
default: return status;
}
};
return (
<Tag
value={getLabel(rowData.statut)}
severity={getSeverity(rowData.statut)}
/>
);
};
const dateBodyTemplate = (rowData: Chantier, field: string) => {
const date = (rowData as any)[field];
return date ? formatDate(date) : '';
};
const clientBodyTemplate = (rowData: Chantier) => {
if (!rowData.client) return '';
return `${rowData.client.prenom} ${rowData.client.nom}`;
};
const dureeBodyTemplate = (rowData: Chantier) => {
if (!rowData.dateFinPrevue) return '';
const debut = new Date(rowData.dateDebut);
const fin = new Date(rowData.dateFinPrevue);
const diffTime = fin.getTime() - debut.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return `${diffDays} jours`;
};
const timelineTemplate = (item: PlanningEvent) => {
return (
<div className="flex flex-column">
<div className="flex align-items-center">
<Tag
value={item.type === 'debut' ? 'Début' : 'Fin'}
severity={item.type === 'debut' ? 'success' : 'info'}
className="mr-2"
/>
<span className="font-semibold">{item.chantier.nom}</span>
</div>
<div className="text-sm text-color-secondary mt-1">
{item.chantier.client ? `${item.chantier.client.prenom} ${item.chantier.client.nom}` : 'Client non défini'}
</div>
<div className="text-sm text-color-secondary">
{formatDate(item.date)}
</div>
</div>
);
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Aujourd'hui"
icon="pi pi-calendar"
onClick={() => setSelectedDate(new Date())}
className="p-button-outlined"
/>
<Dropdown
value={viewMode}
options={viewModeOptions}
onChange={onViewModeChange}
placeholder="Mode d'affichage"
/>
</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="Rafraîchir"
icon="pi pi-refresh"
onClick={loadChantiers}
className="p-button-outlined"
/>
</div>
);
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Planning des Chantiers</h5>
<div className="flex flex-wrap gap-2 mt-2 md:mt-0">
<Calendar
value={selectedDate}
onChange={onDateSelect}
dateFormat="dd/mm/yy"
showIcon
placeholder="Sélectionner une date"
/>
</div>
</div>
);
const eventDialogFooter = (
<Button label="Fermer" icon="pi pi-times" onClick={hideEventDialog} />
);
const renderCalendarView = () => {
const eventsForDate = getEventsForDate(selectedDate);
return (
<div className="grid">
<div className="col-12 md:col-6">
<Card title="Chantiers en cours">
<DataTable
value={filteredChantiers}
paginator
rows={10}
dataKey="id"
loading={loading}
emptyMessage="Aucun chantier pour cette date"
globalFilter={globalFilter}
>
<Column field="nom" header="Nom" sortable />
<Column field="client" header="Client" body={clientBodyTemplate} />
<Column field="statut" header="Statut" body={statusBodyTemplate} />
<Column field="dateDebut" header="Début" body={(rowData) => dateBodyTemplate(rowData, 'dateDebut')} />
<Column field="dateFinPrevue" header="Fin prévue" body={(rowData) => dateBodyTemplate(rowData, 'dateFinPrevue')} />
</DataTable>
</Card>
</div>
<div className="col-12 md:col-6">
<Card title="Événements du jour">
{eventsForDate.length > 0 ? (
<div className="flex flex-column gap-3">
{eventsForDate.map(event => (
<div
key={event.id}
className="p-3 border-1 border-300 border-round cursor-pointer hover:bg-gray-50"
onClick={() => onEventClick(event)}
>
<div className="flex justify-content-between align-items-center">
<span className="font-semibold">{event.title}</span>
<Tag
value={event.type === 'debut' ? 'Début' : 'Fin'}
severity={event.type === 'debut' ? 'success' : 'info'}
/>
</div>
<div className="text-sm text-color-secondary mt-1">
{event.chantier.adresse}
</div>
</div>
))}
</div>
) : (
<p className="text-color-secondary">Aucun événement pour cette date</p>
)}
</Card>
</div>
</div>
);
};
const renderTableView = () => {
return (
<DataTable
value={filteredChantiers}
paginator
rows={10}
dataKey="id"
loading={loading}
globalFilter={globalFilter}
emptyMessage="Aucun chantier trouvé"
header={header}
>
<Column field="nom" header="Nom" sortable />
<Column field="client" header="Client" body={clientBodyTemplate} sortable />
<Column field="adresse" header="Adresse" sortable />
<Column field="dateDebut" header="Date début" body={(rowData) => dateBodyTemplate(rowData, 'dateDebut')} sortable />
<Column field="dateFinPrevue" header="Date fin prévue" body={(rowData) => dateBodyTemplate(rowData, 'dateFinPrevue')} sortable />
<Column field="duree" header="Durée" body={dureeBodyTemplate} />
<Column field="statut" header="Statut" body={statusBodyTemplate} sortable />
</DataTable>
);
};
const renderTimelineView = () => {
return (
<div className="grid">
<div className="col-12">
<Card title="Timeline des événements">
<Timeline
value={planningEvents}
content={timelineTemplate}
align="left"
className="w-full"
/>
</Card>
</div>
</div>
);
};
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
{viewMode === 'calendar' && renderCalendarView()}
{viewMode === 'table' && renderTableView()}
{viewMode === 'timeline' && renderTimelineView()}
<Dialog
visible={eventDialog}
style={{ width: '450px' }}
header="Détails de l'événement"
modal
className="p-fluid"
footer={eventDialogFooter}
onHide={hideEventDialog}
>
{selectedEvent && (
<div className="flex flex-column gap-3">
<div>
<label className="font-semibold">Événement</label>
<p>{selectedEvent.title}</p>
</div>
<div>
<label className="font-semibold">Date</label>
<p>{formatDate(selectedEvent.date)}</p>
</div>
<div>
<label className="font-semibold">Chantier</label>
<p>{selectedEvent.chantier.nom}</p>
</div>
<div>
<label className="font-semibold">Client</label>
<p>{selectedEvent.chantier.client ?
`${selectedEvent.chantier.client.prenom} ${selectedEvent.chantier.client.nom}` :
'Non défini'}</p>
</div>
<div>
<label className="font-semibold">Adresse</label>
<p>{selectedEvent.chantier.adresse}</p>
</div>
<div>
<label className="font-semibold">Statut</label>
<Tag
value={selectedEvent.status}
severity={statusBodyTemplate(selectedEvent.chantier).props.severity}
/>
</div>
</div>
)}
</Dialog>
</Card>
</div>
</div>
);
};
export default PlanningPage;