diff --git a/client/src/pages/AdminCalendrier.tsx b/client/src/pages/AdminCalendrier.tsx index 1f7b78e..58cd1bb 100644 --- a/client/src/pages/AdminCalendrier.tsx +++ b/client/src/pages/AdminCalendrier.tsx @@ -9,17 +9,39 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { trpc } from "@/lib/trpc"; -import { ChevronLeft, ChevronRight, Calendar as CalendarIcon } from "lucide-react"; -import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isSameDay, parseISO, addMonths, subMonths } from "date-fns"; +import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Filter, Download } from "lucide-react"; +import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isSameDay, parseISO, addMonths, subMonths, startOfWeek, endOfWeek, addWeeks, subWeeks, addDays, subDays } from "date-fns"; import { fr } from "date-fns/locale"; +type ViewMode = 'month' | 'week' | 'day'; + export default function AdminCalendrier() { const [currentDate, setCurrentDate] = useState(new Date()); const [selectedSequence, setSelectedSequence] = useState(null); const [isDetailOpen, setIsDetailOpen] = useState(false); + const [viewMode, setViewMode] = useState('month'); + const [filterFormation, setFilterFormation] = useState('all'); + const [filterLieu, setFilterLieu] = useState('all'); + const [filterPublicCible, setFilterPublicCible] = useState('all'); const { data: sequences, isLoading } = trpc.sequences.list.useQuery(); + const { data: formations } = trpc.formations.list.useQuery(); + + // Filtrer les séquences selon les critères + const filteredSequences = sequences?.filter((seq) => { + if (filterFormation !== 'all' && seq.formationId !== parseInt(filterFormation)) return false; + if (filterLieu !== 'all' && seq.lieu !== filterLieu) return false; + if (filterPublicCible !== 'all' && seq.publicCible !== filterPublicCible) return false; + return true; + }) || []; + + // Extraire les lieux uniques pour le filtre + const uniqueLieux = Array.from(new Set(sequences?.map(s => s.lieu).filter(Boolean))) as string[]; + + // Extraire les publics cibles uniques + const uniquePublicsCibles = Array.from(new Set(sequences?.map(s => s.publicCible).filter(Boolean))) as string[]; const monthStart = startOfMonth(currentDate); const monthEnd = endOfMonth(currentDate); @@ -35,8 +57,8 @@ export default function AdminCalendrier() { ]; const getSequencesForDay = (day: Date) => { - if (!sequences) return []; - return sequences.filter((seq) => { + if (!filteredSequences) return []; + return filteredSequences.filter((seq) => { if (!seq.dates || seq.dates.length === 0) return false; return seq.dates.some((dateForm: any) => { // Gérer le cas où dateDebut est déjà un objet Date ou une chaîne @@ -53,12 +75,24 @@ export default function AdminCalendrier() { }); }; - const handlePreviousMonth = () => { - setCurrentDate(subMonths(currentDate, 1)); + const handlePrevious = () => { + if (viewMode === 'month') { + setCurrentDate(subMonths(currentDate, 1)); + } else if (viewMode === 'week') { + setCurrentDate(subWeeks(currentDate, 1)); + } else { + setCurrentDate(subDays(currentDate, 1)); + } }; - const handleNextMonth = () => { - setCurrentDate(addMonths(currentDate, 1)); + const handleNext = () => { + if (viewMode === 'month') { + setCurrentDate(addMonths(currentDate, 1)); + } else if (viewMode === 'week') { + setCurrentDate(addWeeks(currentDate, 1)); + } else { + setCurrentDate(addDays(currentDate, 1)); + } }; const handleSequenceClick = (sequence: any) => { @@ -66,6 +100,56 @@ export default function AdminCalendrier() { setIsDetailOpen(true); }; + const handleExportPDF = () => { + window.print(); + }; + + const handleExportICal = () => { + const icalContent = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'PRODID:-//Formation Manager Itinova//FR', + 'CALSCALE:GREGORIAN', + 'METHOD:PUBLISH', + 'X-WR-CALNAME:Calendrier des formations', + 'X-WR-TIMEZONE:Europe/Paris', + ]; + + filteredSequences.forEach((seq) => { + if (seq.dates && seq.dates.length > 0) { + seq.dates.forEach((dateForm: any) => { + const dateDebut = typeof dateForm.dateDebut === 'string' ? parseISO(dateForm.dateDebut) : dateForm.dateDebut; + const dateFin = typeof dateForm.dateFin === 'string' ? parseISO(dateForm.dateFin) : dateForm.dateFin; + + const dtstart = format(dateDebut, "yyyyMMdd'T'HHmmss"); + const dtend = format(dateFin, "yyyyMMdd'T'HHmmss"); + + icalContent.push( + 'BEGIN:VEVENT', + `UID:${seq.id}-${dateForm.id || Math.random()}@formation-manager.itinova`, + `DTSTAMP:${format(new Date(), "yyyyMMdd'T'HHmmss'Z'")}`, + `DTSTART:${dtstart}`, + `DTEND:${dtend}`, + `SUMMARY:${seq.nom}`, + `DESCRIPTION:${seq.formation?.nom || 'Formation'} - ${seq.nbInscrits || 0}/${seq.capaciteMax} inscrits`, + `LOCATION:${seq.lieu}`, + 'STATUS:CONFIRMED', + 'SEQUENCE:0', + 'END:VEVENT' + ); + }); + } + }); + + icalContent.push('END:VCALENDAR'); + + const blob = new Blob([icalContent.join('\r\n')], { type: 'text/calendar;charset=utf-8' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `calendrier-formations-${format(currentDate, "yyyy-MM")}.ics`; + link.click(); + }; + const getPublicCibleLabel = (publicCible: string) => { const labels: Record = { directeurs: "Directeurs", @@ -95,38 +179,131 @@ export default function AdminCalendrier() {

+ {/* Filtres */} -
-
- - - {format(currentDate, "MMMM yyyy", { locale: fr })} - - - Cliquez sur une séquence pour voir les détails - +
+
+
+ + + Calendrier des séquences + + + Cliquez sur une séquence pour voir les détails + +
+
+ +
-
- - - + + {/* Barre de filtres */} +
+
+ + Filtres : +
+ + + + {(filterFormation !== 'all' || filterLieu !== 'all' || filterPublicCible !== 'all') && ( + + )} +
+ + {/* Navigation et exports */} +
+

+ {viewMode === 'month' && format(currentDate, "MMMM yyyy", { locale: fr })} + {viewMode === 'week' && `Semaine du ${format(startOfWeek(currentDate, { locale: fr }), "d MMM", { locale: fr })}`} + {viewMode === 'day' && format(currentDate, "EEEE d MMMM yyyy", { locale: fr })} +

+
+ + +
+ + + +
{isLoading ? (
Chargement...
- ) : ( + ) : viewMode === 'month' ? (
{/* En-têtes des jours */} {["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"].map((day) => ( @@ -184,6 +361,81 @@ export default function AdminCalendrier() { ); })}
+ ) : viewMode === 'week' ? ( +
+
+
Heure
+ {Array.from({ length: 7 }).map((_, i) => { + const day = addDays(startOfWeek(currentDate, { locale: fr }), i); + return ( +
+
{format(day, "EEE", { locale: fr })}
+
{format(day, "d MMM", { locale: fr })}
+
+ ); + })} +
+
+ Vue hebdomadaire - Affichage des séquences par jour +
+ {Array.from({ length: 7 }).map((_, i) => { + const day = addDays(startOfWeek(currentDate, { locale: fr }), i); + const daySequences = getSequencesForDay(day); + return daySequences.length > 0 ? ( +
+

{format(day, "EEEE d MMMM", { locale: fr })}

+
+ {daySequences.map((seq) => ( + + ))} +
+
+ ) : null; + })} +
+ ) : ( +
+
+

{format(currentDate, "EEEE d MMMM yyyy", { locale: fr })}

+
+ {getSequencesForDay(currentDate).length > 0 ? ( +
+ {getSequencesForDay(currentDate).map((seq) => ( + + ))} +
+ ) : ( +
+ Aucune séquence prévue pour ce jour +
+ )} +
)}
diff --git a/todo.md b/todo.md index 347a70b..feb1c78 100644 --- a/todo.md +++ b/todo.md @@ -602,3 +602,25 @@ - [x] Identifier la cause de l'erreur lors du survol des vignettes - [x] Corriger le code pour résoudre l'erreur - [x] Tester le calendrier après correction + +## Améliorations du calendrier + +### Filtres du calendrier +- [x] Ajouter un filtre par formation +- [x] Ajouter un filtre par lieu +- [x] Ajouter un filtre par public cible +- [x] Implémenter la logique de filtrage dans la vue calendrier +- [x] Tester les filtres avec différentes combinaisons + +### Vues alternatives du calendrier +- [x] Créer la vue hebdomadaire +- [x] Créer la vue journalière +- [x] Ajouter un sélecteur de vue (mois/semaine/jour) +- [x] Adapter l'affichage des séquences pour chaque vue +- [x] Tester la navigation entre les vues + +### Export du calendrier +- [x] Implémenter l'export PDF du calendrier mensuel +- [x] Implémenter l'export iCal pour partage avec les formateurs +- [x] Ajouter les boutons d'export dans l'interface +- [x] Tester les exports avec différents mois