From 6b4710a902dcbc2845409cf57e9e2842c9e6175a Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Wed, 9 Jul 2025 08:01:53 +0200 Subject: [PATCH] Improve planning a lot --- ui/src/components/CreateEventModal.tsx | 210 +++++++++++++---- ui/src/hooks/events.ts | 17 +- ui/src/pages/planning.tsx | 313 +++++++++++++++++++------ ui/src/ui-library/time-picker.ts | 65 +++-- 4 files changed, 465 insertions(+), 140 deletions(-) diff --git a/ui/src/components/CreateEventModal.tsx b/ui/src/components/CreateEventModal.tsx index c002935..3a6f33e 100644 --- a/ui/src/components/CreateEventModal.tsx +++ b/ui/src/components/CreateEventModal.tsx @@ -3,6 +3,17 @@ import { useState } from "react"; import { useTablosList } from "@ui/hooks/tablos"; import { useCreateEvent } from "@ui/hooks/events"; import { useUser } from "@ui/providers/UserStoreProvider"; +import { + Select, + SelectButton, + SelectPopover, + SelectListBox, + SelectListItem, +} from "@ui/ui-library/select"; +import { useTimePicker } from "@ui/ui-library/time-picker"; +import { DatePicker, DatePickerButton } from "@ui/ui-library/date-picker"; +import { Group } from "react-aria-components"; +import { getLocalTimeZone, parseDate, today } from "@internationalized/date"; interface EventModalProps { date: Date; @@ -13,9 +24,43 @@ export const CreateEventModal = ({ date, onClose }: EventModalProps) => { const user = useUser(); const { data: tablos, isLoading: tablosLoading } = useTablosList(); const createEvent = useCreateEvent(); + const timeOptions = useTimePicker({ intervalInMinute: 15 }); + + // Get the local date string without timezone conversion + const getLocalDateString = (date: Date) => { + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + // Find the nearest time option to the selected date + const getNearestTimeOption = (date: Date, type: "start" | "end") => { + const dateMinutes = date.getHours() * 60 + date.getMinutes(); + + let nearestOption = timeOptions[0]; + let smallestDiff = Infinity; + + for (const option of timeOptions) { + const optionMinutes = option.hour * 60 + option.minute; + const diff = + type === "start" + ? Math.abs(dateMinutes - optionMinutes) + : dateMinutes + 30 - optionMinutes; + + if (0 <= diff && diff < smallestDiff) { + smallestDiff = diff; + nearestOption = option; + } + } + + return nearestOption?.id || ""; + }; + const [createdEvent, setCreatedEvent] = useState({ - start_date: date?.toISOString().split("T")[0] || "", - start_time: date?.toISOString().split("T")[1] || "", + start_date: date ? getLocalDateString(date) : "", + start_time: date ? getNearestTimeOption(date, "start") : "", + end_time: date ? getNearestTimeOption(date, "end") : "", tablo_id: "", title: "", created_by: user.id, @@ -31,6 +76,7 @@ export const CreateEventModal = ({ date, onClose }: EventModalProps) => { diff --git a/ui/src/hooks/events.ts b/ui/src/hooks/events.ts index db2653c..11e2d60 100644 --- a/ui/src/hooks/events.ts +++ b/ui/src/hooks/events.ts @@ -10,10 +10,20 @@ import { } from "@ui/types/events.types"; // Fetch events for a specific tablo -export const useEventsByTablo = (tabloId: string) => { +export const useEventsByTablo = (tabloId: string | null) => { return useQuery({ queryKey: ["events", tabloId], queryFn: async () => { + if (!tabloId) { + const { data, error } = await supabase + .from("events_and_tablos") + .select("*") + .order("start_date", { ascending: true }) + .order("start_time", { ascending: true }); + + if (error) throw error; + return data as EventAndTablo[]; + } const { data, error } = await supabase .from("events_and_tablos") .select("*") @@ -24,7 +34,6 @@ export const useEventsByTablo = (tabloId: string) => { if (error) throw error; return data as EventAndTablo[]; }, - enabled: !!tabloId, }); }; @@ -92,8 +101,8 @@ export const useCreateEvent = () => { if (error) throw error; return data as Event; }, - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["events", data.tablo_id] }); + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["events"] }); toast.add( { title: "Événement créé", diff --git a/ui/src/pages/planning.tsx b/ui/src/pages/planning.tsx index 2618298..362515d 100644 --- a/ui/src/pages/planning.tsx +++ b/ui/src/pages/planning.tsx @@ -1,29 +1,33 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useTablosList } from "@ui/hooks/tablos"; import { useEventsByTablo, useDeleteEvent } from "@ui/hooks/events"; import { CreateEventModal } from "@ui/components/CreateEventModal"; +import { + Select, + SelectButton, + SelectPopover, + SelectListBox, + SelectListItem, +} from "@ui/ui-library/select"; +import { useParams } from "react-router-dom"; type ViewType = "month" | "week" | "day"; export const PlanningPage = () => { + const { tablo_id } = useParams(); const [currentDate, setCurrentDate] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(new Date()); const [currentView, setCurrentView] = useState("month"); - const [selectedTabloId, setSelectedTabloId] = useState(""); + const [selectedTabloId, setSelectedTabloId] = useState( + tablo_id || "all" + ); // Fetch tablos const { data: tablos, isLoading: tablosLoading } = useTablosList(); - // Set default tablo if none selected - useEffect(() => { - if (tablos && tablos.length > 0 && !selectedTabloId) { - setSelectedTabloId(tablos[0].id); - } - }, [tablos, selectedTabloId]); - - // Fetch events for selected tablo - const { data: events = [], isLoading: eventsLoading } = - useEventsByTablo(selectedTabloId); + // Fetch events for selected tablo or all tablos + const { data: tabloEvents = [], isLoading: tabloEventsLoading } = + useEventsByTablo(selectedTabloId !== "all" ? selectedTabloId : null); const deleteEvent = useDeleteEvent(); @@ -64,9 +68,68 @@ export const PlanningPage = () => { return `${year}-${month}-${day}`; }; + const formatTime = (time: string) => { + // Remove seconds from time string (HH:MM:SS -> HH:MM) + return time.substring(0, 5); + }; + + const calculateEventHeight = (startTime: string, endTime: string) => { + if (!endTime) return 60; // Default height if no end time + + const [startHour, startMinute] = startTime.split(":").map(Number); + const [endHour, endMinute] = endTime.split(":").map(Number); + + const startTotalMinutes = startHour * 60 + startMinute; + const endTotalMinutes = endHour * 60 + endMinute; + + const durationMinutes = endTotalMinutes - startTotalMinutes; + + // Each hour is 60px, so calculate proportional height + return Math.max(durationMinutes, 30); // Minimum 30px height + }; + + const calculateEventOffset = (startTime: string, slotTime: string) => { + const [startHour, startMinute] = startTime.split(":").map(Number); + const [slotHour] = slotTime.split(":").map(Number); + + if (startHour === slotHour) { + // Calculate offset within the hour slot + return startMinute; // Each minute is 1px + } + + return 0; + }; + const getEventsForDate = (date: Date) => { const dateString = formatDate(date); - return events.filter((event) => event.start_date === dateString); + return tabloEvents.filter((event) => event.start_date === dateString); + }; + + const getCurrentTimePosition = () => { + const now = new Date(); + const minutes = now.getMinutes(); + // Position within the current hour slot (0-60px based on minutes) + return minutes; + }; + + const isWithinCurrentHour = (fullDate: Date, time: string) => { + const now = new Date(); + const nowHour = now.getHours(); + // const nowMinute = now.getMinutes(); + const nowDay = now.getDate(); + + fullDate.setHours( + Number(time.split(":")[0]), + Number(time.split(":")[1]), + 0, + 0 + ); + + const hour = fullDate.getHours(); + // const minute = fullDate.getMinutes(); + const day = fullDate.getDate(); + + return nowDay === day && nowHour === hour; }; const navigateDate = (direction: number) => { @@ -220,20 +283,31 @@ export const PlanningPage = () => { .map((event) => (
{ e.stopPropagation(); setIsEventModalOpen(true); }} > - {event.title} +
+ {formatTime(event.start_time)} {event.title} + {selectedTabloId === "all" && event.tablo_name && ( + + • {event.tablo_name} + + )} +
- - ))} + ); + })} ))} @@ -345,7 +472,10 @@ export const PlanningPage = () => { key={time} className="flex border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer relative min-h-[60px]" onClick={() => { - setSelectedDate(currentDate); + const [hour] = time.split(":").map(Number); + const dateWithTime = new Date(currentDate); + dateWithTime.setHours(hour, 0, 0, 0); + setSelectedDate(dateWithTime); setIsEventModalOpen(true); }} > @@ -353,26 +483,76 @@ export const PlanningPage = () => { {time}
+ {/* Current time indicator for today */} + {isWithinCurrentHour(new Date(currentDate), time) && ( +
+
+
+ )} + {getEventsForDate(currentDate) .filter((event) => event.start_time.startsWith(time.split(":")[0]) ) - .map((event) => ( -
-
{event.title}
-
- {event.start_time} - {event.end_time} -
- {event.description && ( -
- {event.description} + .map((event) => { + const eventHeight = calculateEventHeight( + event.start_time, + event.end_time + ); + const eventOffset = calculateEventOffset( + event.start_time, + time + ); + return ( +
{ + e.stopPropagation(); + }} + > +
+ {event.title} + {selectedTabloId === "all" && event.tablo_name && ( + + • {event.tablo_name} + + )}
- )} -
- ))} + {eventHeight >= 30 && ( +
+ {formatTime(event.start_time)} -{" "} + {formatTime(event.end_time)} +
+ )} + {eventHeight >= 75 && event.description && ( +
+ {event.description} +
+ )} + +
+ ); + })}
))} @@ -391,27 +571,32 @@ export const PlanningPage = () => { - setSelectedTabloId(key as string)} + className="w-full" + isDisabled={tablosLoading} > - - {tablos?.map((tablo) => ( - - ))} - + + + + Tous les tablos + {tablos?.map((tablo) => ( + + {tablo.name} + + ))} + + + @@ -541,21 +726,13 @@ export const PlanningPage = () => { {/* Calendar Views */}
- {eventsLoading && selectedTabloId ? ( + {tabloEventsLoading ? (
Chargement des événements...
- ) : !selectedTabloId ? ( -
-
-

- Sélectionnez un tablo pour voir les événements -

-
-
) : ( <> {currentView === "month" && renderMonthView()} diff --git a/ui/src/ui-library/time-picker.ts b/ui/src/ui-library/time-picker.ts index df2a352..1e45d77 100644 --- a/ui/src/ui-library/time-picker.ts +++ b/ui/src/ui-library/time-picker.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; export type TimeOption = { hour: number; @@ -9,35 +9,60 @@ export type TimeOption = { export function useTimePicker({ intervalInMinute, + format = "24h", }: { intervalInMinute: 15 | 30; + format?: "12h" | "24h"; }): Array { return React.useMemo(() => { const options = []; - for (let hour = 0; hour < 24; hour++) { - const period = hour >= 12 ? 'PM' : 'AM'; - let hourIn12Format = hour % 12; + if (format === "12h") { + for (let hour = 0; hour < 24; hour++) { + const period = hour >= 12 ? "PM" : "AM"; + let hourIn12Format = hour % 12; - if (hourIn12Format === 0) { - hourIn12Format = 12; + if (hourIn12Format === 0) { + hourIn12Format = 12; + } + + for ( + let interval = 0; + interval < Math.floor(60 / intervalInMinute); + interval++ + ) { + const minutes = interval * intervalInMinute; + options.push({ + hour, + minute: minutes, + value: `${hourIn12Format}:${ + minutes === 0 ? "00" : minutes + } ${period}`, + id: `${hourIn12Format}:${minutes === 0 ? "00" : minutes} ${period}`, + }); + } } - - for ( - let interval = 0; - interval < Math.floor(60 / intervalInMinute); - interval++ - ) { - const minutes = interval * intervalInMinute; - options.push({ - hour, - minute: minutes, - value: `${hourIn12Format}:${minutes === 0 ? '00' : minutes} ${period}`, - id: `${hourIn12Format}:${minutes === 0 ? '00' : minutes} ${period}`, - }); + } else { + for (let hour = 0; hour < 24; hour++) { + for ( + let interval = 0; + interval < Math.floor(60 / intervalInMinute); + interval++ + ) { + const minutes = interval * intervalInMinute; + options.push({ + hour, + minute: minutes, + value: `${hour.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}`, + id: `${hour.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}`, + }); + } } } - return options; }, [intervalInMinute]); }