Roadbound-BRR/supabase/migrations/20260326195500_add_operations_channel_tables.sql

341 lines
11 KiB
PL/PgSQL

-- 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 $$;