diff --git a/api/src/database.types.ts b/api/src/database.types.ts index 8d1f8cc..cbb3ea7 100644 --- a/api/src/database.types.ts +++ b/api/src/database.types.ts @@ -63,6 +63,67 @@ export type Database = { } Relationships: [] } + events: { + Row: { + created_at: string | null + created_by: string + deleted_at: string | null + description: string | null + end_time: string | null + id: string + start_date: string + start_time: string + tablo_id: string + title: string + } + Insert: { + created_at?: string | null + created_by: string + deleted_at?: string | null + description?: string | null + end_time?: string | null + id?: string + start_date: string + start_time: string + tablo_id: string + title: string + } + Update: { + created_at?: string | null + created_by?: string + deleted_at?: string | null + description?: string | null + end_time?: string | null + id?: string + start_date?: string + start_time?: string + tablo_id?: string + title?: string + } + Relationships: [ + { + foreignKeyName: "fk_events_tablo_id" + columns: ["tablo_id"] + isOneToOne: false + referencedRelation: "events_and_tablos" + referencedColumns: ["tablo_id"] + }, + { + foreignKeyName: "fk_events_tablo_id" + columns: ["tablo_id"] + isOneToOne: false + referencedRelation: "tablos" + referencedColumns: ["id"] + }, + { + foreignKeyName: "fk_events_tablo_id" + columns: ["tablo_id"] + isOneToOne: false + referencedRelation: "user_tablos" + referencedColumns: ["id"] + }, + ] + } feedbacks: { Row: { created_at: string | null @@ -137,6 +198,13 @@ export type Database = { user_id?: string } Relationships: [ + { + foreignKeyName: "fk_tablo_access_tablo_id" + columns: ["tablo_id"] + isOneToOne: false + referencedRelation: "events_and_tablos" + referencedColumns: ["tablo_id"] + }, { foreignKeyName: "fk_tablo_access_tablo_id" columns: ["tablo_id"] @@ -183,6 +251,13 @@ export type Database = { tablo_id?: string } Relationships: [ + { + foreignKeyName: "fk_tablo_invitations_tablo_id" + columns: ["tablo_id"] + isOneToOne: false + referencedRelation: "events_and_tablos" + referencedColumns: ["tablo_id"] + }, { foreignKeyName: "fk_tablo_invitations_tablo_id" columns: ["tablo_id"] @@ -237,6 +312,21 @@ export type Database = { } } Views: { + events_and_tablos: { + Row: { + description: string | null + end_time: string | null + event_id: string | null + start_date: string | null + start_time: string | null + tablo_color: string | null + tablo_id: string | null + tablo_name: string | null + tablo_status: string | null + title: string | null + } + Relationships: [] + } user_tablos: { Row: { access_level: string | null diff --git a/sql/14_create_events_table.sql b/sql/14_create_events_table.sql new file mode 100644 index 0000000..b88ed67 --- /dev/null +++ b/sql/14_create_events_table.sql @@ -0,0 +1,114 @@ +-- Create events table for calendar/planning functionality +CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY DEFAULT generate_random_string(24), + tablo_id TEXT NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + start_date DATE NOT NULL, + start_time TIME NOT NULL, + end_time TIME, + created_by UUID NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, + + -- Foreign key constraint to tablos table + CONSTRAINT fk_events_tablo_id + FOREIGN KEY (tablo_id) REFERENCES tablos(id) ON DELETE CASCADE, + + -- Foreign key constraint to users table (auth.users) + CONSTRAINT fk_events_created_by + FOREIGN KEY (created_by) REFERENCES auth.users(id) ON DELETE CASCADE +); + +-- Create indexes for performance +CREATE INDEX IF NOT EXISTS idx_events_tablo_id ON events(tablo_id); +CREATE INDEX IF NOT EXISTS idx_events_created_by ON events(created_by); +CREATE INDEX IF NOT EXISTS idx_events_start_date ON events(start_date); +CREATE INDEX IF NOT EXISTS idx_events_deleted_at ON events(deleted_at); + +-- Enable Row Level Security +ALTER TABLE events ENABLE ROW LEVEL SECURITY; + +-- Policy to allow users to view events from tablos they have access to +CREATE POLICY "Users can view events from accessible tablos" ON events + FOR SELECT USING ( + EXISTS ( + SELECT 1 FROM user_tablos ut + JOIN events ON ut.id = events.tablo_id + WHERE events.deleted_at IS NULL + AND ( + ut.user_id = (SELECT auth.uid()) + ) + ) + ); + +-- Policy to allow users to insert events into tablos they have access to +CREATE POLICY "Users can insert events into accessible tablos" ON events + FOR INSERT WITH CHECK ( + (SELECT auth.uid()) = created_by + AND EXISTS ( + SELECT 1 FROM user_tablos ut + JOIN events ON ut.id = events.tablo_id + WHERE events.deleted_at IS NULL + AND ( + ut.user_id = (SELECT auth.uid()) + ) + ) + ); + +-- Policy to allow users to update their own events in accessible tablos +CREATE POLICY "Users can update their own events in accessible tablos" ON events + FOR UPDATE USING ( + created_by = (SELECT auth.uid()) + AND EXISTS ( + SELECT 1 FROM user_tablos ut + JOIN events ON ut.id = events.tablo_id + WHERE events.deleted_at IS NULL + AND ( + ut.user_id = (SELECT auth.uid()) + ) + ) + ) WITH CHECK ( + created_by = (SELECT auth.uid()) + AND EXISTS ( + SELECT 1 FROM user_tablos ut + JOIN events ON ut.id = events.tablo_id + WHERE events.deleted_at IS NULL + AND ( + ut.user_id = (SELECT auth.uid()) + ) + ) + ); + +-- Policy to allow users to delete their own events in accessible tablos +CREATE POLICY "Users can delete their own events in accessible tablos" ON events + FOR DELETE USING ( + created_by = (SELECT auth.uid()) + AND EXISTS ( + SELECT 1 FROM user_tablos ut + JOIN events ON ut.id = events.tablo_id + WHERE events.deleted_at IS NULL + AND ( + ut.user_id = (SELECT auth.uid()) + ) + ) + ); + +-- Add comments to document the table +COMMENT ON TABLE events IS + 'Calendar events linked to tablos with Row Level Security'; + +COMMENT ON COLUMN events.id IS + 'Primary key: random 24-character alphanumeric string'; + +COMMENT ON COLUMN events.tablo_id IS + 'Foreign key reference to tablos.id (24-character string)'; + +COMMENT ON COLUMN events.start_date IS + 'Date of the event (YYYY-MM-DD format)'; + +COMMENT ON COLUMN events.start_time IS + 'Start time of the event (HH:MM format)'; + +COMMENT ON COLUMN events.end_time IS + 'End time of the event (HH:MM format), optional'; \ No newline at end of file diff --git a/sql/15_create_events_and_tablos_view.sql b/sql/15_create_events_and_tablos_view.sql new file mode 100644 index 0000000..fcccac0 --- /dev/null +++ b/sql/15_create_events_and_tablos_view.sql @@ -0,0 +1,22 @@ +CREATE OR REPLACE VIEW events_and_tablos +WITH (security_invoker) +AS +SELECT DISTINCT + e.id as event_id, + e.title, + e.start_date, + e.start_time, + e.end_time, + e.description, + t.id as tablo_id, + t.name as tablo_name, + t.color as tablo_color, + t.status as tablo_status +FROM events e +LEFT JOIN tablos t ON e.tablo_id = t.id +WHERE e.deleted_at IS NULL AND t.deleted_at IS NULL +ORDER BY e.start_date ASC, e.start_time ASC; + +-- Add comment to document the view +COMMENT ON VIEW events_and_tablos IS + 'View that returns all events and their associated tablos parameters'; diff --git a/ui/src/components/CreateEventModal.tsx b/ui/src/components/CreateEventModal.tsx new file mode 100644 index 0000000..c002935 --- /dev/null +++ b/ui/src/components/CreateEventModal.tsx @@ -0,0 +1,184 @@ +import { Event, EventInsert } from "@ui/types/events.types"; +import { useState } from "react"; +import { useTablosList } from "@ui/hooks/tablos"; +import { useCreateEvent } from "@ui/hooks/events"; +import { useUser } from "@ui/providers/UserStoreProvider"; + +interface EventModalProps { + date: Date; + onClose: () => void; +} + +export const CreateEventModal = ({ date, onClose }: EventModalProps) => { + const user = useUser(); + const { data: tablos, isLoading: tablosLoading } = useTablosList(); + const createEvent = useCreateEvent(); + const [createdEvent, setCreatedEvent] = useState({ + start_date: date?.toISOString().split("T")[0] || "", + start_time: date?.toISOString().split("T")[1] || "", + tablo_id: "", + title: "", + created_by: user.id, + }); + + return ( +
+
+ {/* Header with colored accent */} +
+
+

Nouvel événement

+ +
+
+ {date.toLocaleDateString("fr-FR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })} +
+
+ + {/* Form Content */} +
+ {/* Title Input */} +
+ + setCreatedEvent({ + ...createdEvent, + title: e.target.value, + } as Event) + } + className="w-full text-lg font-medium border-none outline-none bg-transparent text-gray-900 dark:text-white placeholder-gray-400 focus:ring-0 px-0" + placeholder="Ajouter un titre" + autoFocus + /> +
+
+ + {/* Tablo Selection */} +
+ + +
+ + {/* Time Selection */} +
+
+ + + setCreatedEvent({ + ...createdEvent, + start_time: e.target.value, + } as Event) + } + className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white transition-all" + /> +
+
+ + + setCreatedEvent({ + ...createdEvent, + end_time: e.target.value, + } as Event) + } + className="w-full px-3 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800 dark:text-white transition-all" + /> +
+
+ + {/* Description */} +
+ +