From 27dc530b1c27cb42cdd39cab4c99e080ef17ba55 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 21 Sep 2025 22:27:21 +0200 Subject: [PATCH] Add event types management functionality, including database schema, UI components, and hooks for CRUD operations. Introduce EventTypeModal for creating and editing event types, and integrate with the navigation bar and event types page for seamless user experience. --- api/src/database.types.ts | 30 +++ sql/18_event_types_table.sql | 100 +++++++++ ui/src/components/CustomModal.tsx | 31 ++- ui/src/components/EventDetailsModal.tsx | 129 +++++------- ui/src/components/EventTypeModal.tsx | 263 ++++++++++++++++++++++++ ui/src/components/NavigationBar.tsx | 17 +- ui/src/hooks/event-types.ts | 156 ++++++++++++++ ui/src/lib/routes.tsx | 5 + ui/src/pages/bookings.tsx | 21 +- ui/src/pages/event-types-page.tsx | 213 +++++++++++++++++++ ui/src/types/database.types.ts | 30 +++ ui/src/utils/helpers.ts | 95 +++++++++ xtablo-expo/lib/database.types.ts | 30 +++ 13 files changed, 1016 insertions(+), 104 deletions(-) create mode 100644 sql/18_event_types_table.sql create mode 100644 ui/src/components/EventTypeModal.tsx create mode 100644 ui/src/hooks/event-types.ts create mode 100644 ui/src/pages/event-types-page.tsx diff --git a/api/src/database.types.ts b/api/src/database.types.ts index f10b08f..779b909 100644 --- a/api/src/database.types.ts +++ b/api/src/database.types.ts @@ -138,6 +138,36 @@ export type Database = { } Relationships: [] } + event_types: { + Row: { + config: Json + created_at: string | null + deleted_at: string | null + id: string + is_active: boolean + updated_at: string | null + user_id: string + } + Insert: { + config?: Json + created_at?: string | null + deleted_at?: string | null + id?: string + is_active?: boolean + updated_at?: string | null + user_id: string + } + Update: { + config?: Json + created_at?: string | null + deleted_at?: string | null + id?: string + is_active?: boolean + updated_at?: string | null + user_id?: string + } + Relationships: [] + } events: { Row: { created_at: string | null diff --git a/sql/18_event_types_table.sql b/sql/18_event_types_table.sql new file mode 100644 index 0000000..63645ac --- /dev/null +++ b/sql/18_event_types_table.sql @@ -0,0 +1,100 @@ +-- Create event_types table to store event type configurations as JSONB +CREATE TABLE IF NOT EXISTS event_types ( + id TEXT PRIMARY KEY DEFAULT generate_random_string(24), + user_id UUID NOT NULL, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + + -- Foreign key constraint to users table (auth.users) + CONSTRAINT fk_event_types_user_id + FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE +); + +-- Create indexes for performance +CREATE INDEX IF NOT EXISTS idx_event_types_user_id ON event_types(user_id); +CREATE INDEX IF NOT EXISTS idx_event_types_is_active ON event_types(is_active); +CREATE INDEX IF NOT EXISTS idx_event_types_deleted_at ON event_types(deleted_at); + +-- Create GIN index for JSONB config column for efficient querying +CREATE INDEX IF NOT EXISTS idx_event_types_config_gin ON event_types USING GIN (config); + +-- Add constraint to ensure config JSONB has required fields +ALTER TABLE event_types ADD CONSTRAINT event_types_config_check + CHECK ( + config ? 'name' AND + (config->>'name')::text <> '' AND + config ? 'duration' AND + (config->>'duration')::int > 0 + ); + +-- Create function to update updated_at timestamp +CREATE OR REPLACE FUNCTION update_event_types_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Create trigger to automatically update updated_at +CREATE TRIGGER update_event_types_updated_at + BEFORE UPDATE ON event_types + FOR EACH ROW + EXECUTE FUNCTION update_event_types_updated_at(); + +-- Enable Row Level Security +ALTER TABLE event_types ENABLE ROW LEVEL SECURITY; + +-- Policy to allow users to view their own event types +CREATE POLICY "Users can view their own event types" ON event_types + FOR SELECT + TO authenticated + USING (user_id = auth.uid()); + +-- Policy to allow users to insert their own event types +CREATE POLICY "Users can insert their own event types" ON event_types + FOR INSERT + TO authenticated + WITH CHECK (user_id = auth.uid()); + +-- Policy to allow users to update their own event types +CREATE POLICY "Users can update their own event types" ON event_types + FOR UPDATE + TO authenticated + USING (user_id = auth.uid()) + WITH CHECK (user_id = auth.uid()); + +-- Policy to allow users to delete their own event types (soft delete) +CREATE POLICY "Users can delete their own event types" ON event_types + FOR UPDATE + TO authenticated + USING (user_id = auth.uid() AND deleted_at IS NULL) + WITH CHECK (user_id = auth.uid()); + +-- Add helpful comments +COMMENT ON TABLE event_types IS + 'Event type configurations stored as JSONB with Row Level Security'; + +COMMENT ON COLUMN event_types.id IS + 'Primary key: random 24-character alphanumeric string'; + +COMMENT ON COLUMN event_types.user_id IS + 'Foreign key reference to auth.users.id'; + +COMMENT ON COLUMN event_types.config IS + 'JSONB configuration containing: name (required), description (optional), duration (required), bufferTime, maxBookingsPerDay, requiresApproval, price, location, minAdvanceBooking, etc.'; + +COMMENT ON COLUMN event_types.is_active IS + 'Whether this event type is active and available for booking'; + +COMMENT ON COLUMN event_types.created_at IS + 'Timestamp when the event type was created'; + +COMMENT ON COLUMN event_types.updated_at IS + 'Timestamp when the event type was last updated (auto-updated by trigger)'; + +COMMENT ON COLUMN event_types.deleted_at IS + 'Timestamp for soft deletion (NULL means not deleted)'; diff --git a/ui/src/components/CustomModal.tsx b/ui/src/components/CustomModal.tsx index 5202907..1e618e6 100644 --- a/ui/src/components/CustomModal.tsx +++ b/ui/src/components/CustomModal.tsx @@ -4,6 +4,7 @@ interface CustomModalProps { onClose: () => void; title: string; children: React.ReactNode; + width?: "sm" | "md" | "lg" | "xl" | "2xl" | "full" | "auto"; } export function CustomModal({ @@ -11,9 +12,31 @@ export function CustomModal({ onClose, title, children, + width = "md", }: CustomModalProps) { if (!isOpen) return null; + const getWidthClasses = () => { + switch (width) { + case "sm": + return "w-full max-w-sm"; + case "md": + return "w-full max-w-md"; + case "lg": + return "w-full max-w-lg"; + case "xl": + return "w-full max-w-xl"; + case "2xl": + return "w-full max-w-2xl"; + case "full": + return "w-full max-w-full mx-4"; + case "auto": + return "w-auto min-w-80 max-w-[90vw]"; + default: + return "w-full max-w-md"; + } + }; + return (
{/* Backdrop */} @@ -23,9 +46,11 @@ export function CustomModal({ /> {/* Modal */} -
+
{/* Header */} -
+

{title}

@@ -50,7 +75,7 @@ export function CustomModal({
{/* Content */} -
{children}
+
{children}
); diff --git a/ui/src/components/EventDetailsModal.tsx b/ui/src/components/EventDetailsModal.tsx index decb6c0..b457e99 100644 --- a/ui/src/components/EventDetailsModal.tsx +++ b/ui/src/components/EventDetailsModal.tsx @@ -1,14 +1,10 @@ import { EventAndTablo } from "@ui/types/events.types"; -import { - Dialog, - DialogHeader, - DialogBody, - DialogCloseButton, -} from "@ui/ui-library/dialog"; +import { DialogBody } from "@ui/ui-library/dialog"; import { Text, Strong } from "@ui/ui-library/text"; import { Button } from "@ui/ui-library/button"; import { CalendarIcon, User } from "lucide-react"; import { CustomModal } from "./CustomModal"; +import { twMerge } from "tailwind-merge"; interface EventDetailsModalProps { event: EventAndTablo | null; @@ -21,7 +17,6 @@ export const EventDetailsModal = ({ event, isOpen, onClose, - onEdit, }: EventDetailsModalProps) => { if (!event) return null; @@ -41,10 +36,10 @@ export const EventDetailsModal = ({ if (event.start_time) { // Remove seconds from time (HH:MM:SS -> HH:MM) const startTime = event.start_time.substring(0, 5); - formatted += ` à ${startTime}`; + formatted += ` de ${startTime}`; if (event.end_time) { const endTime = event.end_time.substring(0, 5); - formatted += ` - ${endTime}`; + formatted += ` à ${endTime}`; } } @@ -83,99 +78,69 @@ export const EventDetailsModal = ({ } }; + console.log(event.tablo_color); return ( - - -
-
-

- {event.title || "Événement sans titre"} -

-
{getEventStatusBadge(event)}
-
+ +
+ {getEventStatusBadge(event)} +
+ {/* Date and Time */} +
+ +
+ + Date et heure + + + {formatEventDateTime(event)} +
- - +
- - {/* Date and Time */} + {/* Tablo */} + {event.tablo_name && (
- +
- Date et heure + Tableau - {formatEventDateTime(event)} + {event.tablo_name}
+ )} - {/* Tablo */} - {event.tablo_name && ( -
-
-
- - Tableau - - - {event.tablo_name} - -
-
- )} - - {/* Description */} - {event.description && ( -
- -
- - Description - - - {event.description} - -
-
- )} - - {/* Event ID (for debugging/reference) */} -
-
- ID: {event.event_id} - {event.tablo_id && Tableau ID: {event.tablo_id}} + {/* Description */} + {event.description && ( +
+ +
+ + Description + + + {event.description} +
- + )} + - {/* Footer */} -
- - {onEdit && ( - - )} -
-
+ {/* Footer */} +
+ +
); }; diff --git a/ui/src/components/EventTypeModal.tsx b/ui/src/components/EventTypeModal.tsx new file mode 100644 index 0000000..307ee6e --- /dev/null +++ b/ui/src/components/EventTypeModal.tsx @@ -0,0 +1,263 @@ +import { Button } from "@ui/ui-library/button"; +import { CustomModal } from "./CustomModal"; +import { + Label, + TextField, + TextArea, + Description, + Input, +} from "@ui/ui-library/field"; +import { NumberField, NumberInput } from "@ui/ui-library/number-field"; +import { + Select, + SelectButton, + SelectListBox, + SelectListItem, + SelectPopover, +} from "@ui/ui-library/select"; +import { EventType } from "@ui/hooks/event-types"; + +export function EventTypeModal({ + isModalOpen, + setIsModalOpen, + editingEventType, + formData, + setFormData, + handleSaveEventType, +}: { + isModalOpen: boolean; + setIsModalOpen: (isOpen: boolean) => void; + editingEventType: EventType | null; + formData: EventType; + setFormData: (formData: EventType) => void; + handleSaveEventType: () => void; +}) { + return ( + setIsModalOpen(false)} + title={ + editingEventType + ? "Modifier le type d'événement" + : "Nouveau type d'événement" + } + width="xl" + > +
+ {/* Basic Information Section */} +
+ setFormData({ ...formData, name: value })} + isRequired + > + + + + + + +