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 + > + + + + + + +