CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS hunt_area ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, description text, geom geometry(MultiPolygon, 4326), created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS app_user ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), display_name text NOT NULL, email text, avatar_url text, color text, is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz ); CREATE TABLE IF NOT EXISTS user_identity ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, provider text NOT NULL, provider_subject text NOT NULL, email text, email_verified boolean, display_name text, avatar_url text, first_login_at timestamptz NOT NULL DEFAULT now(), last_login_at timestamptz NOT NULL DEFAULT now(), UNIQUE (provider, provider_subject), CHECK (provider IN ('google', 'microsoft', 'apple')) ); CREATE TABLE IF NOT EXISTS hunt ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), hunt_area_id uuid NOT NULL REFERENCES hunt_area(id), title text NOT NULL, description text, starts_at timestamptz, ends_at timestamptz, status text NOT NULL DEFAULT 'planned', created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS map_point_type ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL UNIQUE, icon text, color text ); INSERT INTO map_point_type (name, icon, color) VALUES ('HuntingTower', 'tower', '#F59E0B'), ('FeedingPlace', 'corn', '#84CC16'), ('Parking', 'parking', '#3B82F6'), ('MeetingPoint', 'flag', '#22C55E'), ('Observation', 'eye', '#A855F7') ON CONFLICT (name) DO NOTHING; CREATE TABLE IF NOT EXISTS map_point ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), hunt_area_id uuid NOT NULL REFERENCES hunt_area(id), type_id uuid NOT NULL REFERENCES map_point_type(id), name text NOT NULL, description text, geom geometry(Point, 4326) NOT NULL, image_url text, is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS map_zone ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), hunt_area_id uuid NOT NULL REFERENCES hunt_area(id), name text NOT NULL, zone_type text NOT NULL, description text, geom geometry(MultiPolygon, 4326) NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS hunt_area_member ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), hunt_area_id uuid NOT NULL REFERENCES hunt_area(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, role text NOT NULL DEFAULT 'member', created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (hunt_area_id, user_id) ); CREATE TABLE IF NOT EXISTS hunt_participant ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), hunt_id uuid NOT NULL REFERENCES hunt(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES app_user(id), role text NOT NULL DEFAULT 'hunter', status text NOT NULL DEFAULT 'invited', share_location boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (hunt_id, user_id) ); CREATE TABLE IF NOT EXISTS hunt_assignment ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), hunt_id uuid NOT NULL REFERENCES hunt(id) ON DELETE CASCADE, participant_id uuid NOT NULL REFERENCES hunt_participant(id) ON DELETE CASCADE, map_point_id uuid NOT NULL REFERENCES map_point(id), note text, assigned_at timestamptz NOT NULL DEFAULT now(), UNIQUE (hunt_id, participant_id) ); CREATE TABLE IF NOT EXISTS user_current_location ( participant_id uuid PRIMARY KEY REFERENCES hunt_participant(id) ON DELETE CASCADE, geom geometry(Point, 4326) NOT NULL, accuracy_m numeric(8,2), speed_mps numeric(8,2), heading_degrees numeric(6,2), battery_percent int, recorded_at timestamptz NOT NULL, received_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS user_location_history ( id bigserial PRIMARY KEY, participant_id uuid NOT NULL REFERENCES hunt_participant(id) ON DELETE CASCADE, geom geometry(Point, 4326) NOT NULL, accuracy_m numeric(8,2), speed_mps numeric(8,2), heading_degrees numeric(6,2), battery_percent int, recorded_at timestamptz NOT NULL, received_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS hunt_message ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), hunt_id uuid NOT NULL REFERENCES hunt(id) ON DELETE CASCADE, sender_participant_id uuid REFERENCES hunt_participant(id), message text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_hunt_area_geom ON hunt_area USING gist (geom); CREATE INDEX IF NOT EXISTS idx_map_point_geom ON map_point USING gist (geom); CREATE INDEX IF NOT EXISTS idx_map_zone_geom ON map_zone USING gist (geom); CREATE INDEX IF NOT EXISTS idx_user_current_location_geom ON user_current_location USING gist (geom); CREATE INDEX IF NOT EXISTS idx_user_location_history_geom ON user_location_history USING gist (geom); CREATE INDEX IF NOT EXISTS idx_user_location_history_participant_time ON user_location_history (participant_id, recorded_at DESC); CREATE INDEX IF NOT EXISTS idx_hunt_participant_hunt ON hunt_participant (hunt_id); CREATE INDEX IF NOT EXISTS idx_hunt_assignment_hunt ON hunt_assignment (hunt_id);