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 DESCand 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.
Phase 2: Server-Side Sort & Search
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 |
|
case_number, status, admin_unit, worker, opened_at |
case_number, worker, admin_unit |
craig-placement |
|
type, status, started_at; name, admin_unit, license_status |
type, status; name, address, admin_unit |
craig-exchange |
|
partner_name, type; title, status; direction, type; direction, status |
partner_name; title; type; sending_state, receiving_state |
craig-financial |
|
status, period; jurisdiction, type; period, status; status, created_at |
period; jurisdiction; period; reason |
craig-reporting |
|
status, created_at; severity, source_service |
status; severity, source_service |
craig-security |
|
timestamp; control_id, family; review_type |
action, actor; control_id, family; review_type |
craig-rules |
|
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_dirreturn 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) |
|
craig-web |
All 10 route handler files, all 19 list templates |
E2E |
|
Documentation
| File | Update |
|---|---|
|
Document sort/search query params pattern |
|
Add "Adding sort/search to a new list endpoint" guide |
|
Note sort/search support on list endpoints |
|
Entry under |
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 |
|
Format: admin_unit + date |
"Floyd 2024-06-15" |
Investigations |
|
Format: area + due date |
"Bartow 2024-07-01" |
Placements (case) |
|
Cross-service batch-lookup |
Case number |
Placements (child) |
|
Cross-service batch-lookup |
Child name |
Agreements |
|
Same-DB JOIN |
Partner name |
Transactions |
|
Same-DB JOIN |
Partner name |
ICPC |
|
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):
-
Fetch
PageResponse<PlacementView>from craig-placement -
Collect unique
case_idandchild_idfrom the page -
POST /v1/cases/batch-lookupwith collected IDs -
Merge resolved names into view model (
case_display,child_display) -
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_casesreturns correct results; empty input → empty -
Integration (craig-cases):
POST /v1/cases/batch-lookupwith known + unknown IDs -
Integration (craig-exchange):
GET /v1/exchange/agreementsresponse includespartner_name -
E2E: Placements show case numbers; agreements show partner names; referrals show "Area Date" format
Files
| Area | Files |
|---|---|
craig-cases |
|
craig-exchange |
|
craig-web |
|
Templates |
|
test-lib |
|
Tests |
|
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 |
|
Table CSS classes, column resize, BFF enrichment pattern |
|
Verify all endpoints documented |
|
Add "Adding a new list view" guide |
|
Update Phase 10 stats (last MR only per multi-MR rule) |
|
Entry under |
Implementation Order
-
Phase 1 — CSS + pagination (lowest risk, pure frontend)
-
Phase 2 — Sort & search (additive optional params, touches all services)
-
Phase 3 — Name resolution (new batch-lookup API + BFF enrichment)
-
Phase 4 — Polish (column resize, docs, remaining pagination rollout)
Phases 2 and 3 are independent and could run in parallel.
Verification (per phase)
-
cargo fmt --check --all && cargo clippy --workspace --locked — -D warnings -
cargo nextest run --workspace --lib -
cargo xtask dev reload -
cargo nextest run --workspace --locked -
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.