Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.
People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.
Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.
Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.
Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
88 lines
3.5 KiB
SQL
88 lines
3.5 KiB
SQL
-- name: CreatePerson :one
|
|
INSERT INTO core.persons (user_id, display_name, primary_email, primary_email_verified)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING *;
|
|
|
|
-- name: EnsureSystemPerson :exec
|
|
-- Idempotently insert the reserved system person that owns the System tenant
|
|
-- (keyed on the unique user_id). Read back with GetPersonByUserID.
|
|
INSERT INTO core.persons (user_id, display_name, primary_email)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (user_id) DO NOTHING;
|
|
|
|
-- name: GetPersonByUserID :one
|
|
SELECT * FROM core.persons
|
|
WHERE user_id = $1;
|
|
|
|
-- name: GetPersonByID :one
|
|
SELECT * FROM core.persons
|
|
WHERE person_id = $1;
|
|
|
|
-- name: UpdatePerson :one
|
|
UPDATE core.persons
|
|
SET display_name = $1, primary_email = $2, primary_email_verified = $3
|
|
WHERE person_id = $4
|
|
RETURNING *;
|
|
|
|
-- name: ListPersons :many
|
|
SELECT * FROM core.persons
|
|
ORDER BY display_name;
|
|
|
|
-- name: LookupPersons :many
|
|
-- Resolves a lookup term to one or more active persons for the operator
|
|
-- landing-surface lookup affordance. Exact email match wins (deterministic
|
|
-- — operators using email expect precision); otherwise falls through to
|
|
-- case-insensitive substring match against display_name. Status='active'
|
|
-- filters out removed accounts. LIMIT 20 caps the disambiguation list
|
|
-- without paging.
|
|
SELECT person_id, display_name, primary_email, status
|
|
FROM core.persons
|
|
WHERE status = 'active'
|
|
AND (primary_email = $1 OR display_name ILIKE '%' || $1 || '%')
|
|
ORDER BY
|
|
CASE WHEN primary_email = $1 THEN 0 ELSE 1 END,
|
|
display_name
|
|
LIMIT 20;
|
|
|
|
-- name: CountActivePersons :one
|
|
-- Operator overview tile: how many people the deployment currently serves.
|
|
-- Mirrors LookupPersons' status='active' filter so the headline number and
|
|
-- the lookup affordance above it are counting the same population.
|
|
SELECT COUNT(*) FROM core.persons
|
|
WHERE status = 'active';
|
|
|
|
-- name: CountPersonsJoinedLast30Days :one
|
|
-- The People tile's caption: velocity rather than a restatement of the
|
|
-- census. Same status filter as CountActivePersons so both numbers
|
|
-- describe the same population.
|
|
SELECT COUNT(*) FROM core.persons
|
|
WHERE status = 'active' AND created_at > now() - interval '30 days';
|
|
|
|
-- name: ListPersonsPage :many
|
|
-- People directory operator surface (operator-list-scale UX-4 /
|
|
-- operator-people-directory D6): the paged, searched query feeding
|
|
-- GET /operator/persons. Ordered newest-joined first so "who joined this
|
|
-- week?" is answered by page one.
|
|
--
|
|
-- sqlc.narg(q): NULL matches every row; set, a case-insensitive substring
|
|
-- match against display_name or primary_email (operator-list-scale:
|
|
-- "carries a scoped server-side search" -- "people: display name and
|
|
-- email").
|
|
-- sqlc.narg(exclude_person_id): the reserved system person (maintainer
|
|
-- decision 2026-08-23: infrastructure, never "joined", excluded from the
|
|
-- directory and its totals; it stays visible as the System tenant's owner
|
|
-- in the organizations context). NULL excludes nothing.
|
|
-- count(*) OVER() carries the true total for the filtered set alongside
|
|
-- the page's rows (design D2), so the stated total never reflects only
|
|
-- the page.
|
|
SELECT person_id, user_id, display_name, primary_email, primary_email_verified, status, created_at,
|
|
count(*) OVER() AS total_count
|
|
FROM core.persons
|
|
WHERE (sqlc.narg(exclude_person_id)::uuid IS NULL
|
|
OR person_id <> sqlc.narg(exclude_person_id)::uuid)
|
|
AND (sqlc.narg(q)::text IS NULL
|
|
OR display_name ILIKE '%' || sqlc.narg(q)::text || '%'
|
|
OR primary_email ILIKE '%' || sqlc.narg(q)::text || '%')
|
|
ORDER BY created_at DESC
|
|
LIMIT sqlc.arg(page_limit)::int OFFSET sqlc.arg(page_offset)::int;
|