-- Operations channel data model (no service-days table): -- schedules -> trips -> trip_stops -> stop_updates create table if not exists public.operations_schedules ( id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'), channel_id text not null references public.channels(id) on delete cascade, version integer not null check (version > 0), source_file_name text not null, source_mime text, storage_path text, file_sha256 text, parser text not null default 'unknown', parse_status text not null default 'pending', parse_error text, uploaded_by uuid not null references auth.users(id) on delete restrict, uploaded_at timestamptz not null default now(), parsed_at timestamptz, is_active boolean not null default false, unique (channel_id, version) ); create unique index if not exists idx_operations_schedules_active_per_channel on public.operations_schedules(channel_id) where is_active = true; create index if not exists idx_operations_schedules_channel_uploaded on public.operations_schedules(channel_id, uploaded_at desc); create table if not exists public.operations_trips ( id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'), schedule_id text not null references public.operations_schedules(id) on delete cascade, trip_number text not null, duty_number text, running_number text, direction text, service_code text, sort_order integer not null default 0, created_at timestamptz not null default now() ); create index if not exists idx_operations_trips_schedule_sort on public.operations_trips(schedule_id, sort_order, trip_number); create table if not exists public.operations_trip_stops ( id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'), trip_id text not null references public.operations_trips(id) on delete cascade, stop_sequence integer not null check (stop_sequence > 0), stop_name text not null, stop_code text, scheduled_time text, is_timing_point boolean not null default false, raw_label text, created_at timestamptz not null default now(), unique (trip_id, stop_sequence) ); create index if not exists idx_operations_trip_stops_trip_sequence on public.operations_trip_stops(trip_id, stop_sequence); create index if not exists idx_operations_trip_stops_trip_time on public.operations_trip_stops(trip_id, scheduled_time); create table if not exists public.operations_stop_updates ( id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'), trip_stop_id text not null references public.operations_trip_stops(id) on delete cascade, status text, actual_time text, vehicle_id text, notes text, updated_by uuid not null references auth.users(id) on delete restrict, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique (trip_stop_id) ); create index if not exists idx_operations_stop_updates_trip_stop on public.operations_stop_updates(trip_stop_id); alter table public.operations_schedules enable row level security; alter table public.operations_trips enable row level security; alter table public.operations_trip_stops enable row level security; alter table public.operations_stop_updates enable row level security; drop policy if exists "operations_schedules_select_visible" on public.operations_schedules; create policy "operations_schedules_select_visible" on public.operations_schedules for select to authenticated using (public.can_access_channel(channel_id)); drop policy if exists "operations_schedules_insert_admins" on public.operations_schedules; create policy "operations_schedules_insert_admins" on public.operations_schedules for insert to authenticated with check ( uploaded_by = auth.uid() and exists ( select 1 from public.channels c where c.id = channel_id and c.type = 'operations' and public.org_role(c.organization_id) in ('owner', 'admin') ) ); drop policy if exists "operations_schedules_update_admins" on public.operations_schedules; create policy "operations_schedules_update_admins" on public.operations_schedules for update to authenticated using ( exists ( select 1 from public.channels c where c.id = channel_id and c.type = 'operations' and public.org_role(c.organization_id) in ('owner', 'admin') ) ) with check ( exists ( select 1 from public.channels c where c.id = channel_id and c.type = 'operations' and public.org_role(c.organization_id) in ('owner', 'admin') ) ); drop policy if exists "operations_schedules_delete_admins" on public.operations_schedules; create policy "operations_schedules_delete_admins" on public.operations_schedules for delete to authenticated using ( exists ( select 1 from public.channels c where c.id = channel_id and c.type = 'operations' and public.org_role(c.organization_id) in ('owner', 'admin') ) ); drop policy if exists "operations_trips_select_visible" on public.operations_trips; create policy "operations_trips_select_visible" on public.operations_trips for select to authenticated using ( exists ( select 1 from public.operations_schedules s where s.id = schedule_id and public.can_access_channel(s.channel_id) ) ); drop policy if exists "operations_trips_write_admins" on public.operations_trips; create policy "operations_trips_write_admins" on public.operations_trips for all to authenticated using ( exists ( select 1 from public.operations_schedules s join public.channels c on c.id = s.channel_id where s.id = schedule_id and c.type = 'operations' and public.org_role(c.organization_id) in ('owner', 'admin') ) ) with check ( exists ( select 1 from public.operations_schedules s join public.channels c on c.id = s.channel_id where s.id = schedule_id and c.type = 'operations' and public.org_role(c.organization_id) in ('owner', 'admin') ) ); drop policy if exists "operations_trip_stops_select_visible" on public.operations_trip_stops; create policy "operations_trip_stops_select_visible" on public.operations_trip_stops for select to authenticated using ( exists ( select 1 from public.operations_trips t join public.operations_schedules s on s.id = t.schedule_id where t.id = trip_id and public.can_access_channel(s.channel_id) ) ); drop policy if exists "operations_trip_stops_write_admins" on public.operations_trip_stops; create policy "operations_trip_stops_write_admins" on public.operations_trip_stops for all to authenticated using ( exists ( select 1 from public.operations_trips t join public.operations_schedules s on s.id = t.schedule_id join public.channels c on c.id = s.channel_id where t.id = trip_id and c.type = 'operations' and public.org_role(c.organization_id) in ('owner', 'admin') ) ) with check ( exists ( select 1 from public.operations_trips t join public.operations_schedules s on s.id = t.schedule_id join public.channels c on c.id = s.channel_id where t.id = trip_id and c.type = 'operations' and public.org_role(c.organization_id) in ('owner', 'admin') ) ); drop policy if exists "operations_stop_updates_select_visible" on public.operations_stop_updates; create policy "operations_stop_updates_select_visible" on public.operations_stop_updates for select to authenticated using ( exists ( select 1 from public.operations_trip_stops ts join public.operations_trips t on t.id = ts.trip_id join public.operations_schedules s on s.id = t.schedule_id where ts.id = trip_stop_id and public.can_access_channel(s.channel_id) ) ); drop policy if exists "operations_stop_updates_insert_members" on public.operations_stop_updates; create policy "operations_stop_updates_insert_members" on public.operations_stop_updates for insert to authenticated with check ( updated_by = auth.uid() and exists ( select 1 from public.operations_trip_stops ts join public.operations_trips t on t.id = ts.trip_id join public.operations_schedules s on s.id = t.schedule_id where ts.id = trip_stop_id and public.can_access_channel(s.channel_id) ) ); drop policy if exists "operations_stop_updates_update_members" on public.operations_stop_updates; create policy "operations_stop_updates_update_members" on public.operations_stop_updates for update to authenticated using ( exists ( select 1 from public.operations_trip_stops ts join public.operations_trips t on t.id = ts.trip_id join public.operations_schedules s on s.id = t.schedule_id where ts.id = trip_stop_id and public.can_access_channel(s.channel_id) ) ) with check ( updated_by = auth.uid() and exists ( select 1 from public.operations_trip_stops ts join public.operations_trips t on t.id = ts.trip_id join public.operations_schedules s on s.id = t.schedule_id where ts.id = trip_stop_id and public.can_access_channel(s.channel_id) ) ); drop policy if exists "operations_stop_updates_delete_members" on public.operations_stop_updates; create policy "operations_stop_updates_delete_members" on public.operations_stop_updates for delete to authenticated using ( exists ( select 1 from public.operations_trip_stops ts join public.operations_trips t on t.id = ts.trip_id join public.operations_schedules s on s.id = t.schedule_id where ts.id = trip_stop_id and public.can_access_channel(s.channel_id) ) ); -- Keep updated_at fresh on edits. create or replace function public.tg_set_updated_at() returns trigger language plpgsql as $$ begin new.updated_at := now(); return new; end; $$; drop trigger if exists trg_operations_stop_updates_updated_at on public.operations_stop_updates; create trigger trg_operations_stop_updates_updated_at before update on public.operations_stop_updates for each row execute procedure public.tg_set_updated_at(); -- Realtime availability for operations collaboration features. do $$ begin begin alter publication supabase_realtime add table public.operations_schedules; exception when duplicate_object then null; end; begin alter publication supabase_realtime add table public.operations_trips; exception when duplicate_object then null; end; begin alter publication supabase_realtime add table public.operations_trip_stops; exception when duplicate_object then null; end; begin alter publication supabase_realtime add table public.operations_stop_updates; exception when duplicate_object then null; end; end $$;