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

1013 lines
44 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 { InputTextarea } from 'primereact/inputtextarea';
import { MultiSelect } from 'primereact/multiselect';
import { Splitter, SplitterPanel } from 'primereact/splitter';
import { Panel } from 'primereact/panel';
import { ProgressBar } from 'primereact/progressbar';
import { Badge } from 'primereact/badge';
import { Timeline } from 'primereact/timeline';
import { Divider } from 'primereact/divider';
import { chantierService } from '../../../../services/api';
import { formatDate, formatDateTime } from '../../../../utils/formatters';
import type { Chantier, PlanningEvent as PlanningEventType, Equipe, Materiel, Employe, PlanningConflict } from '../../../../types/btp';
interface PlanningCalendrierEvent {
id: string;
title: string;
start: Date;
end: Date;
allDay?: boolean;
resource?: any;
type: 'chantier' | 'reunion' | 'formation' | 'maintenance' | 'conge' | 'autre';
priority: 'basse' | 'normale' | 'haute' | 'critique';
status: 'planifie' | 'confirme' | 'en_cours' | 'termine' | 'annule';
chantier?: Chantier;
equipe?: Equipe;
employes?: Employe[];
materiels?: Materiel[];
color?: string;
description?: string;
}
const CalendrierPlanningPage = () => {
const [events, setEvents] = useState<PlanningCalendrierEvent[]>([]);
const [chantiers, setChantiers] = useState<Chantier[]>([]);
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [selectedEvent, setSelectedEvent] = useState<PlanningCalendrierEvent | null>(null);
const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState<'month' | 'week' | 'day' | 'agenda'>('month');
const [filterType, setFilterType] = useState<string>('');
const [filterEquipe, setFilterEquipe] = useState<string>('');
const [filterPriorite, setFilterPriorite] = useState<string>('');
const [globalFilter, setGlobalFilter] = useState('');
// Dialogs
const [eventDialog, setEventDialog] = useState(false);
const [conflictDialog, setConflictDialog] = useState(false);
const [newEventDialog, setNewEventDialog] = useState(false);
// State pour nouvel événement
const [newEvent, setNewEvent] = useState<Partial<PlanningCalendrierEvent>>({
title: '',
description: '',
start: new Date(),
end: new Date(Date.now() + 2 * 60 * 60 * 1000), // +2h par défaut
type: 'autre',
priority: 'normale',
status: 'planifie',
allDay: false
});
const [conflicts, setConflicts] = useState<PlanningConflict[]>([]);
const [stats, setStats] = useState({
totalEvents: 0,
eventsAujourdHui: 0,
eventsSemaine: 0,
eventsCritiques: 0,
tauxOccupation: 0
});
const toast = useRef<Toast>(null);
const typeOptions = [
{ label: 'Tous les types', value: '' },
{ label: 'Chantier', value: 'chantier' },
{ label: 'Réunion', value: 'reunion' },
{ label: 'Formation', value: 'formation' },
{ label: 'Maintenance', value: 'maintenance' },
{ label: 'Congé', value: 'conge' },
{ label: 'Autre', value: 'autre' }
];
const prioriteOptions = [
{ label: 'Toutes priorités', value: '' },
{ label: 'Basse', value: 'basse' },
{ label: 'Normale', value: 'normale' },
{ label: 'Haute', value: 'haute' },
{ label: 'Critique', value: 'critique' }
];
const statutOptions = [
{ label: 'Planifié', value: 'planifie' },
{ label: 'Confirmé', value: 'confirme' },
{ label: 'En cours', value: 'en_cours' },
{ label: 'Terminé', value: 'termine' },
{ label: 'Annulé', value: 'annule' }
];
const viewModeOptions = [
{ label: 'Mois', value: 'month' },
{ label: 'Semaine', value: 'week' },
{ label: 'Jour', value: 'day' },
{ label: 'Agenda', value: 'agenda' }
];
useEffect(() => {
loadData();
}, []);
useEffect(() => {
generateMockEvents();
calculateStats();
detectConflicts();
}, [chantiers, selectedDate]);
const loadData = async () => {
try {
setLoading(true);
const chantiersData = await chantierService.getAll();
setChantiers(chantiersData);
} 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
});
} finally {
setLoading(false);
}
};
const generateMockEvents = () => {
const mockEvents: PlanningCalendrierEvent[] = [];
const now = new Date();
const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 2, 0);
// Générer des événements basés sur les chantiers
chantiers.forEach((chantier, index) => {
const startDate = new Date(chantier.dateDebut);
const endDate = chantier.dateFinPrevue ? new Date(chantier.dateFinPrevue) : new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1000);
if (startDate >= thisMonth && startDate <= nextMonth) {
mockEvents.push({
id: `chantier-${chantier.id}`,
title: `Chantier: ${chantier.nom}`,
start: startDate,
end: endDate,
type: 'chantier',
priority: index % 4 === 0 ? 'haute' : 'normale',
status: chantier.statut === 'EN_COURS' ? 'en_cours' :
chantier.statut === 'TERMINE' ? 'termine' :
chantier.statut === 'PLANIFIE' ? 'planifie' : 'confirme',
chantier: chantier,
color: getEventColor('chantier', chantier.statut === 'EN_COURS' ? 'en_cours' : 'planifie'),
description: `Chantier ${chantier.nom} - ${chantier.adresse}`
});
}
});
// Ajouter des événements de maintenance
for (let i = 0; i < 5; i++) {
const eventDate = new Date(now.getTime() + (Math.random() * 30 - 15) * 24 * 60 * 60 * 1000);
mockEvents.push({
id: `maintenance-${i}`,
title: `Maintenance ${['Camion', 'Grue', 'Échafaudage', 'Bétonnière', 'Compresseur'][i]}`,
start: eventDate,
end: new Date(eventDate.getTime() + 4 * 60 * 60 * 1000), // 4h
type: 'maintenance',
priority: 'normale',
status: 'planifie',
color: getEventColor('maintenance', 'planifie'),
description: `Maintenance préventive programmée`
});
}
// Ajouter des réunions
for (let i = 0; i < 8; i++) {
const eventDate = new Date(now.getTime() + (Math.random() * 20 - 10) * 24 * 60 * 60 * 1000);
eventDate.setHours(9 + Math.floor(Math.random() * 8)); // Entre 9h et 17h
eventDate.setMinutes(0);
mockEvents.push({
id: `reunion-${i}`,
title: `Réunion ${['Équipe', 'Client', 'Sécurité', 'Planning', 'Suivi', 'Formation', 'Qualité', 'Budget'][i]}`,
start: eventDate,
end: new Date(eventDate.getTime() + (1 + Math.random()) * 60 * 60 * 1000), // 1-2h
type: 'reunion',
priority: i < 2 ? 'haute' : 'normale',
status: 'confirme',
color: getEventColor('reunion', 'confirme'),
description: `Réunion d'équipe programmée`
});
}
setEvents(mockEvents);
};
const getEventColor = (type: string, status: string) => {
const colors = {
chantier: {
planifie: '#3B82F6',
confirme: '#10B981',
en_cours: '#F59E0B',
termine: '#6B7280',
annule: '#EF4444'
},
reunion: {
planifie: '#8B5CF6',
confirme: '#6366F1',
en_cours: '#A855F7',
termine: '#6B7280',
annule: '#EF4444'
},
formation: {
planifie: '#06B6D4',
confirme: '#0891B2',
en_cours: '#0E7490',
termine: '#6B7280',
annule: '#EF4444'
},
maintenance: {
planifie: '#F97316',
confirme: '#EA580C',
en_cours: '#DC2626',
termine: '#6B7280',
annule: '#EF4444'
},
conge: {
planifie: '#84CC16',
confirme: '#65A30D',
en_cours: '#65A30D',
termine: '#6B7280',
annule: '#EF4444'
},
autre: {
planifie: '#6B7280',
confirme: '#4B5563',
en_cours: '#374151',
termine: '#6B7280',
annule: '#EF4444'
}
};
return colors[type as keyof typeof colors]?.[status as keyof typeof colors.chantier] || '#6B7280';
};
const calculateStats = () => {
const today = new Date();
const weekStart = new Date(today.setDate(today.getDate() - today.getDay()));
const weekEnd = new Date(weekStart.getTime() + 7 * 24 * 60 * 60 * 1000);
const eventsAujourdHui = events.filter(event => {
const eventDate = new Date(event.start);
return eventDate.toDateString() === new Date().toDateString();
}).length;
const eventsSemaine = events.filter(event => {
const eventDate = new Date(event.start);
return eventDate >= weekStart && eventDate <= weekEnd;
}).length;
const eventsCritiques = events.filter(event => event.priority === 'critique').length;
// Calculer le taux d'occupation (approximatif)
const totalHours = events.reduce((total, event) => {
const duration = (event.end.getTime() - event.start.getTime()) / (1000 * 60 * 60);
return total + duration;
}, 0);
const tauxOccupation = Math.min(100, Math.round((totalHours / (7 * 8)) * 100)); // Basé sur 7 jours de 8h
setStats({
totalEvents: events.length,
eventsAujourdHui,
eventsSemaine,
eventsCritiques,
tauxOccupation
});
};
const detectConflicts = () => {
const conflicts: PlanningConflict[] = [];
// Détecter les chevauchements d'horaires
for (let i = 0; i < events.length; i++) {
for (let j = i + 1; j < events.length; j++) {
const event1 = events[i];
const event2 = events[j];
if (event1.start < event2.end && event1.end > event2.start) {
conflicts.push({
id: `conflict-${i}-${j}`,
type: 'CHEVAUCHEMENT_HORAIRES',
description: `Conflit horaire entre "${event1.title}" et "${event2.title}"`,
events: [event1, event2] as any,
ressources: [],
gravite: 'ATTENTION',
suggestions: [
'Décaler l\'un des événements',
'Réduire la durée des événements',
'Assigner des équipes différentes'
]
});
}
}
}
setConflicts(conflicts);
};
const getFilteredEvents = () => {
let filtered = [...events];
if (filterType) {
filtered = filtered.filter(event => event.type === filterType);
}
if (filterPriorite) {
filtered = filtered.filter(event => event.priority === filterPriorite);
}
if (globalFilter) {
const searchTerm = globalFilter.toLowerCase();
filtered = filtered.filter(event =>
event.title.toLowerCase().includes(searchTerm) ||
event.description?.toLowerCase().includes(searchTerm)
);
}
return filtered;
};
const getEventsForDate = (date: Date) => {
return getFilteredEvents().filter(event => {
const eventDate = new Date(event.start);
return eventDate.toDateString() === date.toDateString();
});
};
const onEventClick = (event: PlanningCalendrierEvent) => {
setSelectedEvent(event);
setEventDialog(true);
};
const onDateSelect = (date: Date) => {
setSelectedDate(date);
};
const openNewEventDialog = () => {
setNewEvent({
title: '',
description: '',
start: selectedDate,
end: new Date(selectedDate.getTime() + 2 * 60 * 60 * 1000),
type: 'autre',
priority: 'normale',
status: 'planifie',
allDay: false
});
setNewEventDialog(true);
};
const saveNewEvent = () => {
if (newEvent.title && newEvent.start && newEvent.end) {
const event: PlanningCalendrierEvent = {
id: `new-${Date.now()}`,
title: newEvent.title,
start: newEvent.start,
end: newEvent.end,
type: newEvent.type || 'autre',
priority: newEvent.priority || 'normale',
status: newEvent.status || 'planifie',
description: newEvent.description,
color: getEventColor(newEvent.type || 'autre', newEvent.status || 'planifie'),
allDay: newEvent.allDay
};
setEvents([...events, event]);
setNewEventDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Événement créé avec succès',
life: 3000
});
}
};
const statusBodyTemplate = (event: PlanningCalendrierEvent) => {
const getSeverity = (status: string) => {
switch (status) {
case 'planifie': return 'info';
case 'confirme': return 'success';
case 'en_cours': return 'warning';
case 'termine': return 'secondary';
case 'annule': return 'danger';
default: return 'info';
}
};
const getLabel = (status: string) => {
switch (status) {
case 'planifie': return 'Planifié';
case 'confirme': return 'Confirmé';
case 'en_cours': return 'En cours';
case 'termine': return 'Terminé';
case 'annule': return 'Annulé';
default: return status;
}
};
return (
<Tag
value={getLabel(event.status)}
severity={getSeverity(event.status)}
/>
);
};
const priorityBodyTemplate = (event: PlanningCalendrierEvent) => {
const getSeverity = (priority: string) => {
switch (priority) {
case 'basse': return 'secondary';
case 'normale': return 'info';
case 'haute': return 'warning';
case 'critique': return 'danger';
default: return 'info';
}
};
const getLabel = (priority: string) => {
switch (priority) {
case 'basse': return 'Basse';
case 'normale': return 'Normale';
case 'haute': return 'Haute';
case 'critique': return 'Critique';
default: return priority;
}
};
return (
<Tag
value={getLabel(event.priority)}
severity={getSeverity(event.priority)}
/>
);
};
const typeBodyTemplate = (event: PlanningCalendrierEvent) => {
const getIcon = (type: string) => {
switch (type) {
case 'chantier': return 'pi pi-building';
case 'reunion': return 'pi pi-users';
case 'formation': return 'pi pi-book';
case 'maintenance': return 'pi pi-wrench';
case 'conge': return 'pi pi-calendar-times';
default: return 'pi pi-circle';
}
};
const getLabel = (type: string) => {
switch (type) {
case 'chantier': return 'Chantier';
case 'reunion': return 'Réunion';
case 'formation': return 'Formation';
case 'maintenance': return 'Maintenance';
case 'conge': return 'Congé';
default: return 'Autre';
}
};
return (
<div className="flex align-items-center">
<i className={`${getIcon(event.type)} mr-2`} />
{getLabel(event.type)}
</div>
);
};
const timeBodyTemplate = (event: PlanningCalendrierEvent) => {
if (event.allDay) {
return <span className="text-sm">Toute la journée</span>;
}
return (
<div className="flex flex-column">
<span className="text-sm font-semibold">
{formatDateTime(event.start)} - {formatDateTime(event.end)}
</span>
<span className="text-xs text-color-secondary">
Durée: {Math.round((event.end.getTime() - event.start.getTime()) / (1000 * 60 * 60))}h
</span>
</div>
);
};
const actionBodyTemplate = (event: PlanningCalendrierEvent) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-eye"
size="small"
severity="info"
outlined
onClick={() => onEventClick(event)}
/>
<Button
icon="pi pi-pencil"
size="small"
severity="secondary"
outlined
/>
<Button
icon="pi pi-trash"
size="small"
severity="danger"
outlined
/>
</div>
);
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouvel événement"
icon="pi pi-plus"
severity="success"
onClick={openNewEventDialog}
/>
<Button
label="Aujourd'hui"
icon="pi pi-calendar"
severity="info"
outlined
onClick={() => setSelectedDate(new Date())}
/>
<Dropdown
value={viewMode}
options={viewModeOptions}
onChange={(e) => setViewMode(e.value)}
placeholder="Vue"
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Dropdown
value={filterType}
options={typeOptions}
onChange={(e) => setFilterType(e.value)}
placeholder="Type"
showClear
/>
<Dropdown
value={filterPriorite}
options={prioriteOptions}
onChange={(e) => setFilterPriorite(e.value)}
placeholder="Priorité"
showClear
/>
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
/>
</span>
<Button
label="Conflits"
icon="pi pi-exclamation-triangle"
severity="warning"
outlined
badge={conflicts.length.toString()}
onClick={() => setConflictDialog(true)}
disabled={conflicts.length === 0}
/>
</div>
);
};
const renderCalendarView = () => {
const eventsToday = getEventsForDate(selectedDate);
return (
<Splitter style={{ height: '600px' }}>
<SplitterPanel size={70}>
<div className="p-3">
<div className="flex justify-content-between align-items-center mb-3">
<h5>Calendrier {viewMode === 'month' ? 'Mensuel' :
viewMode === 'week' ? 'Hebdomadaire' :
viewMode === 'day' ? 'Journalier' : 'Agenda'}</h5>
<div className="flex gap-2">
<Button
icon="pi pi-chevron-left"
size="small"
onClick={() => {
const newDate = new Date(selectedDate);
if (viewMode === 'month') {
newDate.setMonth(newDate.getMonth() - 1);
} else if (viewMode === 'week') {
newDate.setDate(newDate.getDate() - 7);
} else {
newDate.setDate(newDate.getDate() - 1);
}
setSelectedDate(newDate);
}}
/>
<Button
icon="pi pi-chevron-right"
size="small"
onClick={() => {
const newDate = new Date(selectedDate);
if (viewMode === 'month') {
newDate.setMonth(newDate.getMonth() + 1);
} else if (viewMode === 'week') {
newDate.setDate(newDate.getDate() + 7);
} else {
newDate.setDate(newDate.getDate() + 1);
}
setSelectedDate(newDate);
}}
/>
</div>
</div>
<Calendar
value={selectedDate}
onChange={(e) => onDateSelect(e.value as Date)}
inline
dateFormat="dd/mm/yy"
showWeek
className="w-full"
/>
<div className="mt-3">
<h6>Légende</h6>
<div className="flex flex-wrap gap-2">
{typeOptions.slice(1).map(type => (
<div key={type.value} className="flex align-items-center gap-1">
<div
className="w-1rem h-1rem border-round"
style={{ backgroundColor: getEventColor(type.value, 'confirme') }}
/>
<span className="text-sm">{type.label}</span>
</div>
))}
</div>
</div>
</div>
</SplitterPanel>
<SplitterPanel size={30}>
<div className="p-3">
<Panel header={`Événements du ${formatDate(selectedDate)}`} className="h-full">
{eventsToday.length > 0 ? (
<div className="flex flex-column gap-2">
{eventsToday.map(event => (
<Card
key={event.id}
className="cursor-pointer hover:shadow-2"
onClick={() => onEventClick(event)}
>
<div className="flex align-items-start justify-content-between">
<div className="flex-1">
<div className="flex align-items-center gap-2 mb-1">
<div
className="w-1rem h-1rem border-round"
style={{ backgroundColor: event.color }}
/>
<span className="font-semibold text-sm">{event.title}</span>
</div>
<div className="text-xs text-color-secondary mb-2">
{event.allDay ? 'Toute la journée' :
`${formatDateTime(event.start)} - ${formatDateTime(event.end)}`}
</div>
<div className="flex gap-1">
<Tag
value={typeBodyTemplate(event).props.children[1]}
severity="info"
size="small"
/>
<Tag
value={priorityBodyTemplate(event).props.value}
severity={priorityBodyTemplate(event).props.severity}
size="small"
/>
</div>
</div>
</div>
</Card>
))}
</div>
) : (
<p className="text-center text-color-secondary">
Aucun événement pour cette date
</p>
)}
</Panel>
</div>
</SplitterPanel>
</Splitter>
);
};
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-calendar text-blue-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.totalEvents}</div>
<div className="text-color-secondary">Événements 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-clock text-green-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.eventsAujourdHui}</div>
<div className="text-color-secondary">Aujourd'hui</div>
</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-exclamation-triangle text-orange-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.eventsCritiques}</div>
<div className="text-color-secondary">Critiques</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-chart-pie text-purple-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.tauxOccupation}%</div>
<div className="text-color-secondary">Taux d'occupation</div>
<ProgressBar value={stats.tauxOccupation} className="mt-1" style={{ height: '4px' }} />
</div>
</div>
</Card>
</div>
</div>
);
};
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
{renderStatsCards()}
{viewMode === 'agenda' ? (
<DataTable
value={getFilteredEvents()}
paginator
rows={15}
dataKey="id"
loading={loading}
globalFilter={globalFilter}
emptyMessage="Aucun événement trouvé"
className="datatable-responsive"
>
<Column field="title" header="Événement" sortable style={{ minWidth: '200px' }} />
<Column field="type" header="Type" body={typeBodyTemplate} sortable style={{ minWidth: '120px' }} />
<Column field="priority" header="Priorité" body={priorityBodyTemplate} sortable style={{ minWidth: '100px' }} />
<Column field="status" header="Statut" body={statusBodyTemplate} sortable style={{ minWidth: '100px' }} />
<Column field="start" header="Horaires" body={timeBodyTemplate} sortable style={{ minWidth: '200px' }} />
<Column field="description" header="Description" sortable style={{ minWidth: '200px' }} />
<Column body={actionBodyTemplate} style={{ minWidth: '120px' }} />
</DataTable>
) : (
renderCalendarView()
)}
{/* Dialog détails événement */}
<Dialog
visible={eventDialog}
style={{ width: '600px' }}
header="Détails de l'événement"
modal
onHide={() => setEventDialog(false)}
>
{selectedEvent && (
<div className="flex flex-column gap-3">
<div>
<label className="font-semibold">Titre</label>
<p>{selectedEvent.title}</p>
</div>
<div className="grid">
<div className="col-6">
<label className="font-semibold">Type</label>
<p>{typeBodyTemplate(selectedEvent)}</p>
</div>
<div className="col-6">
<label className="font-semibold">Priorité</label>
<p>{priorityBodyTemplate(selectedEvent)}</p>
</div>
</div>
<div className="grid">
<div className="col-6">
<label className="font-semibold">Début</label>
<p>{formatDateTime(selectedEvent.start)}</p>
</div>
<div className="col-6">
<label className="font-semibold">Fin</label>
<p>{formatDateTime(selectedEvent.end)}</p>
</div>
</div>
<div>
<label className="font-semibold">Statut</label>
<p>{statusBodyTemplate(selectedEvent)}</p>
</div>
{selectedEvent.description && (
<div>
<label className="font-semibold">Description</label>
<p>{selectedEvent.description}</p>
</div>
)}
{selectedEvent.chantier && (
<div>
<label className="font-semibold">Chantier associé</label>
<p>{selectedEvent.chantier.nom} - {selectedEvent.chantier.adresse}</p>
</div>
)}
</div>
)}
</Dialog>
{/* Dialog nouvel événement */}
<Dialog
visible={newEventDialog}
style={{ width: '700px' }}
header="Nouvel événement"
modal
onHide={() => setNewEventDialog(false)}
footer={
<div>
<Button label="Annuler" icon="pi pi-times" onClick={() => setNewEventDialog(false)} />
<Button label="Créer" icon="pi pi-check" onClick={saveNewEvent} />
</div>
}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="title">Titre *</label>
<InputText
id="title"
value={newEvent.title || ''}
onChange={(e) => setNewEvent({...newEvent, title: e.target.value})}
className="w-full"
placeholder="Titre de l'événement"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="type">Type</label>
<Dropdown
id="type"
value={newEvent.type}
options={typeOptions.slice(1)}
onChange={(e) => setNewEvent({...newEvent, type: e.value})}
className="w-full"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="priority">Priorité</label>
<Dropdown
id="priority"
value={newEvent.priority}
options={prioriteOptions.slice(1)}
onChange={(e) => setNewEvent({...newEvent, priority: e.value})}
className="w-full"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="start">Date/Heure début</label>
<Calendar
id="start"
value={newEvent.start}
onChange={(e) => setNewEvent({...newEvent, start: e.value as Date})}
showTime
dateFormat="dd/mm/yy"
timeFormat="24"
className="w-full"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="end">Date/Heure fin</label>
<Calendar
id="end"
value={newEvent.end}
onChange={(e) => setNewEvent({...newEvent, end: e.value as Date})}
showTime
dateFormat="dd/mm/yy"
timeFormat="24"
className="w-full"
/>
</div>
<div className="field col-12">
<label htmlFor="description">Description</label>
<InputTextarea
id="description"
value={newEvent.description || ''}
onChange={(e) => setNewEvent({...newEvent, description: e.target.value})}
rows={3}
className="w-full"
placeholder="Description de l'événement"
/>
</div>
</div>
</Dialog>
{/* Dialog conflits */}
<Dialog
visible={conflictDialog}
style={{ width: '800px' }}
header="Conflits de planification"
modal
onHide={() => setConflictDialog(false)}
>
{conflicts.length > 0 ? (
<div className="flex flex-column gap-3">
{conflicts.map(conflict => (
<Card key={conflict.id} className="border-left-3 border-orange-500">
<div className="flex align-items-start justify-content-between">
<div className="flex-1">
<div className="flex align-items-center gap-2 mb-2">
<i className="pi pi-exclamation-triangle text-orange-500" />
<span className="font-semibold">{conflict.description}</span>
<Tag
value={conflict.gravite}
severity={conflict.gravite === 'CRITIQUE' ? 'danger' : 'warning'}
/>
</div>
<div className="text-sm text-color-secondary">
<strong>Suggestions:</strong>
<ul className="mt-1 ml-3">
{conflict.suggestions.map((suggestion, index) => (
<li key={index}>{suggestion}</li>
))}
</ul>
</div>
</div>
</div>
</Card>
))}
</div>
) : (
<p className="text-center text-color-secondary">Aucun conflit détecté</p>
)}
</Dialog>
</Card>
</div>
</div>
);
};
export default CalendrierPlanningPage;