'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'; import { TypeConflitPlanification, GraviteConflict } 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([]); const [chantiers, setChantiers] = useState([]); const [selectedDate, setSelectedDate] = useState(new Date()); const [selectedEvent, setSelectedEvent] = useState(null); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState<'month' | 'week' | 'day' | 'agenda'>('month'); const [filterType, setFilterType] = useState(''); const [filterEquipe, setFilterEquipe] = useState(''); const [filterPriorite, setFilterPriorite] = useState(''); 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>({ 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([]); const [stats, setStats] = useState({ totalEvents: 0, eventsAujourdHui: 0, eventsSemaine: 0, eventsCritiques: 0, tauxOccupation: 0 }); const toast = useRef(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: TypeConflitPlanification.CHEVAUCHEMENT_HORAIRES, description: `Conflit horaire entre "${event1.title}" et "${event2.title}"`, events: [event1, event2] as any, ressources: [], gravite: GraviteConflict.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 ( ); }; 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 ( ); }; 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 (
{getLabel(event.type)}
); }; const timeBodyTemplate = (event: PlanningCalendrierEvent) => { if (event.allDay) { return Toute la journée; } return (
{formatDateTime(event.start)} - {formatDateTime(event.end)} Durée: {Math.round((event.end.getTime() - event.start.getTime()) / (1000 * 60 * 60))}h
); }; const actionBodyTemplate = (event: PlanningCalendrierEvent) => { return (
); }; const leftToolbarTemplate = () => { return (
); }; const rightToolbarTemplate = () => { return (
setFilterType(e.value)} placeholder="Type" showClear /> setFilterPriorite(e.value)} placeholder="Priorité" showClear /> setGlobalFilter(e.target.value)} />
); }; const renderCalendarView = () => { const eventsToday = getEventsForDate(selectedDate); return (
Calendrier {viewMode === 'month' ? 'Mensuel' : viewMode === 'week' ? 'Hebdomadaire' : viewMode === 'day' ? 'Journalier' : 'Agenda'}
onDateSelect(e.value as Date)} inline dateFormat="dd/mm/yy" showWeek className="w-full" />
Légende
{typeOptions.slice(1).map(type => (
{type.label}
))}
{eventsToday.length > 0 ? (
{eventsToday.map(event => ( onEventClick(event)} >
{event.title}
{event.allDay ? 'Toute la journée' : `${formatDateTime(event.start)} - ${formatDateTime(event.end)}`}
))}
) : (

Aucun événement pour cette date

)}
); }; const renderStatsCards = () => { return (
{stats.totalEvents}
Événements total
{stats.eventsAujourdHui}
Aujourd'hui
{stats.eventsCritiques}
Critiques
{stats.tauxOccupation}%
Taux d'occupation
); }; return (
{renderStatsCards()} {viewMode === 'agenda' ? ( ) : ( renderCalendarView() )} {/* Dialog détails événement */} setEventDialog(false)} > {selectedEvent && (

{selectedEvent.title}

{typeBodyTemplate(selectedEvent)}

{priorityBodyTemplate(selectedEvent)}

{formatDateTime(selectedEvent.start)}

{formatDateTime(selectedEvent.end)}

{statusBodyTemplate(selectedEvent)}

{selectedEvent.description && (

{selectedEvent.description}

)} {selectedEvent.chantier && (

{selectedEvent.chantier.nom} - {selectedEvent.chantier.adresse}

)}
)}
{/* Dialog nouvel événement */} setNewEventDialog(false)} footer={
} >
setNewEvent({...newEvent, title: e.target.value})} className="w-full" placeholder="Titre de l'événement" />
setNewEvent({...newEvent, type: e.value})} className="w-full" />
setNewEvent({...newEvent, priority: e.value})} className="w-full" />
setNewEvent({...newEvent, start: e.value as Date})} showTime hourFormat="24" dateFormat="dd/mm/yy" className="w-full" />
setNewEvent({...newEvent, end: e.value as Date})} showTime hourFormat="24" dateFormat="dd/mm/yy" className="w-full" />
setNewEvent({...newEvent, description: e.target.value})} rows={3} className="w-full" placeholder="Description de l'événement" />
{/* Dialog conflits */} setConflictDialog(false)} > {conflicts.length > 0 ? (
{conflicts.map(conflict => (
{conflict.description}
Suggestions:
    {conflict.suggestions.map((suggestion, index) => (
  • {suggestion}
  • ))}
))}
) : (

Aucun conflit détecté

)}
); }; export default CalendrierPlanningPage;