UI/UX Overhaul: Sort, Search, Name Resolution, Pagination

On this page

Status

All 4 phases complete. Phase 1: MR !12 (issue #23). Phase 2: MR !13 (issue #26). Phase 3: MR !16 (issue #28). Phase 4: MR !17 (issue #29).

Context

CRAIG’s 19 list views have three usability problems that make them non-functional for real caseworkers:

  • Truncated UUIDs — 6 list views show uuid[..8]…​ instead of human-readable names (referrals, investigations, placements, agreements, transactions, ICPC)

  • No sort or search — all 19 list views are server-paginated with hardcoded ORDER BY created_at DESC and no text search

  • Primitive pagination — only Prev/Next links, no page numbers

The code-quality-review refactor (complete) consolidated PageResponse<T>, fetch_page(), total_pages(), and DEFAULT_PER_PAGE into routes/mod.rs. CRUD completion (complete) added 31 new endpoints including list_adjustments_paged on craig-financial. No new standalone list views were added to craig-web, but the payment detail page has a TODO to wire up the adjustments list endpoint (services/craig-web/src/routes/financial.rs:212).

This plan builds on those foundations with 4 phases across 4 feature branches.

Phase 1: Table CSS + JS Infrastructure

Branch: feature/ui-table-infrastructure

Scope: CSS and template changes only. Zero API/backend changes.

Changes

services/craig-web/static/css/components.css — append after .data-table block:

  • .th—​sortable — cursor pointer, right-padding for arrow indicator

  • .th—​sortable::after — neutral up-down arrow

  • .th—​sortable.th—​asc::after — up triangle, dark-green

  • .th—​sortable.th—​desc::after — down triangle, dark-green

  • .table-toolbar — flex container: filter tabs left, search bar right

  • .table-toolbar__search — 320px max, relative positioned for search icon

  • .pagination-bar — flex centered, gap-4, replaces existing prev/next pattern

  • .pagination-bar__page — 28px square page number links

  • .pagination-bar__page—​active — dark-green background, white text

  • .pagination-bar__page—​disabled — muted, no pointer events

  • .pagination-bar__ellipsis — ellipsis between page ranges

  • .pagination-bar__info — "N total" text, muted

services/craig-web/src/routes/mod.rs — add extra_params() helper for preserving query params across pagination links.

services/craig-web/templates/cases/list.html — proof-of-concept: replace prev/next with .pagination-bar numbered pagination using page windowing (first, last, +/-2 around current, ellipsis).

Evaluate whether Askama {% include "_pagination.html" %} works with the required template variables in scope. If yes, create shared partial. If not, replicate the pattern per template.

Files

File Change

services/craig-web/static/css/components.css

Add sortable, search, pagination CSS

services/craig-web/src/routes/mod.rs

Add extra_params()

services/craig-web/templates/cases/list.html

New pagination proof-of-concept

tests/e2e/specs/pagination.spec.ts

Verify numbered pagination

Documentation

File Update

docs/modules/ROOT/pages/developer-guide.adoc

Document pagination CSS classes

CHANGELOG.adoc

Entry under == Unreleased

Branch: feature/ui-sort-search

Scope: Backend store/API + BFF passthrough + all 19 list templates.

Backend Pattern

All list_*paged and count* store functions gain search, sort_by, sort_dir params.

Sort safety — dynamic ORDER BY via validated whitelist (never user input in SQL):

fn validated_sort_column(sort_by: Option<&str>) -> &str {
    match sort_by {
        Some("case_number") => "case_number",
        Some("status") => "status",
        Some("admin_unit") => "admin_unit",
        Some("assigned_worker") => "assigned_worker",
        Some("opened_at") => "opened_at",
        _ => "created_at",
    }
}

fn validated_sort_dir(sort_dir: Option<&str>) -> &str {
    match sort_dir {
        Some("asc") | Some("ASC") => "ASC",
        _ => "DESC",
    }
}

Query built with format!() using validated values — search via ILIKE:

let sql = format!(
    "SELECT * FROM cases
     WHERE ($1::TEXT IS NULL OR assigned_worker = $1)
       AND ($2::TEXT IS NULL OR status = $2)
       AND ($3::TEXT IS NULL
            OR case_number ILIKE '%' || $3 || '%'
            OR assigned_worker ILIKE '%' || $3 || '%'
            OR admin_unit ILIKE '%' || $3 || '%')
     ORDER BY {col} {dir}
     LIMIT $4 OFFSET $5"
);

Per-Service Store Changes

Service Store Files Sortable Columns Searchable Columns

craig-cases

cases.rs, referrals.rs, investigations.rs

case_number, status, admin_unit, worker, opened_at

case_number, worker, admin_unit

craig-placement

placements.rs, foster_homes.rs

type, status, started_at; name, admin_unit, license_status

type, status; name, address, admin_unit

craig-exchange

partners.rs, agreements.rs, transactions.rs, icpc.rs

partner_name, type; title, status; direction, type; direction, status

partner_name; title; type; sending_state, receiving_state

craig-financial

payments.rs, rates.rs, claims.rs, adjustments.rs

status, period; jurisdiction, type; period, status; status, created_at

period; jurisdiction; period; reason

craig-reporting

afcars.rs, ncands.rs, issues.rs

status, created_at; severity, source_service

status; severity, source_service

craig-security

audit.rs, nist.rs, reviews.rs

timestamp; control_id, family; review_type

action, actor; control_id, family; review_type

craig-rules

rulesets.rs

name, jurisdiction

name, jurisdiction

adjustments.rs has list_adjustments_paged + count_adjustments (added by CRUD completion). These need sort/search params added like all other paginated list functions.

API Handler Changes

Each list handler’s query struct gains optional search, sort_by, sort_dir (Option<String>). Pass .as_deref() to store. No RBAC changes.

BFF Route Handler Changes

Each BFF params struct gains search, sort_by, sort_dir. Appended to backend API URL. Passed to template.

Also: Wire up the adjustments list on the payment detail page (services/craig-web/src/routes/financial.rs:212 — currently returns empty Vec with a TODO comment). Call GET /v1/financial/adjustments?payment_id={id}.

Template Changes (all 19 list views)

Search bar in .table-toolbar, sortable headers that toggle direction on click, .pagination-bar from Phase 1 carrying search/sort/filter in extra_params.

Testing

  • Unit: validated_sort_column/validated_sort_dir return correct values; unknown input falls back to default

  • Integration: Sort param returns ordered results; search filters correctly; invalid sort_by still returns results

  • E2E: New tests/e2e/specs/sort-search.spec.ts

Files

Area Files

Backend (7 services)

src/store/.rs (list + count), src/api/.rs (query structs + handlers)

craig-web

All 10 route handler files, all 19 list templates

E2E

tests/e2e/specs/sort-search.spec.ts (new)

Documentation

File Update

docs/modules/ROOT/pages/implementation-guide.adoc

Document sort/search query params pattern

docs/modules/ROOT/pages/developer-guide.adoc

Add "Adding sort/search to a new list endpoint" guide

.claude/docs/services.md

Note sort/search support on list endpoints

CHANGELOG.adoc

Entry under == Unreleased

Phase 3: Name Resolution (Eliminate Truncated UUIDs)

Branch: feature/ui-name-resolution

Scope: Same-service JOINs + cross-service batch-lookup + BFF enrichment.

UUID Display Inventory

List View Current Strategy Target Display

Referrals

r.id[..8]…​

Format: admin_unit + date

"Floyd 2024-06-15"

Investigations

inv.id[..8]…​

Format: area + due date

"Bartow 2024-07-01"

Placements (case)

p.case_id[..8]…​

Cross-service batch-lookup

Case number

Placements (child)

p.child_id[..8]…​

Cross-service batch-lookup

Child name

Agreements

a.partner_id[..8]…​

Same-DB JOIN

Partner name

Transactions

t.partner_id[..8]…​

Same-DB JOIN

Partner name

ICPC

r.case_id[..8]…​

Cross-service batch-lookup

Case number

3A: Same-Service JOINs (craig-exchange)

Agreements and Transactions: AgreementWithPartner / TransactionWithPartner with partner_name via JOIN exchange_partners. List handlers return enriched structs; single-record endpoints unchanged.

3B: Batch-Lookup Endpoint (craig-cases)

POST /v1/cases/batch-lookup — RBAC: caseworker_or_above

Accepts { case_ids: [Uuid], person_ids: [Uuid] } (max 500 each), returns { cases: { uuid: case_number }, persons: { uuid: { first_name, last_name } } }.

Input validation: reject if either array > 500 (400 Bad Request).

3C: BFF Enrichment

Enrichment flow (placement list handler):

  1. Fetch PageResponse<PlacementView> from craig-placement

  2. Collect unique case_id and child_id from the page

  3. POST /v1/cases/batch-lookup with collected IDs

  4. Merge resolved names into view model (case_display, child_display)

  5. Render template

Same pattern for ICPC (case_id only).

3D: Referral/Investigation Format-Based Display

Template-only, no API changes. Referrals: admin_unit + received_date. Investigations: area + due_date.

3E: Template Updates with Fallback

Templates to update: placements.html, agreements.html, transactions.html, icpc.html, referrals.html, worklist.html

Testing

  • Unit (craig-cases): batch_lookup_cases returns correct results; empty input → empty

  • Integration (craig-cases): POST /v1/cases/batch-lookup with known + unknown IDs

  • Integration (craig-exchange): GET /v1/exchange/agreements response includes partner_name

  • E2E: Placements show case numbers; agreements show partner names; referrals show "Area Date" format

Files

Area Files

craig-cases

src/store/{cases,persons}.rs, src/api/cases.rs, src/api/mod.rs

craig-exchange

src/store/{agreements,transactions}.rs, src/api/{agreements,transactions}.rs

craig-web

src/routes/{mod,placement,exchange}.rs, src/routes/intake/{referrals,worklist}.rs

Templates

placement/placements.html, exchange/{agreements,transactions,icpc}.html, intake/{referrals,worklist}.html

test-lib

src/clients/cases.rs (batch_lookup method)

Tests

services/craig-cases/tests/api/ (batch-lookup), E2E specs

Documentation

File Update

docs/modules/ROOT/pages/implementation-guide.adoc

Document batch-lookup pattern, BFF enrichment flow

.claude/docs/services.md

Add batch-lookup endpoint to craig-cases section

CHANGELOG.adoc

Entry under == Unreleased

Phase 4: Polish & Documentation

Branch: feature/ui-polish

Column Resize (Alpine.js + localStorage)

services/craig-web/static/js/table-resize.js (new, ~60 lines): Alpine.js tableResize(tableId) data component. Restores widths from localStorage on init, persists on drag.

services/craig-web/static/css/components.css — .col-resize-handle (4px, cursor: col-resize, highlight on hover).

services/craig-web/templates/base.html — add script include.

Apply Pagination to Remaining 18 List Views

Phase 1 only updates cases/list.html. This phase rolls out .pagination-bar to all remaining templates.

E2E Test Updates

  • New tests/e2e/specs/sort-search.spec.ts — sort click, search filtering, no UUID fragments

  • Update pagination.spec.ts — verify .pagination-bar

  • Update exchange.spec.ts, placement.spec.ts, intake.spec.ts — verify names

Documentation (last MR — updates CLAUDE.md stats)

File Update

This plan file

Final status update — mark all phases complete

docs/modules/ROOT/pages/implementation-guide.adoc

Table CSS classes, column resize, BFF enrichment pattern

.claude/docs/services.md

Verify all endpoints documented

docs/modules/ROOT/pages/developer-guide.adoc

Add "Adding a new list view" guide

.claude/CLAUDE.md

Update Phase 10 stats (last MR only per multi-MR rule)

CHANGELOG.adoc

Entry under == Unreleased

Implementation Order

  1. Phase 1 — CSS + pagination (lowest risk, pure frontend)

  2. Phase 2 — Sort & search (additive optional params, touches all services)

  3. Phase 3 — Name resolution (new batch-lookup API + BFF enrichment)

  4. Phase 4 — Polish (column resize, docs, remaining pagination rollout)

Phases 2 and 3 are independent and could run in parallel.

Verification (per phase)

  1. cargo fmt --check --all && cargo clippy --workspace --locked — -D warnings

  2. cargo nextest run --workspace --lib

  3. cargo xtask dev reload

  4. cargo nextest run --workspace --locked

  5. cargo xtask e2e

GitLab

  • Epic: "UI/UX Overhaul — Sort, Search, Name Resolution, Pagination" (labels: feat, P2-medium)

  • Issue 1: feat: Add table CSS infrastructure and numbered pagination (weight: 2, labels: feat, craig-web)

  • Issue 2: feat: Add server-side sort and search to all list endpoints (weight: 8, labels: feat, P1-high)

  • Issue 3: feat: Resolve truncated UUIDs with name resolution (weight: 5, labels: feat, P1-high)

  • Issue 4: feat: Column resize, pagination rollout, and documentation (weight: 3, labels: feat, docs)

Each MR follows the Delivery Protocol. After all 4 MRs merge, run the Plan Completion Audit and close the epic.

Edit this page · latest