Skip to content

Operations screens API

Each operations screen loads with one request: a GET /screens/... route that calls one database function and returns everything the screen shows first. The rule is PRF-010.

Handler: handleOpsScreens in src/lib/api.ts. Client: src/services/opsScreenService.ts. Database: supabase/migrations/20260930210000_operations_are_one_call.sql.

How access works

Every function is SECURITY INVOKER. Each read inside it runs as the signed-in person, through the same row-level security as the separate reads it replaced. Where one of those reads' routes asked for a permission first, the function asks for the same one with auth_user_has_permission():

  • the screen's own list — the call is refused (42501, answered as 403), as the route was;
  • a side list the screen can live without — the section comes back null, and the screen shows it empty, as it did when that route answered 403.

The anonymous key cannot call any of them. The functions write nothing.

The answers are shaped with the same code the separate routes use, so each part of a screen equals what its separate route returned (src/lib/api.opsScreens.test.ts).

A database without these functions answers 501; the client then falls back to the separate routes.

Routes

Route Function Refused without Sections null without
GET /screens/visa visa_list_screen visa.view lookups.visaGroups: visa.groups.view
GET /screens/visa/:id visa_case_screen visa.view —
GET /screens/visa-groups/:id visa_group_screen visa.groups.view —
GET /screens/approvals approvals_screen what booking_page refuses —
GET /screens/airline-blocks airline_blocks_screen inventory.view glAccounts: finance.view; currencies: admin.currency.view; drafts: inventory.create or inventory.edit; bookings: what booking_page refuses
GET /screens/seat-releases seat_releases_screen inventory.view —
GET /screens/inventory-holds inventory_holds_screen inventory.view —
GET /screens/hotels hotels_screen hotels.view b2bHotelOffers: agents.view
GET /screens/tickets/:id ticket_screen tickets.view —

The airline blocks and hotels screens are refused without inventory.view and hotels.view. Their pages already required them; the separate list routes did not check.

GET /screens/visa

Query: q, status (database status, comma-separated), tripType, groupId, cursor, limit (1–200, default 50), lookups=1.

Returns { data, nextCursor, total, totalIsEstimate, stageCounts, lookups? }: data rows and nextCursor as GET /visa?paged=1; total is exact; stageCounts counts every stage over the whole filtered queue, the stage filter left out. With lookups=1: lookups.visaGroups (as GET /visa/groups) and lookups.travelGroups (the active departures, as GET /groups). Sorted newest first; the route takes no sort.

GET /screens/visa/:id, GET /screens/visa-groups/:id, GET /screens/tickets/:id

The same object as GET /visa/:id, GET /visa/groups/:id and GET /tickets/:id. 404 when the person cannot read the row. A ticket's docs is always empty: TicketDocument was dropped in 20260401190000, and the old read always failed.

GET /screens/approvals

Query: the bookings list filters (q, status, financeStatus, opsStatus, groupId, agentId, type, from, to), sort, dir (default asc), cursor, limit, withTotal (default on), withStats (default on).

Returns a page as GET /sales/bookings?paged=1 plus stats, the tiles of GET /sales/bookings/stats over the same filters.

GET /screens/airline-blocks

Query: blockId (optional — adds that block's manifest).

Returns { airlines, blocks, suppliers, offers, groups, bookings, blockPayments, accounts, glAccounts, paidFromAccounts, currencies, drafts, manifest }:

Key As
airlines GET /inventory/airlines
blocks GET /inventory/quota-blocks
suppliers GET /suppliers (active) — without the creditor-ledger check that route runs for each supplier
offers GET /inventory/b2b-flights
groups GET /groups?scope=all
bookings every booking on a departure linked to a block, walked through booking_page (PLT-050): id, groupId, passengerCount, and each passenger's id, flightId, passengerCategory, needsTicket
blockPayments credits and refunds against a block on active suppliers' accounts, as GET /suppliers/:id/transactions
accounts GET /accounts
glAccounts GET /finance/accounts
paidFromAccounts GET /inventory/paid-from-accounts, only without finance.view
currencies GET /admin/currencies
drafts GET /inventory/quota-blocks/drafts
manifest GET /inventory/quota-blocks/:id/manifest

GET /screens/seat-releases, GET /screens/inventory-holds

Query: status (seat releases; all or comma-separated) / state (holds; active by default, or all).

Return { releases, blocks } — the rows as GET /inventory/releases, and the request form's live blocks — and { holds, pick } — the rows as GET /inventory/holds, and the new-hold form's blocks, FITs, hotels, partners and active departures.

GET /screens/hotels

Returns { hotels, groups, assignments, suppliers, accounts, exchangeRates, b2bHotelOffers } as GET /hotels, GET /groups, GET /hotels/assignments, GET /suppliers, GET /accounts, the exchange_rates in force today and GET /b2b-hotel-offers. A hotel's allotment calendar is drawn from this answer.