From 5d29201552ca6aa4050e3f092839f3cef86822f6 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 5 Oct 2025 20:25:29 +0200 Subject: [PATCH 1/6] improve planning --- ui/src/hooks/tablos.ts | 15 ++++ ui/src/pages/planning.tsx | 109 +++++++++++++++++-------- ui/src/providers/UserStoreProvider.tsx | 2 +- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/ui/src/hooks/tablos.ts b/ui/src/hooks/tablos.ts index 79a94ef..53ce751 100644 --- a/ui/src/hooks/tablos.ts +++ b/ui/src/hooks/tablos.ts @@ -237,3 +237,18 @@ export const useDeleteTablo = () => { }, }); }; + +export const useGetAllTabloAccess = () => { + const user = useUser(); + const { data, isLoading, error } = useQuery({ + queryKey: ["tablo-access", user.id], + queryFn: async () => { + const { data } = await supabase + .from("tablo_access") + .select("*") + .eq("user_id", user.id); + return data; + }, + }); + return { data, isLoading, error }; +}; diff --git a/ui/src/pages/planning.tsx b/ui/src/pages/planning.tsx index 6588391..55c3123 100644 --- a/ui/src/pages/planning.tsx +++ b/ui/src/pages/planning.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { useTablosList } from "@ui/hooks/tablos"; +import { useGetAllTabloAccess, useTablosList } from "@ui/hooks/tablos"; import { useEventsByTablo, useDeleteEvent } from "@ui/hooks/events"; import { Select, @@ -13,6 +13,7 @@ import { generateICSFromEvents, downloadICSFile } from "@ui/utils/helpers"; import { ImportICSModal } from "@ui/components/ImportICSModal"; import { WebcalModal } from "@ui/components/WebcalModal"; import { FolderInputIcon, PlusIcon } from "lucide-react"; +import { EventAndTablo } from "@ui/types/events.types"; type ViewType = "month" | "week" | "day"; @@ -34,9 +35,23 @@ export const PlanningPage = () => { // Fetch events for selected tablo or all tablos const { data: tabloEvents = [], isLoading: tabloEventsLoading } = useEventsByTablo(selectedTabloId !== "all" ? selectedTabloId : null); + // Fetch all tablo accesses + const { data: tabloAccess } = useGetAllTabloAccess(); const deleteEvent = useDeleteEvent(); + // Check if an event can be deleted (e.g., based on permissions, event status, etc.) + const canDeleteEvent = (event: EventAndTablo) => { + if ( + tabloAccess?.find( + (access) => access.tablo_id === event.tablo_id && access.is_admin + ) + ) { + return true; + } + return false; + }; + // Keyboard shortcuts for view switching useEffect(() => { const handleKeyPress = (event: KeyboardEvent) => { @@ -251,12 +266,13 @@ export const PlanningPage = () => { } ${startOfWeek.getFullYear()}`; } } else { - return currentDate.toLocaleDateString("fr-FR", { + const dateString = currentDate.toLocaleDateString("fr-FR", { weekday: "long", year: "numeric", month: "long", day: "numeric", }); + return dateString.charAt(0).toUpperCase() + dateString.slice(1); } }; @@ -359,11 +375,12 @@ export const PlanningPage = () => {
{getEventsForDate(day) + .sort((a, b) => a.start_time.localeCompare(b.start_time)) .slice(0, 3) .map((event) => (
{ )}
- + {canDeleteEvent(event) && ( + + )}
))} {getEventsForDate(day).length > 3 && ( @@ -499,11 +518,18 @@ export const PlanningPage = () => { height: `${eventHeight}px`, minHeight: "30px", }} + title={`${formatTime(event.start_time)} - ${formatTime( + event.end_time + )} ${event.title}${ + selectedTabloId === "all" && event.tablo_name + ? ` - ${event.tablo_name}` + : "" + }`} onClick={(e) => { e.stopPropagation(); }} > -
+
{event.title} {selectedTabloId === "all" && event.tablo_name && ( @@ -517,16 +543,18 @@ export const PlanningPage = () => { {formatTime(event.end_time)}
)} - + {canDeleteEvent(event) && ( + + )}
); })} @@ -601,6 +629,13 @@ export const PlanningPage = () => { height: `${eventHeight}px`, minHeight: "30px", }} + title={`${formatTime(event.start_time)} - ${formatTime( + event.end_time + )} ${event.title}${ + selectedTabloId === "all" && event.tablo_name + ? ` - ${event.tablo_name}` + : "" + }`} onClick={(e) => { e.stopPropagation(); }} @@ -624,16 +659,18 @@ export const PlanningPage = () => { {event.description} )} - + {canDeleteEvent(event) && ( + + )} ); })} @@ -800,9 +837,9 @@ export const PlanningPage = () => { -

+

{getViewTitle()} -

+
diff --git a/ui/src/providers/UserStoreProvider.tsx b/ui/src/providers/UserStoreProvider.tsx index c7da51b..6f9f79a 100644 --- a/ui/src/providers/UserStoreProvider.tsx +++ b/ui/src/providers/UserStoreProvider.tsx @@ -6,7 +6,7 @@ import { useSession } from "@ui/contexts/SessionContext"; import { api } from "@ui/lib/api"; import { LoadingSpinner } from "@ui/components/LoadingSpinner"; -type User = Tables<"profiles"> & { +export type User = Tables<"profiles"> & { streamToken: string | null; }; From f8f931aab129fa6b47305bedd1fad748bf642af9 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 5 Oct 2025 22:50:31 +0200 Subject: [PATCH 2/6] Improve planning view --- ui/src/components/EventDetailsModal.tsx | 4 ++ ui/src/pages/bookings.tsx | 16 +++++++- ui/src/pages/planning.tsx | 53 +++++++++++++++++++------ 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/ui/src/components/EventDetailsModal.tsx b/ui/src/components/EventDetailsModal.tsx index 28da6d3..b736aeb 100644 --- a/ui/src/components/EventDetailsModal.tsx +++ b/ui/src/components/EventDetailsModal.tsx @@ -11,12 +11,15 @@ interface EventDetailsModalProps { isOpen: boolean; onClose: () => void; onEdit?: () => void; + canEdit?: boolean; } export const EventDetailsModal = ({ event, isOpen, onClose, + onEdit, + canEdit = false, }: EventDetailsModalProps) => { if (!event) return null; @@ -139,6 +142,7 @@ export const EventDetailsModal = ({ + {canEdit && onEdit && }
); diff --git a/ui/src/pages/bookings.tsx b/ui/src/pages/bookings.tsx index acab950..1729bdf 100644 --- a/ui/src/pages/bookings.tsx +++ b/ui/src/pages/bookings.tsx @@ -16,7 +16,7 @@ import { SearchIcon } from "lucide-react"; import { CalendarIcon } from "@ui/ui-library/icons/outline/calendar"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { useEventsByTablo } from "@ui/hooks/events"; -import { useTablosList } from "@ui/hooks/tablos"; +import { useTablosList, useGetAllTabloAccess } from "@ui/hooks/tablos"; import { EventAndTablo } from "@ui/types/events.types"; import { LoadingSpinner } from "@ui/components/LoadingSpinner"; import { EventDetailsModal } from "@ui/components/EventDetailsModal"; @@ -57,6 +57,8 @@ export const BookingsPage = () => { const { data: events = [], isLoading: eventsLoading } = useEventsByTablo( selectedTabloId !== "all" ? selectedTabloId : null ); + // Fetch all tablo accesses for permissions + const { data: tabloAccess } = useGetAllTabloAccess(); // Filter and search events const filteredEvents = useMemo(() => { @@ -184,8 +186,17 @@ export const BookingsPage = () => { navigate(`/planning/create?date=${dateParam}${tabloParam}`); }; + // Check if an event can be edited (admin access required) + const canEditEvent = (event: EventAndTablo) => { + return tabloAccess?.find( + (access) => access.tablo_id === event.tablo_id && access.is_admin + ) + ? true + : false; + }; + const handleEditEvent = (event: EventAndTablo) => { - if (event.event_id && event.tablo_id) { + if (event.event_id && event.tablo_id && canEditEvent(event)) { navigate(`/planning/${event.tablo_id}/events/${event.event_id}/edit`); } }; @@ -529,6 +540,7 @@ export const BookingsPage = () => { setSelectedEvent(null); }} onEdit={() => selectedEvent && handleEditEvent(selectedEvent)} + canEdit={selectedEvent ? canEditEvent(selectedEvent) : false} /> diff --git a/ui/src/pages/planning.tsx b/ui/src/pages/planning.tsx index 55c3123..28ee1cb 100644 --- a/ui/src/pages/planning.tsx +++ b/ui/src/pages/planning.tsx @@ -52,6 +52,11 @@ export const PlanningPage = () => { return false; }; + // Check if an event can be edited (same logic as delete - admin access required) + const canEditEvent = (event: EventAndTablo) => { + return canDeleteEvent(event); + }; + // Keyboard shortcuts for view switching useEffect(() => { const handleKeyPress = (event: KeyboardEvent) => { @@ -380,21 +385,29 @@ export const PlanningPage = () => { .map((event) => (
{ e.stopPropagation(); - navigate({ - pathname: "/planning/create", - search: - selectedTabloId === "all" - ? `?date=${day.toISOString()}` - : `?date=${day.toISOString()}&tablo_id=${selectedTabloId}`, - }); + if (canEditEvent(event)) { + navigate({ + pathname: "/planning/create", + search: + selectedTabloId === "all" + ? `?date=${day.toISOString()}` + : `?date=${day.toISOString()}&tablo_id=${selectedTabloId}`, + }); + } }} >
@@ -512,7 +525,13 @@ export const PlanningPage = () => { return (
{ selectedTabloId === "all" && event.tablo_name ? ` - ${event.tablo_name}` : "" - }`} + }${!canEditEvent(event) ? " (Lecture seule)" : ""}`} onClick={(e) => { e.stopPropagation(); + // Read-only events are not clickable }} >
@@ -623,7 +643,13 @@ export const PlanningPage = () => { return (
{ selectedTabloId === "all" && event.tablo_name ? ` - ${event.tablo_name}` : "" - }`} + }${!canEditEvent(event) ? " (Lecture seule)" : ""}`} onClick={(e) => { e.stopPropagation(); + // Read-only events are not clickable }} >
From 222391f315318006b7d0dd0dbda9e63f653ac94d Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 5 Oct 2025 23:06:37 +0200 Subject: [PATCH 3/6] Tmp --- .../{CreateEventModal.tsx => EventModal.tsx} | 126 ++++++++++++------ ui/src/lib/routes.tsx | 9 +- ui/src/pages/planning.tsx | 29 ++-- 3 files changed, 104 insertions(+), 60 deletions(-) rename ui/src/components/{CreateEventModal.tsx => EventModal.tsx} (74%) diff --git a/ui/src/components/CreateEventModal.tsx b/ui/src/components/EventModal.tsx similarity index 74% rename from ui/src/components/CreateEventModal.tsx rename to ui/src/components/EventModal.tsx index 0346d11..1e67c9f 100644 --- a/ui/src/components/CreateEventModal.tsx +++ b/ui/src/components/EventModal.tsx @@ -1,7 +1,7 @@ import { Event, EventInsert } from "@ui/types/events.types"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useTablosList } from "@ui/hooks/tablos"; -import { useCreateEvents } from "@ui/hooks/events"; +import { useCreateEvents, useEvent, useUpdateEvent } from "@ui/hooks/events"; import { useUser } from "@ui/providers/UserStoreProvider"; import { Select, @@ -14,16 +14,21 @@ 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"; -import { useNavigate, useSearchParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; + +export const EventModal = ({ mode }: { mode: "create" | "edit" }) => { + const { event_id } = useParams(); + const { data: event } = useEvent(event_id as string); -export const CreateEventModal = () => { const user = useUser(); const [searchParams] = useSearchParams(); const tablo_id = searchParams.get("tablo_id"); - const date = new Date(searchParams.get("date") || ""); + const dateFromParams = searchParams.get("date"); + const date = dateFromParams ? new Date(dateFromParams) : new Date(); const { data: tablos, isLoading: tablosLoading } = useTablosList(); const createEvents = useCreateEvents(); + const updateEvent = useUpdateEvent(); const timeOptions = useTimePicker({ intervalInMinute: 15 }); const navigate = useNavigate(); @@ -62,7 +67,7 @@ export const CreateEventModal = () => { return nearestOption?.id || ""; }; - const [createdEvent, setCreatedEvent] = useState({ + const [formEvent, setFormEvent] = useState({ start_date: date ? getLocalDateString(date) : "", start_time: date ? getNearestTimeOption(date, "start") : "", end_time: date ? getNearestTimeOption(date, "end") : "", @@ -71,13 +76,30 @@ export const CreateEventModal = () => { created_by: user.id, }); + // Initialize form data when in edit mode + useEffect(() => { + if (mode === "edit" && event) { + setFormEvent({ + start_date: event.start_date, + start_time: event.start_time || "", + end_time: event.end_time || "", + tablo_id: event.tablo_id, + title: event.title, + description: event.description || "", + created_by: event.created_by, + }); + } + }, [mode, event]); + return (
{/* Header with colored accent */}
-

Nouvel événement

+

+ {mode === "edit" ? "Modifier l'événement" : "Nouvel événement"} +

- {date.toLocaleDateString("fr-FR", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - })} + {mode === "edit" && event + ? new Date(event.start_date).toLocaleDateString("fr-FR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }) + : date.toLocaleDateString("fr-FR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })}
@@ -114,10 +143,10 @@ export const CreateEventModal = () => {
- setCreatedEvent({ - ...createdEvent, + setFormEvent({ + ...formEvent, title: e.target.value, } as Event) } @@ -136,10 +165,10 @@ export const CreateEventModal = () => { { const option = timeOptions.find( (option) => option.id === value ); if (option && value) { - setCreatedEvent({ - ...createdEvent, + setFormEvent({ + ...formEvent, start_time: value.toString(), }); } @@ -225,14 +254,14 @@ export const CreateEventModal = () => {