Worklist Data & Column Config
Audit + reconciliation of how worklists get their data and columns across the canonical data and column-config layers.
Why this doc: before adding per-department customizable columns to
/ipd/work-listand OPD/medical-worklist, we audited what already exists. The capability is already here — but it’s split across two parallel data readers and three non-converged column-config code paths, plus two separate (legitimately-scoped) concerns. This maps who-owns-what and recommends ONE standard for each layer so we don’t add a 4th.Audit date: 2026-06-03. Read alongside
miniapp-modal-override.md,bed-board-ehr-suite.md,ekardex-from-journey-cache.md.
TL;DR — the two standards to converge on
| Layer | Canonical standard | Retire / fold in | Keep separate |
|---|---|---|---|
| Order/encounter DATA | manifest-data-layer (useManifestRow / useManifestList / useManifestMedicalWorklist / useManifestDoctorWorklist) over encounter_journey_cache, realtime |
usePatientBodyData → reposition as a thin Patient-360 adapter, not a parallel source |
useSupabaseQueue (single-dept ops queues), useObservationSync (vitals) |
| Worklist COLUMN config | The LIVE accessor model: ColumnDefinition[] (accessor+type) rendered by ConfigDrivenTable — defaults in workflow_templates.ui_manifest.columns, per-dept/role override in role_worklists.table_config.columns (currently authored but NOT rendered — wire it) |
the dormant selector path (useManifestColumnConfig/toManifestTableRow/workflow_action_configurations); IPD hardcoded columns + useIpdColumnVisibility (localStorage) |
nurse_note_column_configs (nurse-note columns), queue_display_configs (kiosk/TV screens) |
Layer 1 — Order / encounter DATA (“is there a useOrderSync?”)
Yes — the manifest-data-layer is it. web/src/services/manifest-data-layer/ is a unified, realtime,
all-departments reader over encounter_journey_cache.
| Hook | Scope | Source | Realtime | Status |
|---|---|---|---|---|
useManifestRow(encounterId) |
one encounter | encounter_journey_cache |
✅ | live — all 12 depts’ pending/completed_tickets + financial_summary + active_alerts |
useManifestList / useManifestMedicalWorklist / useManifestDoctorWorklist |
list | encounter_journey_cache (batch) |
✅ | live — used by patient-roster/doctor-worklist + electronic-ward-rounding |
useManifestCounts / useManifestSelector |
derived | — | ✅ | per-dept counts + typed field extraction via a selector registry |
useEncounterJourney |
one encounter | encounter_journey_cache |
✅ | lower-level building block manifest sits on |
useSupabaseQueue (+useLabQueue/usePharmacyQueue/…) |
one dept | department_queues |
✅ | operational call-queues — not an encounter aggregator |
useOrderBusSubscription |
event stream | order_bus |
✅ | cross-dept event ticker |
usePatientBodyData |
one encounter | encounter_journey_cache + ipd_admissions_dashboard + observations + bloodRequests(REST) |
✅ | overlaps useManifestRow — built for Patient 360; lighter, body-card shaped |
Verdict: useManifestRow/useManifestList are the canonical order/encounter sync. usePatientBodyData
is a parallel reader of the same cache (it adds ward/bed + blood, which the manifest doesn’t surface yet).
Don’t grow a third reader — either (a) keep usePatientBodyData as the Patient-360-only adapter, or
(b) fold its ward/bed + blood reads into the manifest selectors and make usePatientBodyData a thin
wrapper. Recommended: (b) long-term, (a) for now.
Layer 2 — Worklist COLUMN config (the actual fragmentation)
Three worklist column paths exist and do not share a resolution model:
Path A — accessor model via ConfigDrivenTable → OPD /medical-worklist ✅ LIVE (the only one that renders)
workflow_templates.ui_manifest.columns ─┐
├─ WorkflowConfigProvider.getColumnsForNode ← THE column resolver
role_worklists.table_config.columns ─────┘ (NOT the merger; merger only does nodes/visibility/queueConfig)
(per org+dept+role, normalized override) → WorkflowConfigBasedTabs.activeColumns
→ ConfigDrivenTable.renderCell(col, row)
- Column contract:
ColumnDefinition = { header, header_en, accessor, type, width, isSortable }wheretype ∈ text|esi|time|status|statusPoint|booleanStatus|dateTimedrives a type-specific cell component (ESI chip, MM:SS time color-coding, status dot, …).accessor(e.g."encounter.waitDuration") is resolved bymanifest-bindings.tsagainst aResolvedWorklistRow(=department_queues⊕encounter_journey_cache). - The column resolver is
WorkflowConfigProvider.getColumnsForNode(readsconfig.columns=ui_manifest.columns). Theworkflow-config-merger.tsis a separate path that only merges nodes (visibility +queueConfig+rowActions); itsMergedWorkflowtype has nocolumnsfield. So columns never flowed through the merger — that’s why the earlier “merge role.columnsin the merger” framing was wrong. - ✅ SHIPPED 2026-06-03 (read side):
WorkflowConfigProvidernow overlaysrole_worklists.table_config.columnsover the template columns per node (matched by node id ordata.type), vianormalizeRoleColumns()which accepts BOTH theColumnDefinitionshape AND the legacy worklist-editorWorklistColumn(title/cellType/'90px') shape. Null-gated → when no role columns are authored, behavior is identical to before (zero regression). Unit-tested (12 cases). - ⚠️ Remaining gap (authoring side): the worklist-editor (
/admin/worklist-config→CustomizationModal) still authors legacyaccessors in a REST vocabulary (patientRef.hn,episodeOfCareRef.ep) that the manifest-bindings row doesn’t resolve → those cells render'—'. Headers + cell types render correctly. Follow-up: update the editor to emit manifest accessors (patient.hn,encounter.vn, …) +ColumnType, or add an accessor crosswalk. - Reader:
src/common/components/medical/worklist/index.tsx→WorkflowConfigBasedTabs→ConfigDrivenTable.
Path B — manifest selector model (useManifestColumnConfig) ❌ DORMANT (renders nowhere)
ManifestColumnConfig = { key, header, selectorId, width, sortable, formatterId, path?, adapterField? };selectorId→manifestSelectors[id](EncounterJourneyCache)(a clean pure-function registry) →toManifestTableRowproduces a flat{ key: value }(no type-based cell rendering — weaker than Path A’s typed cells).- Storage:
workflow_action_configurations.configuration.columnConfig(migration20250114_workflow_action_config.sql), viaget_active_workflow_config/save_workflow_configRPCs. - Status: complete but NOT IMPORTED by any rendered table. No component calls
useManifestColumnConfig/toManifestTableRow; doctor-worklist/ward-rounding fall back to the hook’s hardcodedDEFAULT_*_COLUMNS. No admin UI authorsManifestColumnConfig[](the WorkflowActionConfigManager edits actions/miniapps/queueConfig only).
Path C — IPD /ipd/work-list ❌ orphaned
IpdRow.tsx(PatientInDormiroty) — 27 hardcoded columns.- Customization =
useIpdColumnVisibility(per-user localStorage, keymedos:ipd-worklist:column-visibility)IpdColumnPicker. Not wired torole_worklists, the manifest, or any per-department store.
Separate concerns — DO NOT merge into the above
| System | Store | Scope | Surface |
|---|---|---|---|
nurse_note_column_configs |
columns_json JSONB, scope-cascade + priority |
patient/dept/ward/role/user | /admin/nurse-note-columns → nursing-care-plan note columns (not worklist rows) |
queue_display_configs |
per kiosk “role” (tv/desk/kiosk) | dept × physical screen | queue display boards, not worklists (migration 20260511) |
Per-surface status
| Surface | Data source | Column source | Per-dept configurable today? |
|---|---|---|---|
OPD /medical-worklist |
department_queues + encounter_journey_cache |
Path A — template ui_manifest.columns + role override (LIVE) |
✅ per-dept/role override now renders (role_worklists.table_config.columns, shipped 2026-06-03); ⚠️ legacy REST accessors show '—' until the editor emits manifest accessors |
doctor-worklist / ward-rounding |
manifest (useManifestDoctorWorklist/useManifestList) |
hardcoded DEFAULT_*_COLUMNS (Path B dormant) |
❌ no |
IPD /ipd/work-list |
REST listAllAdmissions + ipd_admissions/encounter_journey_cache |
Path C (hardcoded + localStorage) | ❌ no |
Recommended ONE standard
- Data: all worklists + Patient 360 read the manifest-data-layer. Add
ward/bed(fromipd_admissions_dashboard) +bloodas manifest selectors so they’re available to any column.usePatientBodyDatabecomes a thin Patient-360 adapter over the same selectors. - Column config — standardize on Path A (the LIVE accessor model), retire Path B (dormant). Rationale:
Path A renders today on the real OPD worklist, has rich type-based cells (
esi/time/statusPoint/…), and is admin-authorable; Path B renders nowhere, has no column authoring UI, and produces flat untyped values. Choosing the dormant model = rebuild rendering + admin UI + migrate working OPD off a proven path. Don’t.- Definition =
ColumnDefinition[]inworkflow_templates.ui_manifest.columns(template defaults). - Per-dept/role override =
role_worklists.table_config.columns— ✅ DONE (read side, 2026-06-03):WorkflowConfigProvider.getColumnsForNodenow overlays normalized role columns over the template (the resolver is the provider, NOT the merger). Remaining: update the worklist-editorCustomizationModalto author manifest accessors +ColumnType(today’s legacyaccessors render'—'), so admins can produce columns that fully bind — or add an accessor crosswalk. - Harvest, don’t resurrect: Path B’s pure
(EncounterJourneyCache) => valueselectors are nice — reuse them asaccessorresolvers insidemanifest-bindings.tsif a new field needs one. Then delete the dormantuseManifestColumnConfig/toManifestTableRowduplicate (or leave it clearly marked dead). - localStorage stays — only as a per-user view preference layer on top of the resolved per-dept config.
- Definition =
- Migrate IPD (Path C) onto it: render
/ipd/work-listthroughConfigDrivenTablefed byColumnDefinition[](template defaults + role override); add the IPD-specific fields (news,esi,orderRing, ward/bed, home-med, blood) toResolvedWorklistRow+manifest-bindings.tsas new accessors. KeepIpdColumnPickeras the per-user override writing localStorage on top. - Leave
nurse_note_column_configsandqueue_display_configsalone — different, correctly-scoped concerns.
Open decisions (need a human call before building)
Path A vs Path B as the column resolver→ RESOLVED by the 2026-06-03 trace: standardize on Path A (the liveConfigDrivenTableaccessor model), retire the dormant Path B selector duplicate. The remaining sub-decision: when wiring the per-dept override, is dept+role (role_worklists) granularity enough, or do we also want per-user/ward (then reuse thenurse_note_column_configsscope-cascade shape)? See §Recommend-2.- IPD data swap: moving IPD to the manifest means moving off REST
listAllAdmissions→ doesencounter_journey_cachecarry every column IPD needs (e.g.news,esi,orderRing, home-med)? Gap analysis required (some are already manifest selectors; ward/bed/blood are being added per §Recommend-1). usePatientBodyDatafate: thin adapter (low effort) vs. fully fold into manifest selectors (cleaner, more work).- Scope granularity:
role_worklistsis org+dept+role;nurse_note_column_configsis full scope-cascade. Do worklist columns need ward/user scope too, or is dept+role enough?