From 74b920d395dbfd550aa783a1e8779171c4c4e6e1 Mon Sep 17 00:00:00 2001 From: ull0sm Date: Wed, 20 May 2026 20:19:04 +0530 Subject: [PATCH] feat: add consolidated database schema with tables for profiles, dojos, students, events, and entries --- supabase/consolidated_db.sql | 764 +++++++++++++++++++++++++++++++++++ 1 file changed, 764 insertions(+) create mode 100644 supabase/consolidated_db.sql diff --git a/supabase/consolidated_db.sql b/supabase/consolidated_db.sql new file mode 100644 index 0000000..597046b --- /dev/null +++ b/supabase/consolidated_db.sql @@ -0,0 +1,764 @@ +-- ============================================================================ +-- EntryDesk - Consolidated Database Schema +-- This file contains all database migrations consolidated into one schema +-- ============================================================================ + +-- ============================================================================ +-- 1. Base Schema (from db.sql) +-- ============================================================================ + +-- Enable Row Level Security +-- alter table auth.users enable row level security; + +-- Enums +create type user_role as enum ('organizer', 'coach', 'admin'); + +create type event_type as enum ('tournament', 'seminar', 'test'); + +create type event_level as enum ('district', 'state', 'national', 'international'); + +create type entry_status as enum ('draft', 'submitted', 'approved', 'rejected'); + +-- Profiles (extends Supabase Auth) +create table profiles ( + id uuid references auth.users on delete cascade primary key, + email text not null, + role user_role default 'coach', + full_name text, + created_at timestamptz default now() +); + +alter table profiles enable row level security; + +-- Dojos (Managed by Coaches) +create table dojos ( + id uuid default gen_random_uuid () primary key, + coach_id uuid references profiles (id) not null, + name text not null, + created_at timestamptz default now() +); + +alter table dojos enable row level security; + +-- Students (Belong to a Dojos) +create table students ( + id uuid default gen_random_uuid () primary key, + dojo_id uuid references dojos (id) on delete cascade, + name text not null, + gender text not null, -- 'male', 'female' + date_of_birth date, + weight numeric, -- in kg + rank text, -- 'white', 'yellow', 'brown_3', etc. + created_at timestamptz default now() +); + +alter table students enable row level security; + +-- Events (Managed by Organizers) +create table events ( + id uuid default gen_random_uuid () primary key, + organizer_id uuid references profiles (id) not null, + title text not null, + description text, + event_type event_type not null, + event_level event_level, + start_date date not null, + end_date date not null, + location text, + is_public boolean default false, + created_at timestamptz default now() +); + +alter table events enable row level security; + +-- Event Days (For multi-day events) +create table event_days ( + id uuid default gen_random_uuid () primary key, + event_id uuid references events (id) on delete cascade, + date date not null, + name text -- e.g., "Day 1 - Kata" +); + +alter table event_days enable row level security; + +-- Categories (For Tournaments) +create table categories ( + id uuid default gen_random_uuid () primary key, + event_id uuid references events (id) on delete cascade, + name text not null, -- e.g., "Boys 10-12yrs White-Green" + gender text, -- 'male', 'female', 'mixed' + min_age int, + max_age int, + min_weight numeric, + max_weight numeric, + min_rank text, -- simple text for matching or int if we formalize ranks + max_rank text +); + +alter table categories enable row level security; + +-- Coach Requests (Permission to join event) +create table event_applications ( + id uuid default gen_random_uuid () primary key, + event_id uuid references events (id) on delete cascade, + coach_id uuid references profiles (id) on delete cascade, + status text default 'pending', -- pending, approved, rejected + created_at timestamptz default now(), + unique (event_id, coach_id) +); + +alter table event_applications enable row level security; + +-- Entries (The core participation record) +create table entries ( + id uuid default gen_random_uuid () primary key, + event_id uuid references events (id) on delete cascade, + coach_id uuid references profiles (id) not null, + student_id uuid references students (id) not null, + category_id uuid references categories (id), -- Nullable for Seminars + event_day_id uuid references event_days (id), + participation_type text, -- 'kata', 'kumite', 'both' + status entry_status default 'draft', + created_at timestamptz default now() +); + +alter table entries enable row level security; + +-- POLICIES -- + +-- Profiles: +-- Public read for now (or restricted to auth users) +create policy "Public profiles are viewable by everyone" on profiles for +select using (true); +-- Users can insert their own profile +create policy "Users can insert their own profile" on profiles for +insert +with + check ( + auth.uid () = id + and role = 'coach' + ); +-- Users can update own profile +create policy "Users can update own profile" on profiles for +update using (auth.uid () = id) +with + check ( + auth.uid () = id + and role = ( + select role + from profiles + where + id = auth.uid () + ) + ); + +-- Dojos: +-- Coaches can view/edit their own dojos. +create policy "Coaches view own dojos" on dojos for +select using ( + auth.uid () = coach_id + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('coach', 'admin') + ) + ); + +create policy "Coaches insert own dojos" on dojos for +insert +with + check ( + auth.uid () = coach_id + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('coach', 'admin') + ) + ); + +create policy "Coaches update own dojos" on dojos for +update using ( + auth.uid () = coach_id + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('coach', 'admin') + ) +); + +create policy "Coaches delete own dojos" on dojos for delete using ( + auth.uid () = coach_id + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('coach', 'admin') + ) +); + +-- Students: +-- Coaches can manage students in their dojos. +-- Need a join check or simple helper if possible, but standard RLS relies on direct relationship or subquery. +create policy "Coaches manage their students" on students using ( + exists ( + select 1 + from dojos + where + dojos.id = students.dojo_id + and dojos.coach_id = auth.uid () + ) + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('coach', 'admin') + ) +); + +-- Events: +-- Public events are viewable by everyone. +create policy "Public events are viewable by everyone" on events for +select using (is_public = true); +-- Organizers can manage their own events. +create policy "Organizers manage own events" on events using ( + auth.uid () = organizer_id + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('organizer', 'admin') + ) +) +with + check ( + auth.uid () = organizer_id + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('organizer', 'admin') + ) + ); + +-- Categories/EventDays: +-- Viewable by everyone (if event is visible/public). +create policy "View categories" on categories for +select using (true); +-- Organizers manage. +create policy "Organizers manage categories" on categories using ( + exists ( + select 1 + from events + where + events.id = categories.event_id + and events.organizer_id = auth.uid () + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('organizer', 'admin') + ) + ) +); + +create policy "View event days" on event_days for +select using (true); + +create policy "Organizers manage event days" on event_days using ( + exists ( + select 1 + from events + where + events.id = event_days.event_id + and events.organizer_id = auth.uid () + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('organizer', 'admin') + ) + ) +); + +-- Entries: +-- Coaches can view/create entries for their students. +create policy "Coaches manage own entries" on entries using ( + auth.uid () = coach_id + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('coach', 'admin') + ) +); +-- Organizers can view entries for their events. +create policy "Organizers view entries for their events" on entries for +select using ( + exists ( + select 1 + from events + where + events.id = entries.event_id + and events.organizer_id = auth.uid () + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('organizer', 'admin') + ) + ) + ); +-- Organizers can update status of entries. +create policy "Organizers update entries" on entries for +update using ( + exists ( + select 1 + from events + where + events.id = entries.event_id + and events.organizer_id = auth.uid () + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('organizer', 'admin') + ) + ) +); + +-- Event Applications: +-- Coaches can create applications. +create policy "Coaches apply" on event_applications for +insert +with + check ( + auth.uid () = coach_id + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('coach', 'admin') + ) + ); + +create policy "Coaches view own applications" on event_applications for +select using ( + auth.uid () = coach_id + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('coach', 'admin') + ) + ); +-- Organizers manage applications. +create policy "Organizers manage applications" on event_applications using ( + exists ( + select 1 + from events + where + events.id = event_applications.event_id + and events.organizer_id = auth.uid () + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('organizer', 'admin') + ) + ) +); + +-- Views +-- Create a specific view for Organizers to see all entry details for their events +create or replace view organizer_entries_view as +select e.id as entry_id, e.event_id, e.status, e.participation_type, e.created_at, e.coach_id, e.event_day_id, e.category_id, e.student_id, + +-- Student info +s.name as student_name, +s.rank as student_rank, +s.gender as student_gender, +s.weight as student_weight, +s.date_of_birth as student_dob, + +-- Dojo info +d.name as dojo_name, + +-- Category info +c.name as category_name, + +-- Day info +ed.name as event_day_name, ed.date as event_day_date, + +-- Coach info +p.full_name as coach_name, p.email as coach_email, + +-- Event Organizer ID for filtering +ev.organizer_id +from + entries e + join students s on e.student_id = s.id + left join dojos d on s.dojo_id = d.id + left join categories c on e.category_id = c.id + left join event_days ed on e.event_day_id = ed.id + join profiles p on e.coach_id = p.id + join events ev on e.event_id = ev.id +where + ev.organizer_id = auth.uid () + and exists ( + select 1 + from profiles + where + id = auth.uid () + and role in ('organizer', 'admin') + ); + +grant select on organizer_entries_view to authenticated; + +-- ============================================================================ +-- 2. Update-only migration: role-based RLS and organizer view hardening (from migration.sql) +-- ============================================================================ + +-- DROP POLICY IF EXISTS "Public profiles are viewable by everyone" ON profiles; +-- DROP POLICY IF EXISTS "Users can insert their own profile" ON profiles; +-- (Policies already created above in base schema) + +-- View update (already created above with chest_no and registration_no) + +-- ============================================================================ +-- 3. Prevent accidental duplicate event creation (from prevent_duplicate_events.sql) +-- ============================================================================ + +CREATE UNIQUE INDEX IF NOT EXISTS events_dedupe_unique_idx ON events ( + organizer_id, + lower(title), + event_type, + start_date, + end_date, + lower(coalesce(location, '')) +); + +-- ============================================================================ +-- 4. Add student registration IDs and entry chest numbers (from 20240304_registration_and_chest_no.sql) +-- ============================================================================ + +-- 1. Student Registration IDs +CREATE SEQUENCE IF NOT EXISTS student_reg_seq; + +ALTER TABLE public.students +ADD COLUMN IF NOT EXISTS registration_no TEXT UNIQUE; + +CREATE OR REPLACE FUNCTION generate_student_registration_no() +RETURNS TRIGGER AS $$ +DECLARE + year_prefix TEXT; + next_val INTEGER; +BEGIN + year_prefix := 'SK' || to_char(now(), 'YY'); + SELECT nextval('student_reg_seq') INTO next_val; + NEW.registration_no := year_prefix || '-' || LPAD(next_val::TEXT, 4, '0'); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS tr_generate_student_registration_no ON public.students; +CREATE TRIGGER tr_generate_student_registration_no +BEFORE INSERT ON public.students +FOR EACH ROW +EXECUTE FUNCTION generate_student_registration_no(); + +-- 2. Entry Chest Numbers +ALTER TABLE public.entries +ADD COLUMN IF NOT EXISTS chest_no INTEGER; + +CREATE OR REPLACE FUNCTION assign_chest_no_on_approval() +RETURNS TRIGGER AS $$ +DECLARE + next_chest_no INTEGER; +BEGIN + -- Only assign chest_no if status is changing to 'approved' and chest_no is not already set + IF (NEW.status = 'approved' AND (OLD.status IS NULL OR OLD.status != 'approved') AND NEW.chest_no IS NULL) THEN + SELECT COALESCE(MAX(chest_no), 0) + 1 + INTO next_chest_no + FROM public.entries + WHERE event_id = NEW.event_id; + + NEW.chest_no := next_chest_no; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS tr_assign_chest_no_on_approval ON public.entries; +CREATE TRIGGER tr_assign_chest_no_on_approval +BEFORE UPDATE ON public.entries +FOR EACH ROW +EXECUTE FUNCTION assign_chest_no_on_approval(); + +-- ============================================================================ +-- 5. Registration open/close toggle (from registration open close toggle.sql) +-- ============================================================================ + +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS is_registration_open BOOLEAN DEFAULT true; + +-- ============================================================================ +-- 6. Registration close date (from 20260309_registration_close_date.sql) +-- ============================================================================ + +ALTER TABLE public.events +ADD COLUMN IF NOT EXISTS registration_close_date DATE; + +-- ============================================================================ +-- 7. Temporary registration (from 20260310234831_temporary_registration.sql) +-- ============================================================================ + +ALTER TABLE public.events +ADD COLUMN IF NOT EXISTS temporary_registration_closes_at TIMESTAMPTZ; + +-- ============================================================================ +-- 8. Event level support (from 20260312_event_level.sql) +-- ============================================================================ + +-- Note: event_level enum already created in base schema +-- This just adds the column if not exists + +comment on column public.events.event_level is 'Competition scope such as district, state, national, or international.'; + +-- ============================================================================ +-- 9. Create contacts table (from 20260508_create_contacts_table.sql) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS contacts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + email TEXT NOT NULL, + message TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create index on email for faster lookups +CREATE INDEX IF NOT EXISTS idx_contacts_email ON contacts(email); + +-- Create index on created_at for sorting +CREATE INDEX IF NOT EXISTS idx_contacts_created_at ON contacts(created_at DESC); + +-- Enable RLS (Row Level Security) +ALTER TABLE contacts ENABLE ROW LEVEL SECURITY; + +-- Create policy to allow anonymous inserts (for contact form) +CREATE POLICY "Allow anonymous inserts to contacts" ON contacts + FOR INSERT + WITH CHECK (true); + +-- Create policy to allow authenticated users to read all contacts +CREATE POLICY "Allow authenticated users to read contacts" ON contacts + FOR SELECT + USING (auth.role() = 'authenticated'); + +-- ============================================================================ +-- 10. Student inactivity tracking (from 20260518230000_student_inactivity.sql) +-- ============================================================================ + +-- Add is_active column +ALTER TABLE students ADD COLUMN IF NOT EXISTS is_active boolean DEFAULT true; + +-- Function to evaluate activity for all students in a dojo +CREATE OR REPLACE FUNCTION evaluate_dojo_students_activity(target_dojo_id uuid) +RETURNS void AS $$ +DECLARE + recent_events_count int; +BEGIN + -- Check how many distinct events this dojo has participated in (up to 2 most recent) + SELECT COUNT(*) INTO recent_events_count + FROM ( + SELECT ev.id + FROM events ev + JOIN entries e ON e.event_id = ev.id + JOIN students s ON e.student_id = s.id + WHERE s.dojo_id = target_dojo_id + GROUP BY ev.id, ev.end_date + ORDER BY ev.end_date DESC + LIMIT 2 + ) as subquery; + + IF recent_events_count >= 2 THEN + -- Evaluate students in this dojo + -- A student is inactive if they have 0 entries in the 2 most recent events of the dojo + UPDATE students st + SET is_active = ( + CASE + -- If the student was created AFTER the start date of the older of the 2 events, + -- they haven't had the chance to miss 2 events yet, so keep them active. + WHEN st.created_at > ( + SELECT MIN(start_date)::timestamptz + FROM ( + SELECT ev.start_date + FROM events ev + JOIN entries e2 ON e2.event_id = ev.id + JOIN students s2 ON e2.student_id = s2.id + WHERE s2.dojo_id = target_dojo_id + GROUP BY ev.id, ev.end_date, ev.start_date + ORDER BY ev.end_date DESC + LIMIT 2 + ) AS recent_two + ) THEN true + -- Otherwise, they must have attended at least 1 of the 2 recent events to remain active. + ELSE + EXISTS ( + SELECT 1 + FROM entries my_e + WHERE my_e.student_id = st.id + AND my_e.event_id IN ( + SELECT ev.id + FROM events ev + JOIN entries e2 ON e2.event_id = ev.id + JOIN students s2 ON e2.student_id = s2.id + WHERE s2.dojo_id = target_dojo_id + GROUP BY ev.id, ev.end_date + ORDER BY ev.end_date DESC + LIMIT 2 + ) + ) + END + ) + WHERE st.dojo_id = target_dojo_id; + ELSE + -- If dojo hasn't participated in 2 events, everyone is active + UPDATE students + SET is_active = true + WHERE dojo_id = target_dojo_id; + END IF; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Trigger function that extracts the dojo_id from the entry's student and calls the evaluation +CREATE OR REPLACE FUNCTION trigger_evaluate_dojo_students_activity() +RETURNS trigger AS $$ +DECLARE + dojo_uuid uuid; +BEGIN + IF TG_OP = 'DELETE' THEN + SELECT dojo_id INTO dojo_uuid FROM students WHERE id = OLD.student_id; + ELSE + SELECT dojo_id INTO dojo_uuid FROM students WHERE id = NEW.student_id; + END IF; + + IF dojo_uuid IS NOT NULL THEN + PERFORM evaluate_dojo_students_activity(dojo_uuid); + END IF; + + IF TG_OP = 'DELETE' THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Drop trigger if exists to prevent errors on multiple runs +DROP TRIGGER IF EXISTS evaluate_activity_on_entry ON entries; + +-- Create the trigger on entries +CREATE TRIGGER evaluate_activity_on_entry +AFTER INSERT OR UPDATE OR DELETE ON entries +FOR EACH ROW +EXECUTE FUNCTION trigger_evaluate_dojo_students_activity(); + +-- ============================================================================ +-- 11. Move generic_checked from students to entries (from 20260520110000_move_generic_checked.sql) +-- ============================================================================ + +-- Add generic_checked to entries table +ALTER TABLE entries ADD COLUMN IF NOT EXISTS generic_checked boolean NOT NULL DEFAULT false; + +-- ============================================================================ +-- 12. Update organizer_entries_view to include all new columns +-- ============================================================================ + +DROP VIEW IF EXISTS organizer_entries_view CASCADE; + +CREATE OR REPLACE VIEW organizer_entries_view AS +SELECT + e.id AS entry_id, + e.event_id, + e.status, + e.participation_type, + e.created_at, + e.coach_id, + e.event_day_id, + e.category_id, + e.student_id, + e.chest_no, + e.generic_checked, + + -- Student info + s.name AS student_name, + s.rank AS student_rank, + s.gender AS student_gender, + s.weight AS student_weight, + s.date_of_birth AS student_dob, + s.registration_no AS student_registration_no, + s.is_active AS student_is_active, + + -- Dojo info + d.name AS dojo_name, + + -- Category info + c.name AS category_name, + + -- Day info + ed.name AS event_day_name, + ed.date AS event_day_date, + + -- Coach info + p.full_name AS coach_name, + p.email AS coach_email, + + -- Event info + ev.organizer_id, + ev.is_registration_open, + ev.registration_close_date, + ev.temporary_registration_closes_at, + ev.event_level + +FROM + entries e + JOIN students s ON e.student_id = s.id + LEFT JOIN dojos d ON s.dojo_id = d.id + LEFT JOIN categories c ON e.category_id = c.id + LEFT JOIN event_days ed ON e.event_day_id = ed.id + JOIN profiles p ON e.coach_id = p.id + JOIN events ev ON e.event_id = ev.id +WHERE + ev.organizer_id = auth.uid () + AND EXISTS ( + SELECT 1 + FROM profiles + WHERE + id = auth.uid () + AND role IN ('organizer', 'admin') + ); + +GRANT SELECT ON organizer_entries_view TO authenticated; + +-- ============================================================================ +-- End of consolidated database schema +-- ============================================================================