Admission Request Composability
Composability design for the admission-request surface.
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-parentlets admins compose a patient-profile tab viaDraggableDialogFlowin 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-ai—DiagnosisModuleAI) - An e-form (
DynamicCoreApp.E_FORM/EFORM_SUMMARY) - Any other
DynamicCoreAppvalue (lab order panel, prescription panel, etc.) - Custom user-built dialog flows (the
DraggableDialogFloweditor 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 `` withpatientRef+encounterRefinjected asselectData.e-form→ mount via the existing e-form engine (same pattern asEFORM_SUMMARYconsumers).dialog-flow→ mount `` — same wrapperSummaryParentuses 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
- 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. - Phase 2 — anchored injection. Add anchor points at the 5 most-likely
insertion sites (see
AdmissionRequestAnchor). Each anchor is a single `` invocation in JSX. - Phase 3 — replace core fields. Let layouts optionally hide built-in
field groups (e.g. swap the built-in Dx text field for the
DiagnosisModuleAImodule). 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 frommedical-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.