medOS ultra

Worklist Data & Column Config

Audit + reconciliation of how worklists get their data and columns across the canonical data and column-config layers.

7 min read diagramsUpdated 2026-06-03docs/architecture/worklist-data-and-column-config-reconciliation.md

Why this doc: before adding per-department customizable columns to /ipd/work-list and 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 } where type ∈ text|esi|time|status|statusPoint|booleanStatus|dateTime drives a type-specific cell component (ESI chip, MM:SS time color-coding, status dot, …). accessor (e.g. "encounter.waitDuration") is resolved by manifest-bindings.ts against a ResolvedWorklistRow (= department_queuesencounter_journey_cache).
  • The column resolver is WorkflowConfigProvider.getColumnsForNode (reads config.columns = ui_manifest.columns). The workflow-config-merger.ts is a separate path that only merges nodes (visibility + queueConfig + rowActions); its MergedWorkflow type has no columns field. So columns never flowed through the merger — that’s why the earlier “merge role .columns in the merger” framing was wrong.
  • SHIPPED 2026-06-03 (read side): WorkflowConfigProvider now overlays role_worklists.table_config.columns over the template columns per node (matched by node id or data.type), via normalizeRoleColumns() which accepts BOTH the ColumnDefinition shape AND the legacy worklist-editor WorklistColumn (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-configCustomizationModal) still authors legacy accessors 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.tsxWorkflowConfigBasedTabsConfigDrivenTable.

Path B — manifest selector model (useManifestColumnConfig) ❌ DORMANT (renders nowhere)

  • ManifestColumnConfig = { key, header, selectorId, width, sortable, formatterId, path?, adapterField? }; selectorIdmanifestSelectors[id](EncounterJourneyCache) (a clean pure-function registry) → toManifestTableRow produces a flat { key: value } (no type-based cell rendering — weaker than Path A’s typed cells).
  • Storage: workflow_action_configurations.configuration.columnConfig (migration 20250114_workflow_action_config.sql), via get_active_workflow_config/save_workflow_config RPCs.
  • Status: complete but NOT IMPORTED by any rendered table. No component calls useManifestColumnConfig/ toManifestTableRow; doctor-worklist/ward-rounding fall back to the hook’s hardcoded DEFAULT_*_COLUMNS. No admin UI authors ManifestColumnConfig[] (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, key medos:ipd-worklist:column-visibility)
    • IpdColumnPicker. Not wired to role_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

  1. Data: all worklists + Patient 360 read the manifest-data-layer. Add ward/bed (from ipd_admissions_dashboard) + blood as manifest selectors so they’re available to any column. usePatientBodyData becomes a thin Patient-360 adapter over the same selectors.
  2. 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[] in workflow_templates.ui_manifest.columns (template defaults).
    • Per-dept/role override = role_worklists.table_config.columns — ✅ DONE (read side, 2026-06-03): WorkflowConfigProvider.getColumnsForNode now overlays normalized role columns over the template (the resolver is the provider, NOT the merger). Remaining: update the worklist-editor CustomizationModal to author manifest accessors + ColumnType (today’s legacy accessors render '—'), so admins can produce columns that fully bind — or add an accessor crosswalk.
    • Harvest, don’t resurrect: Path B’s pure (EncounterJourneyCache) => value selectors are nice — reuse them as accessor resolvers inside manifest-bindings.ts if a new field needs one. Then delete the dormant useManifestColumnConfig/toManifestTableRow duplicate (or leave it clearly marked dead).
    • localStorage stays — only as a per-user view preference layer on top of the resolved per-dept config.
  3. Migrate IPD (Path C) onto it: render /ipd/work-list through ConfigDrivenTable fed by ColumnDefinition[] (template defaults + role override); add the IPD-specific fields (news, esi, orderRing, ward/bed, home-med, blood) to ResolvedWorklistRow + manifest-bindings.ts as new accessors. Keep IpdColumnPicker as the per-user override writing localStorage on top.
  4. Leave nurse_note_column_configs and queue_display_configs alone — different, correctly-scoped concerns.

Open decisions (need a human call before building)

  1. Path A vs Path B as the column resolverRESOLVED by the 2026-06-03 trace: standardize on Path A (the live ConfigDrivenTable accessor 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 the nurse_note_column_configs scope-cascade shape)? See §Recommend-2.
  2. IPD data swap: moving IPD to the manifest means moving off REST listAllAdmissions → does encounter_journey_cache carry 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).
  3. usePatientBodyData fate: thin adapter (low effort) vs. fully fold into manifest selectors (cleaner, more work).
  4. Scope granularity: role_worklists is org+dept+role; nurse_note_column_configs is full scope-cascade. Do worklist columns need ward/user scope too, or is dept+role enough?
Ask Anything