From 625cbedf0854130fbab3edd094d52c7030444962 Mon Sep 17 00:00:00 2001
From: Manus Sandbox
Date: Mon, 24 Nov 2025 12:17:03 -0500
Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Am=C3=A9liorations=20majeures=20d?=
=?UTF-8?q?u=20calendrier=20:=20ajout=20de=20filtres=20(formation,=20lieu,?=
=?UTF-8?q?=20public=20cible),=20vues=20alternatives=20(semaine,=20jour)?=
=?UTF-8?q?=20et=20exports=20(PDF,=20iCal)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
client/src/pages/AdminCalendrier.tsx | 314 ++++++++++++++++++++++++---
todo.md | 22 ++
2 files changed, 305 insertions(+), 31 deletions(-)
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
+
+
+
+ setViewMode(value as ViewMode)}>
+
+
+
+
+ Mois
+ Semaine
+ Jour
+
+
+
-
-
-
-
-
setCurrentDate(new Date())}
- >
- Aujourd'hui
-
-
-
-
+
+ {/* Barre de filtres */}
+
+
+
+ Filtres :
+
+
+
+
+
+
+ Toutes les formations
+ {formations?.map((formation) => (
+
+ {formation.nom}
+
+ ))}
+
+
+
+
+
+
+
+ Tous les lieux
+ {uniqueLieux.map((lieu) => (
+
+ {lieu}
+
+ ))}
+
+
+
+
+
+
+
+ Tous les publics
+ {uniquePublicsCibles.map((pc) => (
+
+ {getPublicCibleLabel(pc)}
+
+ ))}
+
+
+ {(filterFormation !== 'all' || filterLieu !== 'all' || filterPublicCible !== 'all') && (
+
{
+ setFilterFormation('all');
+ setFilterLieu('all');
+ setFilterPublicCible('all');
+ }}
+ >
+ Réinitialiser
+
+ )}
+
+
+ {/* 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 })}
+
+
+
+
+ PDF
+
+
+
+ iCal
+
+
+
+
+
+
setCurrentDate(new Date())}
+ >
+ Aujourd'hui
+
+
+
+
+
{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) => (
+
handleSequenceClick(seq)}
+ className="w-full text-left p-3 rounded-lg border bg-card hover:bg-accent transition-colors"
+ >
+ {seq.nom}
+ {seq.formation?.nom || "Sans formation"}
+
+ {seq.lieu} • {seq.nbInscrits || 0}/{seq.capaciteMax} inscrits
+
+
+ ))}
+
+
+ ) : null;
+ })}
+
+ ) : (
+
+
+
{format(currentDate, "EEEE d MMMM yyyy", { locale: fr })}
+
+ {getSequencesForDay(currentDate).length > 0 ? (
+
+ {getSequencesForDay(currentDate).map((seq) => (
+
handleSequenceClick(seq)}
+ className="w-full text-left p-4 rounded-lg border bg-card hover:bg-accent transition-colors"
+ >
+
+
+
{seq.nom}
+
{seq.formation?.nom || "Sans formation"}
+
+ 📍 {seq.lieu}
+ 👥 {seq.nbInscrits || 0}/{seq.capaciteMax} inscrits
+ {seq.formateur && 👨🏫 {typeof seq.formateur === 'object' ? seq.formateur.nom : seq.formateur} }
+
+
+
+
+ ))}
+
+ ) : (
+
+ 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