diff --git a/apps/api/src/routers/tablo.ts b/apps/api/src/routers/tablo.ts index 4a12e77..5eba26e 100644 --- a/apps/api/src/routers/tablo.ts +++ b/apps/api/src/routers/tablo.ts @@ -262,8 +262,11 @@ const inviteToTablo = ( const tabloId = c.req.param("tabloId"); const { email: recipientmail } = await c.req.json(); + const recipientEmail = String(recipientmail || "") + .trim() + .toLowerCase(); - if (sender.email === recipientmail) { + if (sender.email?.toLowerCase() === recipientEmail) { return c.json({ error: "You cannot invite yourself" }, 400); } @@ -292,7 +295,7 @@ const inviteToTablo = ( const introEmail = introConfigData?.config?.intro_email; const { error } = await supabase.from("tablo_invites").insert({ - invited_email: recipientmail, + invited_email: recipientEmail, tablo_id: tabloId, invited_by: sender.id, invite_token: token, @@ -311,7 +314,7 @@ const inviteToTablo = ( const { data: recipientUser, error: recipientError } = await supabase .from("profiles") .select("id") - .eq("email", recipientmail) + .eq("email", recipientEmail) .maybeSingle(); if (recipientError) { @@ -324,7 +327,7 @@ const inviteToTablo = ( supabase, streamServerClient, transporter, - recipientmail, + recipientEmail, sender.email ); @@ -378,7 +381,7 @@ const inviteToTablo = ( // Let the user know that they have been invited to the tablo await transporter.sendMail({ from: `${sender.email} via XTablo `, - to: recipientmail, + to: recipientEmail, subject: "Vous avez été invité à un tablo", html: ` ${introEmail ? `

${introEmail}

` : ""} @@ -403,7 +406,8 @@ ${introEmail ? `

${introEmail}

` : ""} const cancelPendingInvite = ( middlewareManager: ReturnType ) => - factory.createHandlers(middlewareManager.regularUserCheck, checkTabloAdmin, async (c) => { + factory.createHandlers(middlewareManager.regularUserCheck, async (c) => { + const user = c.get("user"); const supabase = c.get("supabase"); const streamServerClient = c.get("streamServerClient"); const tabloId = c.req.param("tabloId"); @@ -432,6 +436,27 @@ const cancelPendingInvite = ( return c.json({ error: "Invite is no longer pending" }, 400); } + const isInvitee = invite.invited_email?.toLowerCase() === user.email?.toLowerCase(); + + const { data: adminAccess, error: adminAccessError } = await supabase + .from("tablo_access") + .select("id") + .eq("tablo_id", tabloId) + .eq("user_id", user.id) + .eq("is_active", true) + .eq("is_admin", true) + .maybeSingle(); + + if (adminAccessError) { + return c.json({ error: adminAccessError.message }, 500); + } + + const isAdmin = !!adminAccess; + + if (!isInvitee && !isAdmin) { + return c.json({ error: "You are not authorized to cancel this invite" }, 403); + } + const { error: cancelError } = await supabase .from("tablo_invites") .update({ is_pending: false }) @@ -471,6 +496,112 @@ const cancelPendingInvite = ( return c.json({ message: "Invite cancelled successfully" }); }); +const getPendingInvitesForCurrentUser = ( + middlewareManager: ReturnType +) => + factory.createHandlers(middlewareManager.regularUserCheck, async (c) => { + const user = c.get("user"); + const supabase = c.get("supabase"); + + const { data: pendingInvites, error: pendingInvitesError } = await supabase + .from("tablo_invites") + .select("id, tablo_id, created_at") + .eq("invited_email", user.email?.toLowerCase()) + .eq("is_pending", true) + .order("created_at", { ascending: false }); + + if (pendingInvitesError) { + return c.json({ error: pendingInvitesError.message }, 500); + } + + const tabloIds = Array.from(new Set((pendingInvites || []).map((invite) => invite.tablo_id))); + let tablosById = new Map(); + + if (tabloIds.length > 0) { + const { data: tablos, error: tablosError } = await supabase + .from("tablos") + .select("id, name") + .in("id", tabloIds); + + if (tablosError) { + return c.json({ error: tablosError.message }, 500); + } + + tablosById = new Map((tablos || []).map((tablo) => [tablo.id, tablo.name])); + } + + return c.json({ + invites: (pendingInvites || []).map((invite) => ({ + id: invite.id, + tablo_id: invite.tablo_id, + tablo_name: tablosById.get(invite.tablo_id) || "Tablo", + created_at: invite.created_at, + })), + }); + }); + +const acceptInviteById = (middlewareManager: ReturnType) => + factory.createHandlers(middlewareManager.regularUserCheck, async (c) => { + const user = c.get("user"); + const supabase = c.get("supabase"); + const streamServerClient = c.get("streamServerClient"); + const inviteId = Number(c.req.param("inviteId")); + + if (!Number.isInteger(inviteId) || inviteId <= 0) { + return c.json({ error: "Invalid invite id" }, 400); + } + + const { data: inviteData, error: inviteError } = await supabase + .from("tablo_invites") + .select("id, tablo_id, invited_by, invited_email, is_pending") + .eq("id", inviteId) + .maybeSingle(); + + if (inviteError) { + return c.json({ error: inviteError.message }, 500); + } + + if (!inviteData || !inviteData.is_pending) { + return c.json({ error: "Invite not found or no longer pending" }, 404); + } + + if (inviteData.invited_email?.toLowerCase() !== user.email?.toLowerCase()) { + return c.json({ error: "You are not authorized to accept this invite" }, 403); + } + + try { + await upsertStreamUserFromProfile(supabase, streamServerClient, user.id); + } catch (error) { + console.error("error upserting joining user to stream", error); + return c.json({ error: "Failed to provision chat user" }, 500); + } + + const { error: tabloAccessError } = await supabase.from("tablo_access").insert({ + tablo_id: inviteData.tablo_id, + user_id: user.id, + is_admin: false, + is_active: true, + granted_by: inviteData.invited_by, + }); + + if (tabloAccessError) { + if (tabloAccessError.code !== "23505") { + return c.json({ error: tabloAccessError.message }, 500); + } + } + + await supabase.from("tablo_invites").update({ is_pending: false }).eq("id", inviteData.id); + + try { + await ensureTabloChannelMember(supabase, streamServerClient, inviteData.tablo_id, user.id); + } catch (error) { + console.error("error adding member to channel", error); + return c.json({ error: "Failed to sync chat access for this tablo" }, 500); + } + + return c.json({ tablo_id: inviteData.tablo_id }); + }); + const joinTablo = factory.createHandlers(async (c) => { const { token } = await c.req.json(); @@ -712,6 +843,8 @@ export const getTabloRouter = (config: AppConfig) => { tabloRouter.post("/create", ...createTablo(middlewareManager)); tabloRouter.patch("/update", ...updateTablo(middlewareManager)); tabloRouter.delete("/delete", ...deleteTablo); + tabloRouter.get("/invites/pending", ...getPendingInvitesForCurrentUser(middlewareManager)); + tabloRouter.post("/invites/:inviteId/accept", ...acceptInviteById(middlewareManager)); tabloRouter.post("/invite/:tabloId", ...inviteToTablo(config, middlewareManager)); tabloRouter.delete("/invite/:tabloId/:inviteId", ...cancelPendingInvite(middlewareManager)); tabloRouter.post("/join", ...joinTablo); diff --git a/apps/main/src/components/EventTypeCard.test.tsx b/apps/main/src/components/EventTypeCard.test.tsx index 55ad049..45620e8 100644 --- a/apps/main/src/components/EventTypeCard.test.tsx +++ b/apps/main/src/components/EventTypeCard.test.tsx @@ -118,7 +118,7 @@ describe("EventTypeCard", () => { const { container } = renderWithProviders( ); - const card = container.querySelector(".opacity-60"); + const card = container.querySelector(".opacity-70"); expect(card).toBeInTheDocument(); }); }); diff --git a/apps/main/src/components/EventTypeCard.tsx b/apps/main/src/components/EventTypeCard.tsx index c171b09..7cf7984 100644 --- a/apps/main/src/components/EventTypeCard.tsx +++ b/apps/main/src/components/EventTypeCard.tsx @@ -61,7 +61,11 @@ export function EventTypeCard({ return ( {eventType.name} @@ -71,6 +75,7 @@ export function EventTypeCard({ variant="ghost" size="icon" onClick={() => setIsEmbedModalOpen(true)} + className="text-muted-foreground hover:bg-muted hover:text-foreground" aria-label={t("eventTypeCard.aria.settings")} > @@ -81,6 +86,7 @@ export function EventTypeCard({ onClick={() => window.open(getPublicLink(eventType.standardName ?? null, "normal"), "_blank") } + className="text-muted-foreground hover:bg-muted hover:text-foreground" aria-label={t("eventTypeCard.aria.preview")} > @@ -89,6 +95,7 @@ export function EventTypeCard({ variant="ghost" size="icon" onClick={() => handleEditEventType(eventType.id, eventType as EventTypeConfig)} + className="text-muted-foreground hover:bg-muted hover:text-foreground" aria-label={t("eventTypeCard.aria.edit")} > @@ -146,7 +153,7 @@ export function EventTypeCard({ - + {t("eventTypeCard.status")} + + +
+ {t("invites.title")} +
+
+ {invites.map((invite) => ( +
+ {invite.tablo_name} +
+ + +
+
+ ))} +
+
+ + ); +} + function ProfileDropdown() { const { t } = useTranslation("navigation"); const user = useUser(); @@ -314,13 +394,6 @@ function ProfileDropdown() { - - - - {t("myEvents")} - - - @@ -401,6 +474,7 @@ export function TopBar() { )} + diff --git a/apps/main/src/hooks/tablo_invites.ts b/apps/main/src/hooks/tablo_invites.ts index c6d26bf..28df205 100644 --- a/apps/main/src/hooks/tablo_invites.ts +++ b/apps/main/src/hooks/tablo_invites.ts @@ -6,6 +6,12 @@ import { useUser } from "../providers/UserStoreProvider"; import { useAuthedApi } from "./auth"; type TabloInvite = Database["public"]["Tables"]["tablo_invites"]["Row"]; +type PendingReceivedInvite = { + id: number; + tablo_id: string; + tablo_name: string; + created_at: string; +}; // Fetch all pending invites created by the current user // export const usePendingTabloInvites = () => { @@ -62,7 +68,9 @@ export const useCancelTabloInvite = () => { }, onSuccess: (_data, { tabloId }) => { queryClient.invalidateQueries({ queryKey: ["tablo-invites", tabloId] }); + queryClient.invalidateQueries({ queryKey: ["tablo-invites", "received"] }); queryClient.invalidateQueries({ queryKey: ["tablo-members", tabloId] }); + queryClient.invalidateQueries({ queryKey: ["notifications"] }); toast.add( { title: "Invitation retirée", @@ -85,3 +93,56 @@ export const useCancelTabloInvite = () => { }, }); }; + +export const useReceivedTabloInvites = () => { + const api = useAuthedApi(); + + return useQuery({ + queryKey: ["tablo-invites", "received"], + queryFn: async () => { + const { data } = await api.get<{ invites: PendingReceivedInvite[] }>( + "/api/v1/tablos/invites/pending" + ); + return data.invites; + }, + refetchInterval: 30000, + }); +}; + +export const useAcceptTabloInvite = () => { + const api = useAuthedApi(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ inviteId }: { inviteId: number }) => { + const { data } = await api.post<{ tablo_id: string }>( + `/api/v1/tablos/invites/${inviteId}/accept` + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["tablo-invites", "received"] }); + queryClient.invalidateQueries({ queryKey: ["tablos"] }); + queryClient.invalidateQueries({ queryKey: ["notifications"] }); + toast.add( + { + title: "Invitation acceptée", + description: "Vous avez rejoint le tablo", + type: "success", + }, + { timeout: 3000 } + ); + }, + onError: (error) => { + console.error("Error accepting invite:", error); + toast.add( + { + title: "Erreur", + description: "Impossible d'accepter l'invitation", + type: "error", + }, + { timeout: 5000 } + ); + }, + }); +}; diff --git a/apps/main/src/locales/en/availabilities.json b/apps/main/src/locales/en/availabilities.json index b980275..dca0a23 100644 --- a/apps/main/src/locales/en/availabilities.json +++ b/apps/main/src/locales/en/availabilities.json @@ -4,7 +4,8 @@ "tabs": { "availabilities": "Availabilities", "exceptions": "Exceptions", - "visualization": "Visualization" + "visualization": "Visualization", + "callTypes": "Call types" }, "actions": { "save": "Save", @@ -50,6 +51,14 @@ "exceptionDate": "Exception date" } }, + "callTypes": { + "title": "Call Types", + "description": "Create and manage your call types for bookings.", + "create": "New Call Type", + "empty": "No call types configured yet.", + "readOnly": "You are in read-only mode. You cannot create call types.", + "required": "Please fill all required fields." + }, "copy": { "title": "Copy {{day}} hours", "description": "Select the days you want to copy these hours to", diff --git a/apps/main/src/locales/en/navigation.json b/apps/main/src/locales/en/navigation.json index 424b0eb..315b6c8 100644 --- a/apps/main/src/locales/en/navigation.json +++ b/apps/main/src/locales/en/navigation.json @@ -26,5 +26,10 @@ "noNotifications": "No new notifications", "allCaughtUp": "You're all caught up!", "viewAll": "View all notifications" + }, + "invites": { + "title": "Invites", + "dismiss": "Dismiss", + "accept": "Accept" } } diff --git a/apps/main/src/locales/fr/availabilities.json b/apps/main/src/locales/fr/availabilities.json index e4601dc..3c4297d 100644 --- a/apps/main/src/locales/fr/availabilities.json +++ b/apps/main/src/locales/fr/availabilities.json @@ -4,7 +4,8 @@ "tabs": { "availabilities": "Disponibilités", "exceptions": "Exceptions", - "visualization": "Visualisation" + "visualization": "Visualisation", + "callTypes": "Types d'appel" }, "actions": { "save": "Enregistrer", @@ -50,6 +51,14 @@ "exceptionDate": "Date de l'exception" } }, + "callTypes": { + "title": "Types d'appel", + "description": "Créez et gérez vos types d'appel pour vos réservations.", + "create": "Nouveau Type d'appel", + "empty": "Aucun type d'appel configuré pour le moment.", + "readOnly": "Vous êtes en mode lecture seule. Vous ne pouvez pas créer de type d'appel.", + "required": "Veuillez remplir tous les champs obligatoires." + }, "copy": { "title": "Copier les horaires de {{day}}", "description": "Sélectionnez les jours vers lesquels vous souhaitez copier ces horaires", diff --git a/apps/main/src/locales/fr/navigation.json b/apps/main/src/locales/fr/navigation.json index 43881cb..8de6425 100644 --- a/apps/main/src/locales/fr/navigation.json +++ b/apps/main/src/locales/fr/navigation.json @@ -26,5 +26,10 @@ "noNotifications": "Aucune nouvelle notification", "allCaughtUp": "Vous êtes à jour !", "viewAll": "Voir toutes les notifications" + }, + "invites": { + "title": "Invitations", + "dismiss": "Ignorer", + "accept": "Accepter" } } diff --git a/apps/main/src/pages/availabilities.tsx b/apps/main/src/pages/availabilities.tsx index 021349f..26f1118 100644 --- a/apps/main/src/pages/availabilities.tsx +++ b/apps/main/src/pages/availabilities.tsx @@ -25,13 +25,17 @@ import { Strong, Text, TypographyH3, TypographyMuted } from "@xtablo/ui/componen import { Plus as PlusIcon, SaveIcon } from "lucide-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { EventTypeCard } from "../components/EventTypeCard"; +import { EventTypeModal } from "../components/EventTypeModal"; import { ExceptionModal } from "../components/ExceptionModal"; +import { EventTypeConfig, useEventTypes } from "../hooks/event-types"; import { DEFAULT_AVAILABILITIES, Exception, useAvailabilities, WeeklyAvailability, } from "../hooks/availabilities"; +import { useIsReadOnlyUser } from "../providers/UserStoreProvider"; const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6]; const DAYS_OF_WEEK_DISPLAY = [ @@ -51,6 +55,7 @@ interface TimeRange { export function AvailabilitiesPage() { const { t } = useTranslation(["availabilities", "common"]); + const isReadOnly = useIsReadOnlyUser(); const { updateAvailabilities, draftAvailabilities, @@ -59,6 +64,7 @@ export function AvailabilitiesPage() { deleteException, isModified, } = useAvailabilities(); + const { eventTypes: eventTypesData, addEventType, updateEventType } = useEventTypes(); const [copyModalOpen, setCopyModalOpen] = useState(false); const [sourceDayData, setSourceDayData] = useState<{ @@ -68,6 +74,20 @@ export function AvailabilitiesPage() { } | null>(null); const [selectedDays, setSelectedDays] = useState([]); const [exceptionModalOpen, setExceptionModalOpen] = useState(false); + const [isEventTypeModalOpen, setIsEventTypeModalOpen] = useState(false); + const [editingEventType, setEditingEventType] = useState< + (EventTypeConfig & { id: string }) | null + >(null); + const [eventTypeFormData, setEventTypeFormData] = useState< + Partial & { isActive?: boolean } + >({ + name: "", + description: "", + duration: 60, + bufferTime: 15, + maxBookingsPerDay: 8, + requiresApproval: false, + }); const handleCopyToOtherDays = (sourceDay: number, enabled: boolean, timeRanges: TimeRange[]) => { setSourceDayData({ day: sourceDay, enabled, timeRanges }); @@ -98,6 +118,58 @@ export function AvailabilitiesPage() { }); }; + const handleCreateEventType = () => { + if (isReadOnly) { + toast.add({ + title: t("common:error"), + description: t("availabilities:callTypes.readOnly"), + type: "error", + }); + return; + } + + setEditingEventType(null); + setEventTypeFormData({ + name: "", + description: "", + duration: 60, + isActive: true, + bufferTime: 15, + maxBookingsPerDay: 8, + requiresApproval: false, + }); + setIsEventTypeModalOpen(true); + }; + + const handleEditEventType = (id: string, eventType: EventTypeConfig) => { + setEditingEventType({ id, ...eventType }); + setEventTypeFormData(eventType); + setIsEventTypeModalOpen(true); + }; + + const handleSaveEventType = () => { + if (!eventTypeFormData.name) { + toast.add({ + title: t("common:error"), + description: t("availabilities:callTypes.required"), + type: "error", + }); + return; + } + + if (editingEventType) { + updateEventType({ + id: editingEventType.id, + eventType: eventTypeFormData as EventTypeConfig, + }); + } else { + addEventType({ eventType: eventTypeFormData as EventTypeConfig }); + } + + setIsEventTypeModalOpen(false); + setEditingEventType(null); + }; + // Filter exceptions to only show upcoming dates (today and future) const today = new Date(); today.setHours(0, 0, 0, 0); @@ -111,7 +183,7 @@ export function AvailabilitiesPage() { return (
-
+
{t("availabilities:title")} @@ -122,14 +194,31 @@ export function AvailabilitiesPage() {
- - + + {t("availabilities:tabs.availabilities")} - {t("availabilities:tabs.exceptions")} - + + {t("availabilities:tabs.exceptions")} + + {t("availabilities:tabs.visualization")} + + {t("availabilities:tabs.callTypes")} + @@ -138,7 +227,7 @@ export function AvailabilitiesPage() { )} - - +
-
+
{DAYS_OF_WEEK.map((day) => (
- + {t("availabilities:timezone.your")} @@ -241,7 +330,7 @@ export function AvailabilitiesPage() { - + Information @@ -270,7 +359,12 @@ export function AvailabilitiesPage() {
{upcomingExceptions.length > 0 && ( - )} @@ -285,7 +379,12 @@ export function AvailabilitiesPage() { - @@ -295,7 +394,7 @@ export function AvailabilitiesPage() { {upcomingExceptions.map(({ exception, originalIndex }) => (
@@ -375,6 +474,47 @@ export function AvailabilitiesPage() {
)} + + +
+
+
+

+ {t("availabilities:callTypes.title")} +

+ + {t("availabilities:callTypes.description")} + +
+ +
+ + {eventTypesData.length === 0 ? ( +
+ + {t("availabilities:callTypes.empty")} + +
+ ) : ( +
+ {eventTypesData.map((eventType) => ( + + ))} +
+ )} +
+
@@ -417,6 +557,7 @@ export function AvailabilitiesPage() { variant="default" disabled={selectedDays.length === 0} onClick={applyCopyToSelectedDays} + className="bg-[#804EEC] text-white hover:bg-[#6f3fd4]" > {t("availabilities:copy.copyTo", { count: selectedDays.length })} @@ -424,6 +565,15 @@ export function AvailabilitiesPage() { + + setExceptionModalOpen(false)} diff --git a/apps/main/src/pages/events.tsx b/apps/main/src/pages/events.tsx index 2ff7c1e..e11e149 100644 --- a/apps/main/src/pages/events.tsx +++ b/apps/main/src/pages/events.tsx @@ -1,5 +1,4 @@ import { EventDetailsModal } from "@ui/components/EventDetailsModal"; -import { EventTypeModal } from "@ui/components/EventTypeModal"; import { LoadingSpinner } from "@ui/components/LoadingSpinner"; import { getTextColorFromTabloColor, toast } from "@xtablo/shared"; import { EventAndTablo } from "@xtablo/shared/types/events.types"; @@ -13,21 +12,17 @@ import { SelectTrigger, SelectValue, } from "@xtablo/ui/components/select"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@xtablo/ui/components/tabs"; import { Strong, Text, TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography"; import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, - PlusIcon, SearchIcon, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Outlet, useNavigate } from "react-router-dom"; import { twMerge } from "tailwind-merge"; -import { EventTypeCard } from "../components/EventTypeCard"; -import { EventTypeConfig, useEventTypes } from "../hooks/event-types"; import { useEventsByTablo } from "../hooks/events"; import { useGetAllTabloAccess, useTablosList } from "../hooks/tablos"; import { useIsReadOnlyUser } from "../providers/UserStoreProvider"; @@ -42,7 +37,6 @@ export function EventsPage() { const navigate = useNavigate(); const { t } = useTranslation(["pages", "common"]); const isReadOnly = useIsReadOnlyUser(); - const [activeTab, setActiveTab] = useState<"events" | "event-types">("events"); const statusOptions: BookingStatusOption[] = [ { id: "upcoming", name: t("pages:events.filters.upcoming") }, @@ -58,29 +52,12 @@ export function EventsPage() { const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); - // Event Types state - const [isEventTypeModalOpen, setIsEventTypeModalOpen] = useState(false); - const [editingEventType, setEditingEventType] = useState< - (EventTypeConfig & { id: string }) | null - >(null); - const [eventTypeFormData, setEventTypeFormData] = useState< - Partial & { isActive?: boolean } - >({ - name: "", - description: "", - duration: 60, - bufferTime: 15, - maxBookingsPerDay: 8, - requiresApproval: false, - }); - // Fetch data const { data: tablos, isLoading: tablosLoading } = useTablosList(); const { data: events = [], isLoading: eventsLoading } = useEventsByTablo( selectedTabloId !== "all" ? selectedTabloId : null ); const { data: tabloAccess } = useGetAllTabloAccess(); - const { eventTypes: eventTypesData, addEventType, updateEventType } = useEventTypes(); // Filter and search events const filteredEvents = useMemo(() => { @@ -138,62 +115,6 @@ export function EventsPage() { setCurrentPage(1); }, [searchTerm, statusFilter, selectedTabloId, itemsPerPage]); - // Event Types handlers - const handleCreateEventType = () => { - if (isReadOnly) { - toast.add( - { - title: t("common:error"), - description: - "Vous êtes en mode lecture seule. Vous ne pouvez pas créer de type d'événement.", - type: "error", - }, - { timeout: 5000 } - ); - return; - } - setEditingEventType(null); - setEventTypeFormData({ - name: "", - description: "", - duration: 60, - isActive: true, - bufferTime: 15, - maxBookingsPerDay: 8, - requiresApproval: false, - }); - setIsEventTypeModalOpen(true); - }; - - const handleEditEventType = (id: string, eventType: EventTypeConfig) => { - setEditingEventType({ id, ...eventType }); - setEventTypeFormData(eventType as EventTypeConfig); - setIsEventTypeModalOpen(true); - }; - - const handleSaveEventType = () => { - if (!eventTypeFormData.name) { - toast.add({ - title: "Erreur", - description: "Veuillez remplir tous les champs obligatoires", - type: "error", - }); - return; - } - - if (editingEventType) { - updateEventType({ - id: editingEventType.id, - eventType: eventTypeFormData as EventTypeConfig, - }); - } else { - addEventType({ eventType: eventTypeFormData as EventTypeConfig }); - } - - setIsEventTypeModalOpen(false); - setEditingEventType(null); - }; - // Events handlers const formatEventDateTime = (event: EventAndTablo) => { if (!event.start_date) return "Date non définie"; @@ -299,34 +220,16 @@ export function EventsPage() {
- {/* Main Content with Tabs */} + {/* Main Content */}
- setActiveTab(value as "events" | "event-types")} - className="w-full" - > -
- - {t("pages:events.tabs.events")} - {t("pages:events.tabs.eventTypes")} - +
+ +
- {activeTab === "events" ? ( - - ) : ( - - )} -
- - {/* Events Tab */} - +
{/* Filters */}
@@ -606,38 +509,7 @@ export function EventsPage() {
)} - - - {/* Event Types Tab */} - -
- {eventTypesData?.map((eventType) => ( - - ))} -
- - {eventTypesData?.length === 0 && ( -
- - {t("pages:events.eventTypes.emptyState.title")} - - -
- )} -
- +
{/* Event Details Modal */} - {/* Event Type Modal */} -
{/* Render child routes (e.g. EventModal) */}