- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts - Ajout des propriétés manquantes aux objets User mockés - Conversion des dates de string vers objets Date - Correction des appels asynchrones et des types incompatibles - Ajout de dynamic rendering pour résoudre les erreurs useSearchParams - Enveloppement de useSearchParams dans Suspense boundary - Configuration de force-dynamic au niveau du layout principal Build réussi: 126 pages générées avec succès 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
715 lines
29 KiB
TypeScript
715 lines
29 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import FullCalendar from '@fullcalendar/react';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Button } from 'primereact/button';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Timeline } from 'primereact/timeline';
|
|
import { Divider } from 'primereact/divider';
|
|
import { useRouter } from 'next/navigation';
|
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
|
import interactionPlugin from '@fullcalendar/interaction';
|
|
import frLocale from '@fullcalendar/core/locales/fr';
|
|
import RoleProtectedPage from '@/components/RoleProtectedPage';
|
|
|
|
interface PlanningEvent {
|
|
id: string;
|
|
title: string;
|
|
start: Date;
|
|
end: Date;
|
|
type: string;
|
|
chantier: string;
|
|
ressources: string[];
|
|
statut: string;
|
|
priorite: string;
|
|
description: string;
|
|
responsable: string;
|
|
color?: string;
|
|
}
|
|
|
|
interface ConflitPlanning {
|
|
id: string;
|
|
type: string;
|
|
ressource: string;
|
|
evenements: string[];
|
|
dateDebut: Date;
|
|
dateFin: Date;
|
|
severite: string;
|
|
}
|
|
|
|
const DashboardPlanningContent = () => {
|
|
const toast = useRef<Toast>(null);
|
|
const router = useRouter();
|
|
|
|
const [events, setEvents] = useState<PlanningEvent[]>([]);
|
|
const [conflits, setConflits] = useState<ConflitPlanning[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedView, setSelectedView] = useState('month');
|
|
const [selectedType, setSelectedType] = useState('');
|
|
const [selectedChantier, setSelectedChantier] = useState('');
|
|
const [dateRange, setDateRange] = useState<Date[]>([]);
|
|
|
|
const [calendarOptions, setCalendarOptions] = useState({});
|
|
const [timelineEvents, setTimelineEvents] = useState([]);
|
|
|
|
const viewOptions = [
|
|
{ label: 'Mois', value: 'month' },
|
|
{ label: 'Semaine', value: 'week' },
|
|
{ label: 'Jour', value: 'day' },
|
|
{ label: 'Liste', value: 'list' }
|
|
];
|
|
|
|
const typeOptions = [
|
|
{ label: 'Tous les types', value: '' },
|
|
{ label: 'Chantier', value: 'CHANTIER' },
|
|
{ label: 'Maintenance', value: 'MAINTENANCE' },
|
|
{ label: 'Formation', value: 'FORMATION' },
|
|
{ label: 'Réunion', value: 'REUNION' },
|
|
{ label: 'Livraison', value: 'LIVRAISON' }
|
|
];
|
|
|
|
const chantierOptions = [
|
|
{ label: 'Tous les chantiers', value: '' },
|
|
{ label: 'Résidence Les Jardins', value: 'jardins' },
|
|
{ label: 'Centre Commercial Atlantis', value: 'atlantis' },
|
|
{ label: 'Rénovation Hôtel Luxe', value: 'hotel' },
|
|
{ label: 'Usine Pharmaceutique', value: 'usine' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadPlanning();
|
|
initCalendar();
|
|
initTimeline();
|
|
}, [selectedView, selectedType, selectedChantier, dateRange]);
|
|
|
|
const loadPlanning = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// TODO: Remplacer par un vrai appel API
|
|
// const response = await planningService.getDashboardData({
|
|
// view: selectedView,
|
|
// type: selectedType,
|
|
// chantier: selectedChantier,
|
|
// dateRange
|
|
// });
|
|
|
|
// Données simulées pour la démonstration
|
|
const mockEvents: PlanningEvent[] = [
|
|
{
|
|
id: '1',
|
|
title: 'Coulage dalle béton',
|
|
start: new Date('2025-01-02T08:00:00'),
|
|
end: new Date('2025-01-02T17:00:00'),
|
|
type: 'CHANTIER',
|
|
chantier: 'Résidence Les Jardins',
|
|
ressources: ['Équipe Gros Œuvre', 'Bétonnière S36X', 'Grue LTM 1050'],
|
|
statut: 'PLANIFIE',
|
|
priorite: 'HAUTE',
|
|
description: 'Coulage de la dalle du niveau R+1',
|
|
responsable: 'Jean Dupont',
|
|
color: '#10b981'
|
|
},
|
|
{
|
|
id: '2',
|
|
title: 'Maintenance préventive',
|
|
start: new Date('2025-01-03T09:00:00'),
|
|
end: new Date('2025-01-03T12:00:00'),
|
|
type: 'MAINTENANCE',
|
|
chantier: 'Atelier Central',
|
|
ressources: ['Pelleteuse CAT 320D'],
|
|
statut: 'PLANIFIE',
|
|
priorite: 'MOYENNE',
|
|
description: 'Révision système hydraulique',
|
|
responsable: 'Marie Martin',
|
|
color: '#f59e0b'
|
|
},
|
|
{
|
|
id: '3',
|
|
title: 'Formation CACES',
|
|
start: new Date('2025-01-06T08:00:00'),
|
|
end: new Date('2025-01-08T17:00:00'),
|
|
type: 'FORMATION',
|
|
chantier: 'Centre de Formation',
|
|
ressources: ['Pierre Leroy', 'Luc Bernard'],
|
|
statut: 'CONFIRME',
|
|
priorite: 'MOYENNE',
|
|
description: 'Formation CACES R482 - Engins de chantier',
|
|
responsable: 'Sophie Dubois',
|
|
color: '#3b82f6'
|
|
},
|
|
{
|
|
id: '4',
|
|
title: 'Livraison matériaux',
|
|
start: new Date('2025-01-07T14:00:00'),
|
|
end: new Date('2025-01-07T16:00:00'),
|
|
type: 'LIVRAISON',
|
|
chantier: 'Centre Commercial Atlantis',
|
|
ressources: ['Camion benne Volvo'],
|
|
statut: 'PLANIFIE',
|
|
priorite: 'HAUTE',
|
|
description: 'Livraison acier pour structure',
|
|
responsable: 'Marc Rousseau',
|
|
color: '#8b5cf6'
|
|
},
|
|
{
|
|
id: '5',
|
|
title: 'Réunion chantier',
|
|
start: new Date('2025-01-09T10:00:00'),
|
|
end: new Date('2025-01-09T11:30:00'),
|
|
type: 'REUNION',
|
|
chantier: 'Résidence Les Jardins',
|
|
ressources: ['Équipe complète'],
|
|
statut: 'CONFIRME',
|
|
priorite: 'MOYENNE',
|
|
description: 'Point hebdomadaire équipe',
|
|
responsable: 'Jean Dupont',
|
|
color: '#06b6d4'
|
|
}
|
|
];
|
|
|
|
const mockConflits: ConflitPlanning[] = [
|
|
{
|
|
id: '1',
|
|
type: 'RESSOURCE_DOUBLE',
|
|
ressource: 'Grue LTM 1050',
|
|
evenements: ['Coulage dalle béton', 'Montage charpente'],
|
|
dateDebut: new Date('2025-01-02T08:00:00'),
|
|
dateFin: new Date('2025-01-02T17:00:00'),
|
|
severite: 'HAUTE'
|
|
},
|
|
{
|
|
id: '2',
|
|
type: 'EMPLOYE_INDISPONIBLE',
|
|
ressource: 'Pierre Leroy',
|
|
evenements: ['Formation CACES'],
|
|
dateDebut: new Date('2025-01-06T08:00:00'),
|
|
dateFin: new Date('2025-01-08T17:00:00'),
|
|
severite: 'MOYENNE'
|
|
}
|
|
];
|
|
|
|
// Filtrer selon les critères sélectionnés
|
|
let filteredEvents = mockEvents;
|
|
|
|
if (selectedType) {
|
|
filteredEvents = filteredEvents.filter(e => e.type === selectedType);
|
|
}
|
|
|
|
if (selectedChantier) {
|
|
filteredEvents = filteredEvents.filter(e => e.chantier.toLowerCase().includes(selectedChantier));
|
|
}
|
|
|
|
setEvents(filteredEvents);
|
|
setConflits(mockConflits);
|
|
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement du planning:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les données du planning'
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const initCalendar = () => {
|
|
const calendarEvents = events.map(event => ({
|
|
id: event.id,
|
|
title: event.title,
|
|
start: event.start,
|
|
end: event.end,
|
|
backgroundColor: event.color,
|
|
borderColor: event.color,
|
|
textColor: '#ffffff',
|
|
extendedProps: {
|
|
type: event.type,
|
|
chantier: event.chantier,
|
|
responsable: event.responsable,
|
|
description: event.description,
|
|
ressources: event.ressources
|
|
}
|
|
}));
|
|
|
|
const options = {
|
|
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
|
|
headerToolbar: {
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
|
},
|
|
initialView: selectedView === 'month' ? 'dayGridMonth' :
|
|
selectedView === 'week' ? 'timeGridWeek' : 'timeGridDay',
|
|
locale: frLocale,
|
|
events: calendarEvents,
|
|
editable: true,
|
|
selectable: true,
|
|
selectMirror: true,
|
|
dayMaxEvents: true,
|
|
weekends: true,
|
|
eventClick: (info: any) => {
|
|
const event = events.find(e => e.id === info.event.id);
|
|
if (event) {
|
|
handleEventClick(event);
|
|
}
|
|
},
|
|
select: (info: any) => {
|
|
handleDateSelect(info.start, info.end);
|
|
},
|
|
eventDrop: (info: any) => {
|
|
handleEventDrop(info);
|
|
},
|
|
eventResize: (info: any) => {
|
|
handleEventResize(info);
|
|
}
|
|
};
|
|
|
|
setCalendarOptions(options);
|
|
};
|
|
|
|
const initTimeline = () => {
|
|
const upcomingEvents = events
|
|
.filter(e => e.start > new Date())
|
|
.sort((a, b) => a.start.getTime() - b.start.getTime())
|
|
.slice(0, 5)
|
|
.map(e => ({
|
|
status: e.priorite === 'HAUTE' ? 'danger' : e.priorite === 'MOYENNE' ? 'warning' : 'info',
|
|
date: e.start.toLocaleDateString('fr-FR'),
|
|
time: e.start.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }),
|
|
icon: getTypeIcon(e.type),
|
|
color: e.color,
|
|
title: e.title,
|
|
chantier: e.chantier,
|
|
responsable: e.responsable
|
|
}));
|
|
|
|
setTimelineEvents(upcomingEvents);
|
|
};
|
|
|
|
const getTypeIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'CHANTIER': return 'pi pi-building';
|
|
case 'MAINTENANCE': return 'pi pi-wrench';
|
|
case 'FORMATION': return 'pi pi-graduation-cap';
|
|
case 'REUNION': return 'pi pi-users';
|
|
case 'LIVRAISON': return 'pi pi-truck';
|
|
default: return 'pi pi-calendar';
|
|
}
|
|
};
|
|
|
|
const getStatutSeverity = (statut: string) => {
|
|
switch (statut) {
|
|
case 'PLANIFIE': return 'info';
|
|
case 'CONFIRME': return 'success';
|
|
case 'EN_COURS': return 'warning';
|
|
case 'TERMINE': return 'success';
|
|
case 'ANNULE': return 'danger';
|
|
default: return 'info';
|
|
}
|
|
};
|
|
|
|
const getPrioriteSeverity = (priorite: string) => {
|
|
switch (priorite) {
|
|
case 'HAUTE': return 'danger';
|
|
case 'MOYENNE': return 'warning';
|
|
case 'BASSE': return 'info';
|
|
default: return 'info';
|
|
}
|
|
};
|
|
|
|
const getSeveriteSeverity = (severite: string) => {
|
|
switch (severite) {
|
|
case 'HAUTE': return 'danger';
|
|
case 'MOYENNE': return 'warning';
|
|
case 'BASSE': return 'info';
|
|
default: return 'info';
|
|
}
|
|
};
|
|
|
|
const handleEventClick = (event: PlanningEvent) => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: event.title,
|
|
detail: `${event.chantier} - ${event.responsable}`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const handleDateSelect = (start: Date, end: Date) => {
|
|
router.push(`/planning/nouveau?start=${start.toISOString()}&end=${end.toISOString()}`);
|
|
};
|
|
|
|
const handleEventDrop = (info: any) => {
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Événement déplacé',
|
|
detail: `${info.event.title} déplacé au ${info.event.start.toLocaleDateString('fr-FR')}`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const handleEventResize = (info: any) => {
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Événement redimensionné',
|
|
detail: `Durée de ${info.event.title} modifiée`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const statutBodyTemplate = (rowData: PlanningEvent) => (
|
|
<Tag value={rowData.statut} severity={getStatutSeverity(rowData.statut)} />
|
|
);
|
|
|
|
const prioriteBodyTemplate = (rowData: PlanningEvent) => (
|
|
<Tag value={rowData.priorite} severity={getPrioriteSeverity(rowData.priorite)} />
|
|
);
|
|
|
|
const typeBodyTemplate = (rowData: PlanningEvent) => (
|
|
<div className="flex align-items-center">
|
|
<i className={`${getTypeIcon(rowData.type)} mr-2`}></i>
|
|
<span>{rowData.type}</span>
|
|
</div>
|
|
);
|
|
|
|
const ressourcesBodyTemplate = (rowData: PlanningEvent) => (
|
|
<div className="flex flex-wrap gap-1">
|
|
{rowData.ressources.slice(0, 2).map((ressource, index) => (
|
|
<Tag key={index} value={ressource} severity={"secondary" as any} className="text-xs" />
|
|
))}
|
|
{rowData.ressources.length > 2 && (
|
|
<Tag value={`+${rowData.ressources.length - 2}`} severity="info" className="text-xs" />
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const conflitTypeBodyTemplate = (rowData: ConflitPlanning) => (
|
|
<div className="flex align-items-center">
|
|
<i className="pi pi-exclamation-triangle text-orange-500 mr-2"></i>
|
|
<span>{rowData.type.replace('_', ' ')}</span>
|
|
</div>
|
|
);
|
|
|
|
const conflitSeveriteBodyTemplate = (rowData: ConflitPlanning) => (
|
|
<Tag value={rowData.severite} severity={getSeveriteSeverity(rowData.severite)} />
|
|
);
|
|
|
|
const actionBodyTemplate = (rowData: PlanningEvent) => (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-eye"
|
|
className="p-button-text p-button-sm"
|
|
tooltip="Voir détails"
|
|
onClick={() => router.push(`/planning/${rowData.id}`)}
|
|
/>
|
|
<Button
|
|
icon="pi pi-pencil"
|
|
className="p-button-text p-button-sm"
|
|
tooltip="Modifier"
|
|
onClick={() => router.push(`/planning/${rowData.id}/edit`)}
|
|
/>
|
|
<Button
|
|
icon="pi pi-copy"
|
|
className="p-button-text p-button-sm"
|
|
tooltip="Dupliquer"
|
|
onClick={() => router.push(`/planning/nouveau?template=${rowData.id}`)}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
const conflitActionBodyTemplate = (rowData: ConflitPlanning) => (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-check"
|
|
className="p-button-text p-button-sm p-button-success"
|
|
tooltip="Résoudre"
|
|
onClick={() => {
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Conflit résolu',
|
|
detail: 'Le conflit a été résolu avec succès',
|
|
life: 3000
|
|
});
|
|
}}
|
|
/>
|
|
<Button
|
|
icon="pi pi-times"
|
|
className="p-button-text p-button-sm p-button-danger"
|
|
tooltip="Ignorer"
|
|
onClick={() => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Conflit ignoré',
|
|
detail: 'Le conflit a été marqué comme ignoré',
|
|
life: 3000
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
// Calculs des métriques
|
|
const evenementsAujourdhui = events.filter(e =>
|
|
e.start.toDateString() === new Date().toDateString()
|
|
).length;
|
|
|
|
const evenementsSemaine = events.filter(e => {
|
|
const today = new Date();
|
|
const weekStart = new Date(today.setDate(today.getDate() - today.getDay()));
|
|
const weekEnd = new Date(today.setDate(today.getDate() - today.getDay() + 6));
|
|
return e.start >= weekStart && e.start <= weekEnd;
|
|
}).length;
|
|
|
|
const conflitsActifs = conflits.filter(c => c.severite === 'HAUTE').length;
|
|
|
|
return (
|
|
<div className="grid">
|
|
<Toast ref={toast} />
|
|
|
|
{/* En-tête avec filtres */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h2 className="text-2xl font-bold m-0">Dashboard Planning</h2>
|
|
<Button
|
|
label="Nouvel événement"
|
|
icon="pi pi-plus"
|
|
onClick={() => router.push('/planning/nouveau')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-3 align-items-center">
|
|
<div className="field">
|
|
<label htmlFor="view" className="font-semibold">Vue</label>
|
|
<Dropdown
|
|
id="view"
|
|
value={selectedView}
|
|
options={viewOptions}
|
|
onChange={(e) => setSelectedView(e.value)}
|
|
className="w-full md:w-10rem"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label htmlFor="type" className="font-semibold">Type</label>
|
|
<Dropdown
|
|
id="type"
|
|
value={selectedType}
|
|
options={typeOptions}
|
|
onChange={(e) => setSelectedType(e.value)}
|
|
className="w-full md:w-14rem"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label htmlFor="chantier" className="font-semibold">Chantier</label>
|
|
<Dropdown
|
|
id="chantier"
|
|
value={selectedChantier}
|
|
options={chantierOptions}
|
|
onChange={(e) => setSelectedChantier(e.value)}
|
|
className="w-full md:w-16rem"
|
|
/>
|
|
</div>
|
|
|
|
<div className="field">
|
|
<label htmlFor="dateRange" className="font-semibold">Période</label>
|
|
<Calendar
|
|
id="dateRange"
|
|
value={dateRange}
|
|
onChange={(e) => setDateRange(e.value as Date[])}
|
|
selectionMode="range"
|
|
readOnlyInput
|
|
className="w-full md:w-14rem"
|
|
placeholder="Sélectionner une période"
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
className="p-button-outlined"
|
|
onClick={loadPlanning}
|
|
loading={loading}
|
|
tooltip="Actualiser"
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Métriques principales */}
|
|
<div className="col-12 lg:col-3 md:col-6">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between mb-3">
|
|
<div>
|
|
<span className="block text-500 font-medium mb-3">Aujourd'hui</span>
|
|
<div className="text-900 font-medium text-xl">{evenementsAujourdhui}</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-blue-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-calendar text-blue-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-3 md:col-6">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between mb-3">
|
|
<div>
|
|
<span className="block text-500 font-medium mb-3">Cette semaine</span>
|
|
<div className="text-900 font-medium text-xl">{evenementsSemaine}</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-green-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-calendar-plus text-green-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-3 md:col-6">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between mb-3">
|
|
<div>
|
|
<span className="block text-500 font-medium mb-3">Conflits actifs</span>
|
|
<div className="text-900 font-medium text-xl">{conflitsActifs}</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-red-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-exclamation-triangle text-red-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 lg:col-3 md:col-6">
|
|
<Card className="h-full">
|
|
<div className="flex justify-content-between mb-3">
|
|
<div>
|
|
<span className="block text-500 font-medium mb-3">Total événements</span>
|
|
<div className="text-900 font-medium text-xl">{events.length}</div>
|
|
</div>
|
|
<div className="flex align-items-center justify-content-center bg-purple-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
|
<i className="pi pi-list text-purple-500 text-xl"></i>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Calendrier */}
|
|
{selectedView !== 'list' && (
|
|
<div className="col-12 lg:col-8">
|
|
<Card>
|
|
<h6>Calendrier</h6>
|
|
<FullCalendar {...calendarOptions} />
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Timeline des prochains événements */}
|
|
<div className={`col-12 ${selectedView !== 'list' ? 'lg:col-4' : ''}`}>
|
|
<Card>
|
|
<h6>Prochains Événements</h6>
|
|
<Timeline
|
|
value={timelineEvents}
|
|
align="alternate"
|
|
className="customized-timeline"
|
|
marker={(item) => <span className={`flex w-2rem h-2rem align-items-center justify-content-center text-white border-circle z-1 shadow-1`} style={{ backgroundColor: item.color }}>
|
|
<i className={item.icon}></i>
|
|
</span>}
|
|
content={(item) => (
|
|
<Card title={item.title} subTitle={`${item.date} à ${item.time}`}>
|
|
<p className="text-sm">
|
|
<strong>Chantier:</strong> {item.chantier}<br/>
|
|
<strong>Responsable:</strong> {item.responsable}
|
|
</p>
|
|
</Card>
|
|
)}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Conflits de planning */}
|
|
{conflits.length > 0 && (
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h6>Conflits de Planning</h6>
|
|
<Badge value={`${conflitsActifs} critiques`} severity="danger" />
|
|
</div>
|
|
|
|
<DataTable
|
|
value={conflits}
|
|
responsiveLayout="scroll"
|
|
emptyMessage="Aucun conflit détecté"
|
|
>
|
|
<Column field="type" header="Type" body={conflitTypeBodyTemplate} />
|
|
<Column field="ressource" header="Ressource" />
|
|
<Column field="evenements" header="Événements" body={(rowData) => rowData.evenements.join(', ')} />
|
|
<Column field="dateDebut" header="Date début" body={(rowData) => rowData.dateDebut.toLocaleDateString('fr-FR')} />
|
|
<Column field="dateFin" header="Date fin" body={(rowData) => rowData.dateFin.toLocaleDateString('fr-FR')} />
|
|
<Column field="severite" header="Sévérité" body={conflitSeveriteBodyTemplate} />
|
|
<Column header="Actions" body={conflitActionBodyTemplate} style={{ width: '100px' }} />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Liste des événements */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h6>Liste des Événements ({events.length})</h6>
|
|
<Badge value={`${events.filter(e => e.statut === 'CONFIRME').length} confirmés`} severity="success" />
|
|
</div>
|
|
|
|
<DataTable
|
|
value={events}
|
|
loading={loading}
|
|
responsiveLayout="scroll"
|
|
paginator
|
|
rows={10}
|
|
rowsPerPageOptions={[5, 10, 25]}
|
|
emptyMessage="Aucun événement trouvé"
|
|
sortMode="multiple"
|
|
>
|
|
<Column field="title" header="Titre" sortable />
|
|
<Column field="type" header="Type" body={typeBodyTemplate} sortable />
|
|
<Column field="chantier" header="Chantier" sortable />
|
|
<Column field="start" header="Début" body={(rowData) => rowData.start.toLocaleString('fr-FR')} sortable />
|
|
<Column field="end" header="Fin" body={(rowData) => rowData.end.toLocaleString('fr-FR')} sortable />
|
|
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
|
<Column field="priorite" header="Priorité" body={prioriteBodyTemplate} sortable />
|
|
<Column field="ressources" header="Ressources" body={ressourcesBodyTemplate} />
|
|
<Column field="responsable" header="Responsable" sortable />
|
|
<Column header="Actions" body={actionBodyTemplate} style={{ width: '120px' }} />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const DashboardPlanning = () => {
|
|
return (
|
|
<RoleProtectedPage
|
|
requiredPage="PLANNING"
|
|
fallbackMessage="Vous devez avoir un rôle de gestionnaire ou supérieur pour accéder au planning."
|
|
>
|
|
<DashboardPlanningContent />
|
|
</RoleProtectedPage>
|
|
);
|
|
};
|
|
|
|
export default DashboardPlanning;
|
|
|