medOS ultra

Admission Request Composability

Composability design for the admission-request surface.

4 min read diagramsUpdated 2026-05-11docs/architecture/admission-request-composability.md

Status: design draft (iteration 3 of the autonomous loop on 2026-05-11). Goal: let admins add other DynamicContentRenderer modules / e-forms as additional sections to the admission-request form, the same way summary-parent lets admins compose a patient-profile tab via DraggableDialogFlow in edit-flow-management.

Why

The user wants admission-request to stop being a hardcoded 42-field form and start accepting runtime-injected child blocks:

  • A SNOMED-aware AI diagnosis module (@miniapps/diagnosis-aiDiagnosisModuleAI)
  • An e-form (DynamicCoreApp.E_FORM / EFORM_SUMMARY)
  • Any other DynamicCoreApp value (lab order panel, prescription panel, etc.)
  • Custom user-built dialog flows (the DraggableDialogFlow editor target)

Without this, every new requirement requires editing packages/miniapps/admission-request/RequestForAdmin.tsx (1,700+ lines) and rebuilding the bundle. With it, admins configure once in /edit-flow-management and the changes flow through to every region.

Reference pattern: summary-parent

packages/miniapps/summary-parent/SummaryParent.tsx is the canonical pattern:

SummaryParent
 └─ DynamicDialogFlow
     └─ CustomDialogFlow (isSidebarMenu=[CORE_MEDICAL, CORE_FLOW, DYNAMIC_FLOW,
                          DYNAMIC_SHEET, E_FORM, PHARMACY, WEB_FORM,
                          GRAPHIC_SHEET, QUICK_ORDER])
        ├─ initialDialogs: read from editFlowDialog API
        └─ DraggableDialogFlow (the actual editor surface)

DynamicDialogFlow fetches the persisted layout via getEditFlowDialogByIdApi, hands it to CustomDialogFlow, and the renderer materialises whichever child blocks the admin chose.

Proposed shape

1. New entity: admissionRequestLayout

A persisted config (Mongo doc) keyed by tenant + (optional) ward type:

type AdmissionRequestLayout = {
  _id: string;
  tenantRef: string;
  scope: 'global' | { ward: string };
  // Ordered sections. Each section sits beneath the existing core form,
  // OR (Phase 2) replaces a specific anchor (`:after-dx`, `:after-priority`).
  sections: AdmissionRequestSection[];
};

type AdmissionRequestSection = {
  id: string;            // stable UUID for keys/diff
  kind: 'dynamic-module' | 'e-form' | 'dialog-flow';
  label: { th: string; en: string };
  collapsible?: boolean;
  anchor?: AdmissionRequestAnchor; // optional placement hint
  payload:
    | { kind: 'dynamic-module'; module: DynamicCoreApp }          // mounts via DynamicContentRenderer
    | { kind: 'e-form'; templateRef: string }                     // mounts via the e-form engine
    | { kind: 'dialog-flow'; dialogId: string };                  // mounts via DynamicDialogFlow
};

type AdmissionRequestAnchor =
  | 'after-dx' | 'after-priority' | 'after-bed' | 'after-notes' | 'end';

Stored alongside the existing editFlowDialog collection (re-use the schema, or add a thin sibling admissionRequestLayouts collection — picking the sibling keeps separation of concerns).

2. Render contract: ``

A thin React component owned by admission-request:

<AdmissionRequestSections
  anchor="after-dx"            // only render sections at this anchor
  layout={resolvedLayout}      // pulled via useAdmissionRequestLayout()
  patientRef={patientRef}
  encounterRef={encounterRef}
/>

Internally, it iterates layout.sections filtered by anchor and, for each section, switches on kind:

  • dynamic-module → mount via existing `` with patientRef + encounterRef injected as selectData.
  • e-form → mount via the existing e-form engine (same pattern as EFORM_SUMMARY consumers).
  • dialog-flow → mount `` — same wrapper SummaryParent uses today.

Failure mode: every section is wrapped in a `` (already defined in pages/sandbox/workspace.tsx) so a buggy plug-in can’t kill the admission form.

3. Admin UI

Re-use the existing /edit-flow-management editor and persist to the new collection. No new editor surface required for the MVP.

4. Migration path

  1. Phase 1 — opt-in addendum (smallest patch). Render `` at the bottom of RequestForAdmin.tsx. When no layout is configured, nothing renders — zero behaviour change for existing tenants.
  2. Phase 2 — anchored injection. Add anchor points at the 5 most-likely insertion sites (see AdmissionRequestAnchor). Each anchor is a single `` invocation in JSX.
  3. Phase 3 — replace core fields. Let layouts optionally hide built-in field groups (e.g. swap the built-in Dx text field for the DiagnosisModuleAI module). Gate behind a feature flag until the field-shape mapping (Dx ↔ DiagnosisModuleAI.initialData.principalDiagnosis) is firm.

What changed in this loop (iteration 1)

  • Dx field now opens DialogSnomedSearchGroup (the existing SNOMED dialog from medical-kit/diagnostics/components/dialogs/DialogSnomedSearchGroup) — same pattern blood-bank already uses, no new dependency.
  • Selecting a concept populates formik.values.dx (display term) + formik.values.dxSnomed ({ code, term, raw }), so FHIR mapping can recover the concept ID later.
  • End-to-end verified in the sandbox: typed “sarcoptic” → got “Sarcoptic itch” → Save closed dialog → Dx field showed the term.

This is the incremental fix the user asked for (visible SNOMED group). The embedding architecture above is the bigger lever the user described (“allow these components to add other DynamicContentRenderer and e-form into here like summary parents in edit-flow-management”). The phased migration lets us ship the small win today and the architectural win without breaking the existing form.

Ask Anything