| |
| |
|
|
| |
| CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS users ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| email TEXT UNIQUE NOT NULL, |
| hashed_password TEXT NOT NULL, |
| account_type TEXT NOT NULL CHECK (account_type IN ('team', 'personal', 'coach')), |
| full_name TEXT, |
| avatar_url TEXT, |
| phone TEXT, |
| organization_id UUID, |
| staff_role TEXT, |
| created_at TIMESTAMPTZ DEFAULT NOW(), |
| updated_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| |
| CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS organizations ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| name TEXT NOT NULL, |
| description TEXT, |
| logo_url TEXT, |
| primary_color TEXT DEFAULT '#FF5733', |
| secondary_color TEXT DEFAULT '#333333', |
| jersey_style TEXT DEFAULT 'Solid', |
| home_court TEXT, |
| website TEXT, |
| phone TEXT, |
| email TEXT, |
| twitter_handle TEXT, |
| instagram_handle TEXT, |
| competition_settings JSONB DEFAULT '{}', |
| roster_settings JSONB DEFAULT '{}', |
| owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, |
| created_at TIMESTAMPTZ DEFAULT NOW(), |
| updated_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_organizations_owner ON organizations(owner_id); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS players ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| name TEXT NOT NULL, |
| jersey_number INTEGER, |
| position TEXT, |
| height_cm REAL, |
| weight_kg REAL, |
| date_of_birth DATE, |
| avatar_url TEXT, |
| phone TEXT, |
| address TEXT, |
| experience_years TEXT, |
| bio TEXT, |
| status TEXT DEFAULT 'active', |
| organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL, |
| user_id UUID REFERENCES users(id) ON DELETE SET NULL, |
| created_at TIMESTAMPTZ DEFAULT NOW(), |
| updated_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_players_org ON players(organization_id); |
| CREATE INDEX IF NOT EXISTS idx_players_user ON players(user_id); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS videos ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| uploader_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, |
| title TEXT, |
| description TEXT, |
| analysis_mode TEXT NOT NULL CHECK (analysis_mode IN ('team', 'personal')), |
| status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), |
| storage_path TEXT NOT NULL, |
| duration_seconds REAL, |
| frame_count INTEGER, |
| fps REAL, |
| width INTEGER, |
| height INTEGER, |
| file_size_bytes BIGINT, |
| organization_id UUID REFERENCES organizations(id) ON DELETE SET NULL, |
| error_message TEXT, |
| progress_percent REAL, |
| current_step TEXT, |
| annotated_url TEXT, |
| created_at TIMESTAMPTZ DEFAULT NOW(), |
| updated_at TIMESTAMPTZ DEFAULT NOW(), |
| completed_at TIMESTAMPTZ |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_videos_uploader ON videos(uploader_id); |
| CREATE INDEX IF NOT EXISTS idx_videos_status ON videos(status); |
| CREATE INDEX IF NOT EXISTS idx_videos_org ON videos(organization_id); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS detections ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| video_id UUID NOT NULL REFERENCES videos(id) ON DELETE CASCADE, |
| frame INTEGER NOT NULL, |
| object_type TEXT NOT NULL CHECK (object_type IN ('player', 'ball')), |
| track_id INTEGER NOT NULL, |
| bbox REAL[] NOT NULL, |
| confidence REAL NOT NULL, |
| keypoints JSONB, |
| team_id INTEGER, |
| has_ball BOOLEAN DEFAULT FALSE, |
| tactical_x REAL, |
| tactical_y REAL, |
| created_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_detections_video ON detections(video_id); |
| CREATE INDEX IF NOT EXISTS idx_detections_frame ON detections(video_id, frame); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS analysis_results ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| video_id UUID NOT NULL REFERENCES videos(id) ON DELETE CASCADE, |
| total_frames INTEGER NOT NULL, |
| duration_seconds REAL NOT NULL, |
| fps REAL DEFAULT 30.0, |
| players_detected INTEGER, |
| |
| |
| team_1_possession_percent REAL, |
| team_2_possession_percent REAL, |
| total_passes INTEGER, |
| total_interceptions INTEGER, |
| defensive_actions INTEGER, |
| |
| |
| shot_attempts INTEGER, |
| overall_shooting_percentage REAL, |
| shot_form_consistency REAL, |
| dribble_count INTEGER, |
| dribble_frequency_per_minute REAL, |
| total_distance_meters REAL, |
| avg_speed_kmh REAL, |
| max_speed_kmh REAL, |
| acceleration_events INTEGER, |
| avg_knee_bend_angle REAL, |
| avg_elbow_angle_shooting REAL, |
| training_load_score REAL, |
| |
| |
| events JSONB DEFAULT '[]', |
| |
| processing_time_seconds REAL, |
| created_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_analysis_video ON analysis_results(video_id); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS analytics ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| player_id UUID NOT NULL REFERENCES players(id) ON DELETE CASCADE, |
| video_id UUID REFERENCES videos(id) ON DELETE SET NULL, |
| metric_type TEXT NOT NULL, |
| value REAL NOT NULL, |
| timestamp TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_analytics_player ON analytics(player_id); |
| CREATE INDEX IF NOT EXISTS idx_analytics_type ON analytics(player_id, metric_type); |
| CREATE INDEX IF NOT EXISTS idx_analytics_video ON analytics(video_id); |
|
|
| |
| |
| |
|
|
| |
| ALTER TABLE users ENABLE ROW LEVEL SECURITY; |
| ALTER TABLE organizations ENABLE ROW LEVEL SECURITY; |
| ALTER TABLE players ENABLE ROW LEVEL SECURITY; |
| ALTER TABLE videos ENABLE ROW LEVEL SECURITY; |
| ALTER TABLE detections ENABLE ROW LEVEL SECURITY; |
| ALTER TABLE analysis_results ENABLE ROW LEVEL SECURITY; |
| ALTER TABLE analytics ENABLE ROW LEVEL SECURITY; |
|
|
| |
| DROP POLICY IF EXISTS "Users can view own profile" ON users; |
| DROP POLICY IF EXISTS "Users can update own profile" ON users; |
| DROP POLICY IF EXISTS "Enable insert for registration" ON users; |
| DROP POLICY IF EXISTS "Users can manage own profile" ON users; |
| CREATE POLICY "Users can manage own profile" ON users |
| FOR ALL USING (true) WITH CHECK (true); |
|
|
| |
| DROP POLICY IF EXISTS "Owners can manage organizations" ON organizations; |
| CREATE POLICY "Owners can manage organizations" ON organizations |
| FOR ALL USING (true) WITH CHECK (true); |
|
|
| |
| DROP POLICY IF EXISTS "Uploaders can manage videos" ON videos; |
| CREATE POLICY "Uploaders can manage videos" ON videos |
| FOR ALL USING (true) WITH CHECK (true); |
|
|
| |
| DROP POLICY IF EXISTS "Users can manage their players" ON players; |
| CREATE POLICY "Users can manage their players" ON players |
| FOR ALL USING (true) WITH CHECK (true); |
|
|
| |
| DROP POLICY IF EXISTS "Users can view own detections" ON detections; |
| CREATE POLICY "Users can manage detections" ON detections |
| FOR ALL USING (true) WITH CHECK (true); |
|
|
| |
| DROP POLICY IF EXISTS "Users can view own analysis" ON analysis_results; |
| CREATE POLICY "Users can manage analysis" ON analysis_results |
| FOR ALL USING (true) WITH CHECK (true); |
|
|
| |
| DROP POLICY IF EXISTS "Users can view own analytics" ON analytics; |
| CREATE POLICY "Users can manage analytics" ON analytics |
| FOR ALL USING (true) WITH CHECK (true); |
|
|
| |
| |
| |
|
|
| |
| CREATE OR REPLACE FUNCTION update_updated_at() |
| RETURNS TRIGGER AS $$ |
| BEGIN |
| NEW.updated_at = NOW(); |
| RETURN NEW; |
| END; |
| $$ LANGUAGE plpgsql |
| SET search_path = ''; |
|
|
| |
| DROP TRIGGER IF EXISTS update_users_updated_at ON users; |
| CREATE TRIGGER update_users_updated_at |
| BEFORE UPDATE ON users |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); |
|
|
| DROP TRIGGER IF EXISTS update_organizations_updated_at ON organizations; |
| CREATE TRIGGER update_organizations_updated_at |
| BEFORE UPDATE ON organizations |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); |
|
|
| DROP TRIGGER IF EXISTS update_players_updated_at ON players; |
| CREATE TRIGGER update_players_updated_at |
| BEFORE UPDATE ON players |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); |
|
|
| DROP TRIGGER IF EXISTS update_videos_updated_at ON videos; |
| CREATE TRIGGER update_videos_updated_at |
| BEFORE UPDATE ON videos |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS schedules ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE, |
| title TEXT NOT NULL, |
| start_time TIMESTAMPTZ NOT NULL, |
| end_time TIMESTAMPTZ NOT NULL, |
| type TEXT NOT NULL, |
| location TEXT, |
| description TEXT, |
| created_by UUID REFERENCES users(id) ON DELETE SET NULL, |
| created_at TIMESTAMPTZ DEFAULT NOW(), |
| updated_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| |
| CREATE TABLE IF NOT EXISTS announcements ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, |
| author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, |
| title TEXT NOT NULL, |
| content TEXT NOT NULL, |
| created_at TIMESTAMPTZ DEFAULT NOW(), |
| updated_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
| CREATE INDEX IF NOT EXISTS idx_schedules_org ON schedules(organization_id); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS matches ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, |
| opponent TEXT NOT NULL, |
| date TIMESTAMPTZ NOT NULL, |
| location TEXT, |
| result TEXT, |
| score_us INTEGER, |
| score_them INTEGER, |
| notes TEXT, |
| created_at TIMESTAMPTZ DEFAULT NOW(), |
| updated_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_matches_org ON matches(organization_id); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS notifications ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| recipient_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, |
| title TEXT NOT NULL, |
| message TEXT NOT NULL, |
| type TEXT DEFAULT 'info', |
| read BOOLEAN DEFAULT FALSE, |
| action_link TEXT, |
| created_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_id); |
|
|
| |
| |
| |
|
|
| ALTER TABLE schedules ENABLE ROW LEVEL SECURITY; |
| ALTER TABLE matches ENABLE ROW LEVEL SECURITY; |
| ALTER TABLE notifications ENABLE ROW LEVEL SECURITY; |
| ALTER TABLE announcements ENABLE ROW LEVEL SECURITY; |
|
|
| |
| DROP POLICY IF EXISTS "Users can view schedules" ON schedules; |
| CREATE POLICY "Users can view schedules" ON schedules |
| FOR SELECT USING ( |
| organization_id IN ( |
| SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text |
| UNION |
| SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text |
| UNION |
| SELECT organization_id FROM players WHERE user_id::text = (SELECT auth.uid())::text |
| ) |
| ); |
|
|
| DROP POLICY IF EXISTS "Owners and Staff can manage schedules" ON schedules; |
| CREATE POLICY "Owners and Staff can manage schedules" ON schedules |
| FOR ALL WITH CHECK ( |
| organization_id IN ( |
| SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text |
| UNION |
| SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text AND (account_type = 'team' OR account_type = 'coach') |
| ) |
| ); |
|
|
| |
| DROP POLICY IF EXISTS "Users can view matches" ON matches; |
| CREATE POLICY "Users can view matches" ON matches |
| FOR SELECT USING ( |
| organization_id IN ( |
| SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text |
| UNION |
| SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text |
| UNION |
| SELECT organization_id FROM players WHERE user_id::text = (SELECT auth.uid())::text |
| ) |
| ); |
|
|
| DROP POLICY IF EXISTS "Owners and Staff can manage matches" ON matches; |
| CREATE POLICY "Owners and Staff can manage matches" ON matches |
| FOR ALL WITH CHECK ( |
| organization_id IN ( |
| SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text |
| UNION |
| SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text AND (account_type = 'team' OR account_type = 'coach') |
| ) |
| ); |
|
|
| |
| DROP POLICY IF EXISTS "Users can view announcements" ON announcements; |
| CREATE POLICY "Users can view announcements" ON announcements |
| FOR SELECT USING ( |
| organization_id IN ( |
| SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text |
| UNION |
| SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text |
| UNION |
| SELECT organization_id FROM players WHERE user_id::text = (SELECT auth.uid())::text |
| ) |
| ); |
|
|
| DROP POLICY IF EXISTS "Owners and Staff can manage announcements" ON announcements; |
| CREATE POLICY "Owners and Staff can manage announcements" ON announcements |
| FOR ALL WITH CHECK ( |
| organization_id IN ( |
| SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text |
| UNION |
| SELECT organization_id FROM users WHERE id::text = (SELECT auth.uid())::text AND (account_type = 'team' OR account_type = 'coach') |
| ) |
| ); |
|
|
| |
| DROP POLICY IF EXISTS "Users can manage own notifications" ON notifications; |
| CREATE POLICY "Users can manage own notifications" ON notifications |
| FOR ALL USING (recipient_id::text = (SELECT auth.uid())::text); |
| |
| DROP TRIGGER IF EXISTS update_schedules_updated_at ON schedules; |
| CREATE TRIGGER update_schedules_updated_at |
| BEFORE UPDATE ON schedules |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); |
|
|
| DROP TRIGGER IF EXISTS update_matches_updated_at ON matches; |
| CREATE TRIGGER update_matches_updated_at |
| BEFORE UPDATE ON matches |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); |
|
|
| |
| |
| |
| CREATE TABLE IF NOT EXISTS activities ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| player_id UUID REFERENCES players(id) ON DELETE CASCADE, |
| type TEXT NOT NULL, |
| description TEXT, |
| date TIMESTAMPTZ DEFAULT NOW(), |
| metadata JSONB, |
| created_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_activities_player ON activities(player_id); |
|
|
| ALTER TABLE activities ENABLE ROW LEVEL SECURITY; |
|
|
| |
| DROP POLICY IF EXISTS "Users can view activities" ON activities; |
| CREATE POLICY "Users can view activities" ON activities |
| FOR SELECT USING ( |
| player_id IN ( |
| SELECT id FROM players WHERE user_id::text = (SELECT auth.uid())::text |
| OR organization_id IN ( |
| SELECT id FROM organizations WHERE owner_id::text = (SELECT auth.uid())::text |
| ) |
| ) |
| ); |
|
|
| DROP POLICY IF EXISTS "Players can manage own activities" ON activities; |
| CREATE POLICY "Players can manage own activities" ON activities |
| FOR ALL WITH CHECK ( |
| player_id IN ( |
| SELECT id FROM players WHERE user_id::text = (SELECT auth.uid())::text |
| ) |
| ); |
| |
| |
| |
| CREATE TABLE IF NOT EXISTS personal_analyses ( |
| id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), |
| job_id UUID UNIQUE NOT NULL, |
| user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, |
| status TEXT NOT NULL, |
| results_json JSONB, |
| created_at TIMESTAMPTZ DEFAULT NOW(), |
| updated_at TIMESTAMPTZ DEFAULT NOW() |
| ); |
|
|
| CREATE INDEX IF NOT EXISTS idx_personal_analyses_user ON personal_analyses(user_id); |
| CREATE INDEX IF NOT EXISTS idx_personal_analyses_job ON personal_analyses(job_id); |
|
|
| ALTER TABLE personal_analyses ENABLE ROW LEVEL SECURITY; |
|
|
| DROP POLICY IF EXISTS "Users can manage own personal analyses" ON personal_analyses; |
| CREATE POLICY "Users can manage own personal analyses" ON personal_analyses |
| FOR ALL USING ((SELECT auth.uid())::text = user_id::text); |
|
|
| |
| DROP TRIGGER IF EXISTS update_personal_analyses_updated_at ON personal_analyses; |
| CREATE TRIGGER update_personal_analyses_updated_at |
| BEFORE UPDATE ON personal_analyses |
| FOR EACH ROW EXECUTE FUNCTION update_updated_at(); |
|
|
| |
| |
| |
|
|
| |
| INSERT INTO storage.buckets (id, name, public) |
| VALUES ('team-analysis-videos', 'team-analysis-videos', true) |
| ON CONFLICT (id) DO NOTHING; |
|
|
| |
| |
| DROP POLICY IF EXISTS "Public Access Team Videos" ON storage.objects; |
| DROP POLICY IF EXISTS "Allow Uploads Team Videos" ON storage.objects; |
| DROP POLICY IF EXISTS "Allow Updates Team Videos" ON storage.objects; |
| DROP POLICY IF EXISTS "Allow Deletes Team Videos" ON storage.objects; |
|
|
| |
| CREATE POLICY "Public Access Team Videos" ON storage.objects |
| FOR SELECT |
| USING (bucket_id = 'team-analysis-videos'); |
|
|
| |
| CREATE POLICY "Allow Uploads Team Videos" ON storage.objects |
| FOR INSERT |
| WITH CHECK (bucket_id = 'team-analysis-videos'); |
|
|
| |
| CREATE POLICY "Allow Updates Team Videos" ON storage.objects |
| FOR UPDATE |
| USING (bucket_id = 'team-analysis-videos'); |
|
|
| |
| CREATE POLICY "Allow Deletes Team Videos" ON storage.objects |
| FOR DELETE |
| USING (bucket_id = 'team-analysis-videos'); |
|
|
| |
|
|
| |
| |
| |
| |
| ALTER TABLE videos ADD COLUMN IF NOT EXISTS annotated_url TEXT; |
|
|