-- Portal roles, league masters, team extras, support issues CREATE TYPE portal_role AS ENUM ('league_master', 'manager'); ALTER TABLE profiles ADD COLUMN IF NOT EXISTS portal_role portal_role NOT NULL DEFAULT 'manager'; ALTER TABLE teams ADD COLUMN IF NOT EXISTS nickname TEXT, ADD COLUMN IF NOT EXISTS icon TEXT NOT NULL DEFAULT 'shield'; CREATE TABLE IF NOT EXISTS league_masters ( league_id UUID NOT NULL REFERENCES leagues(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, assigned_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (league_id, user_id) ); CREATE TABLE IF NOT EXISTS support_issues ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), league_id UUID NOT NULL REFERENCES leagues(id) ON DELETE CASCADE, competition_id UUID REFERENCES competitions(id) ON DELETE SET NULL, submitted_by UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, subject TEXT NOT NULL, body TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'in_progress', 'resolved')), master_reply TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TRIGGER support_issues_updated_at BEFORE UPDATE ON support_issues FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- Helpers CREATE OR REPLACE FUNCTION is_global_league_master() RETURNS BOOLEAN AS $$ SELECT EXISTS ( SELECT 1 FROM profiles WHERE id = auth.uid() AND portal_role = 'league_master' ); $$ LANGUAGE sql SECURITY DEFINER STABLE; CREATE OR REPLACE FUNCTION is_league_master(p_league_id UUID) RETURNS BOOLEAN AS $$ SELECT is_global_league_master() OR EXISTS (SELECT 1 FROM leagues WHERE id = p_league_id AND created_by = auth.uid()) OR EXISTS ( SELECT 1 FROM league_masters WHERE league_id = p_league_id AND user_id = auth.uid() ); $$ LANGUAGE sql SECURITY DEFINER STABLE; -- Extend organizer check to include assigned masters CREATE OR REPLACE FUNCTION is_league_organizer(p_league_id UUID) RETURNS BOOLEAN AS $$ SELECT is_league_master(p_league_id); $$ LANGUAGE sql SECURITY DEFINER STABLE; -- Profile trigger: portal role from signup metadata CREATE OR REPLACE FUNCTION handle_new_user() RETURNS TRIGGER AS $$ DECLARE v_role portal_role; BEGIN BEGIN v_role := COALESCE( (NEW.raw_user_meta_data->>'portal_role')::portal_role, 'manager'::portal_role ); EXCEPTION WHEN OTHERS THEN v_role := 'manager'; END; INSERT INTO public.profiles (id, display_name, portal_role) VALUES ( NEW.id, COALESCE(NEW.raw_user_meta_data->>'display_name', split_part(NEW.email, '@', 1)), v_role ) ON CONFLICT (id) DO UPDATE SET display_name = COALESCE(EXCLUDED.display_name, profiles.display_name), portal_role = COALESCE(EXCLUDED.portal_role, profiles.portal_role); RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- RLS new tables ALTER TABLE league_masters ENABLE ROW LEVEL SECURITY; ALTER TABLE support_issues ENABLE ROW LEVEL SECURITY; CREATE POLICY league_masters_select ON league_masters FOR SELECT TO authenticated USING (is_league_master(league_id) OR user_id = auth.uid()); CREATE POLICY league_masters_insert ON league_masters FOR INSERT TO authenticated WITH CHECK (is_league_master(league_id)); CREATE POLICY league_masters_delete ON league_masters FOR DELETE TO authenticated USING (is_league_master(league_id)); CREATE POLICY support_issues_select ON support_issues FOR SELECT TO authenticated USING ( submitted_by = auth.uid() OR is_league_master(league_id) ); CREATE POLICY support_issues_insert ON support_issues FOR INSERT TO authenticated WITH CHECK (submitted_by = auth.uid()); CREATE POLICY support_issues_update ON support_issues FOR UPDATE TO authenticated USING (is_league_master(league_id)); -- League delete (masters only) CREATE POLICY leagues_delete ON leagues FOR DELETE TO authenticated USING (is_league_master(id)); -- Team delete in draft competitions CREATE POLICY teams_delete ON teams FOR DELETE TO authenticated USING ( EXISTS ( SELECT 1 FROM competitions c WHERE c.id = competition_id AND c.status = 'draft' AND is_league_master(c.league_id) ) ); -- Recreate standings view with nickname/icon (must DROP first: REPLACE cannot reorder columns) DROP VIEW IF EXISTS competition_standings; CREATE VIEW competition_standings AS SELECT t.competition_id, t.id AS team_id, t.name AS team_name, t.nickname AS team_nickname, t.icon AS team_icon, t.logo_path, COUNT(m.id) FILTER (WHERE m.status = 'completed') AS played, COUNT(m.id) FILTER ( WHERE m.status = 'completed' AND ( (m.home_team_id = t.id AND m.home_score > m.away_score) OR (m.away_team_id = t.id AND m.away_score > m.home_score) ) ) AS won, COUNT(m.id) FILTER ( WHERE m.status = 'completed' AND m.home_score = m.away_score ) AS drawn, COUNT(m.id) FILTER ( WHERE m.status = 'completed' AND ( (m.home_team_id = t.id AND m.home_score < m.away_score) OR (m.away_team_id = t.id AND m.away_score < m.home_score) ) ) AS lost, COALESCE(SUM( CASE WHEN m.status = 'completed' AND m.home_team_id = t.id THEN m.home_score WHEN m.status = 'completed' AND m.away_team_id = t.id THEN m.away_score ELSE 0 END ), 0)::INT AS goals_for, COALESCE(SUM( CASE WHEN m.status = 'completed' AND m.home_team_id = t.id THEN m.away_score WHEN m.status = 'completed' AND m.away_team_id = t.id THEN m.home_score ELSE 0 END ), 0)::INT AS goals_against, COALESCE(SUM( CASE WHEN m.status = 'completed' AND m.home_team_id = t.id THEN m.home_score - m.away_score WHEN m.status = 'completed' AND m.away_team_id = t.id THEN m.away_score - m.home_score ELSE 0 END ), 0)::INT AS goal_difference, COALESCE(SUM( CASE WHEN m.status <> 'completed' THEN 0 WHEN m.home_score = m.away_score THEN 1 WHEN m.home_team_id = t.id AND m.home_score > m.away_score THEN 3 WHEN m.away_team_id = t.id AND m.away_score > m.home_score THEN 3 ELSE 0 END ), 0)::INT AS points FROM teams t LEFT JOIN matches m ON m.competition_id = t.competition_id AND (m.home_team_id = t.id OR m.away_team_id = t.id) GROUP BY t.competition_id, t.id, t.name, t.nickname, t.icon, t.logo_path; CREATE OR REPLACE FUNCTION get_user_id_by_email(p_email TEXT) RETURNS UUID AS $$ SELECT id FROM auth.users WHERE email = lower(trim(p_email)) LIMIT 1; $$ LANGUAGE sql SECURITY DEFINER STABLE;