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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,448 @@
'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;