medOS ultra

Screening Modal AI Authoring Kit

Single-source recipe for an AI or human to drop new tabs into the screening modal.

7 min read diagramsUpdated 2026-05-13docs/screening-modal-ai-authoring.md

Single-source recipe for an AI (or human) to drop new tabs into the screening modal (ซักประวัติผู้ป่วย dialog inside FutureOrderCheckedIn) by pairing DynamicCoreApp modules with subClinic UUIDs. Output: one EditFlowTemplate row in Mongo via /admin-panel/edit-flow-management’s API — which the screening modal reads at runtime.

If you’re an AI: read § “Pick + paste” first, fill in the template, run generate-screening-tab.sh, and you’re done. Everything else here is context for when something goes wrong.


Pick + paste

  1. Pick a subClinic UUID from web/scripts/screening-tab-catalog.jsonsubDepts[]. Or paste any production subClinic._id.

  2. Pick one or more module IDs from modulesByCategory._recommended_for_screening_tabs (or grep the full 293-module list).

  3. Build a JSON tab item per module:

    {
      "id": "tab-<short-slug>",                     // any unique string
      "type": "screening-tab",                      // discriminator — must be exactly this
      "appId": "modules.NutritionRequest",          // the DynamicCoreApp value
      "name": "Nutrition ผู้ป่วยเบาหวาน",            // Thai label shown on the tab
      "data": {
        "visible":      true,                       // optional, default true
        "editable":     true,                       // optional, default true
        "displayOrder": 100,                        // higher = further right; missing = natural order
        "fieldOverrides": { }                       // optional, per-field hide/readOnly/required
      }
    }
    
  4. Run the generator:

    web/scripts/generate-screening-tab.sh \
      --subdept 8df92e78-0823-4908-b8ac-b24ccd3f3276 \
      --name "DM clinic — Nutrition tab" \
      --tabs '[ { "id": "tab-dm-nutri", "type": "screening-tab", "appId": "modules.NutritionRequest", "name": "Nutrition เบาหวาน", "data": { "visible": true, "editable": true, "displayOrder": 100 } } ]'
    

    Or stash the JSON in a file and pass --tabs-file ./tabs.json.

    Default API target is https://his-thailand.vercel.app/api/. Override with API_BASE=https://your-host/api/. Default login is sa / dev321456 (PH demo). Override with USER=… PASS=… or supply AUTH_TOKEN=eyJ....

  5. Verify — reload the screening modal. The new tab appears at the end of the existing tabs (or at the position implied by displayOrder). Its body renders the chosen DynamicCoreApp module via DynamicContentRenderer.

That’s the whole loop. Below is reference material.


What this writes to

Mongo collection: edit_flow_template API: POST /v2/administration/editFlowTemplates Shape:

{
  _id: Uuid,
  name: string,            // human label, shown in the admin builder list
  type: Ref<EditFlowTemplateType>,  // resolves to "Screening Modal Grouping" — the SCREENING_TAB_GROUPING discriminator
  subClinicRef: Uuid,      // matches patientData.subClinic._id at runtime
  data: Array<{
    id: string,
    type: 'screening-tab', // exact string — anything else is ignored by the adapter
    appId: string,         // DynamicCoreApp value (e.g. 'modules.X')
    name: string,          // tab label
    data: {
      visible?:       boolean,
      editable?:      boolean,
      displayOrder?:  number,
      fieldOverrides?: Record<string, { hide?: boolean; readOnly?: boolean; required?: boolean }>
    }
  }>,
  active: true
}

The screening modal reads this via useScreeningModalConfig (Tier 1 in the 4-tier resolver: EditFlowTemplate ▸ Supabase ▸ localStorage ▸ defaults). Tier 1 is checked first by screeningModalConfig.service.ts:resolveConfig.

Catalog files

File Purpose
web/scripts/screening-tab-catalog.json The stock tab list (12 ids), the 16 known subdept UUIDs, and a curated modulesByCategory._recommended_for_screening_tabs list. AI: jq this for everything.
web/src/setup/dynamic/DynamicCoreApp.ts Source of truth for all 293 modules.X values. grep "^ [A-Z_]\+ = 'modules\\." web/src/setup/dynamic/DynamicCoreApp.ts | sed -E "s/.* = '([^']+)'.*/\1/" extracts the full list.
web/src/setup/data/screening/screening.ts The 10 production subdept buckets (checkAdditionalVitalSignNewScreeningV2). Each lists the subClinic UUIDs that resolve to one of OBSTETRIC / DIABETES / HEARTRESCUE / LISTBABYCLINIC / LISTEMERGENCYCLINIC / IVFCLINIC / ARVCLINIC / FAMILYPLANNING / LISTOPHTHLMIC / DENTAL.
web/src/services/screening-config/screeningModalConfig.types.ts ScreeningTabConfig, ScreeningModalConfigResult — the runtime shapes.
web/src/services/screening-config/screeningModalConfig.service.ts flowDataToTabs() adapter (the function the screening modal calls to convert the EditFlowTemplate data array into runtime tabs) — accepts BOTH the flow-builder native shape (above) and a raw-passthrough shape ({ tabId, visible, … }).
web/scripts/generate-screening-tab.sh The curl-based one-shot generator.
web/src/setup/data/screening/__tests__/screening.test.ts npx tsx <path> — 22 assertions covering every production + legacy subdept UUID resolves to the right enum. Run this if you suspect the resolver mapping.

Common recipes

1. Add a “Diabetes Nutrition” tab to all DM departments

for DEPT in \
  8df92e78-0823-4908-b8ac-b24ccd3f3276 \
  762a2101-3824-4483-b469-184fc8b08a85 \
  42ea2e9f-53b7-42a9-b297-a165d198ef16 \
  4c6a7a26-4662-48e5-bc1c-8f4e9689a943
do
  web/scripts/generate-screening-tab.sh \
    --subdept "$DEPT" \
    --name    "DM dept — Nutrition + Education appendix" \
    --tabs    '[
      { "id": "tab-dm-nutri", "type": "screening-tab", "appId": "modules.NutritionRequest", "name": "Nutrition เบาหวาน",
        "data": { "visible": true, "displayOrder": 100 } },
      { "id": "tab-dm-edu",   "type": "screening-tab", "appId": "modules.PatientEducation",  "name": "DSME · DM education",
        "data": { "visible": true, "displayOrder": 110 } }
    ]'
done

2. Replace the 4 base tabs with a single Notes-only view for a specific clinic

Set the 4 stock tabs to visible: false and append modules.Notes:

[
  { "tabId": "0", "visible": false, "editable": false },
  { "tabId": "1", "visible": false, "editable": false },
  { "tabId": "2", "visible": false, "editable": false },
  { "tabId": "3", "visible": false, "editable": false },
  { "id": "tab-notes-only", "type": "screening-tab", "appId": "modules.Notes", "name": "Doctor Notes",
    "data": { "visible": true, "displayOrder": 0 } }
]

(Note: raw-passthrough {tabId, visible} shape is fine for stock-tab toggles; flow-builder shape with appId is for custom tabs.)

3. Reorder stock tabs (put behavioral health first)

[
  { "tabId": "3", "visible": true, "editable": true, "displayOrder": 0 },
  { "tabId": "0", "visible": true, "editable": true, "displayOrder": 10 },
  { "tabId": "1", "visible": true, "editable": true, "displayOrder": 20 },
  { "tabId": "2", "visible": true, "editable": true, "displayOrder": 30 }
]

4. Hide a single field on the diabetes tab

[
  { "tabId": "5", "visible": true, "editable": true,
    "fieldOverrides": { "ckdStage": { "hide": true }, "dsmeCompleted": { "required": true } } }
]

(Field keys are the form-state paths under values.additionalClinic.* — see Diabetes.tsx for the catalog.)

Verification

A. Quick — visual check in the sandbox

http://localhost:5173/sandbox/workspace?module=modules.ScreeningModal&subdept=<your-subdept-uuid>
  1. Pick a test patient (Sandbox Adult Male is fine — the demo bypasses encounter fetch).
  2. Confirm the new tab appears in the strip.
  3. Click it — body should render the chosen DynamicCoreApp module.
  4. The 🧪 SubClinic Override + 🏥 Dept Override bars (at the top) let you flip without re-logging-in.

B. Logic — pure-function tests

cd web
npx tsx src/setup/data/screening/__tests__/screening.test.ts
npx tsx src/services/screening-config/__tests__/screeningModalConfig.defaults.test.ts

Both return N pass, 0 fail when the resolver + defaults are healthy.

C. Backend — confirm the row landed

curl -sS -H "Authorization: Bearer $AUTH_TOKEN" \
  "$API_BASE/v2/administration/editFlowTemplates?type=$TYPE_ID&subClinicRef=$SUBDEPT" \
  | jq '.data.rows[] | { _id, name, data: (.data | length) }'

Troubleshooting

Symptom Likely cause Fix
Tab appears in admin builder but NOT in the screening modal type name mismatch — must be exactly “Screening Modal Grouping” Confirm via GET /v2/administration/editFlowTemplateTypes; recreate type with the exact name (case-insensitive on the resolver side).
Tab appears in the strip but body is blank appId not a valid DynamicCoreApp value Grep web/src/setup/dynamic/DynamicCoreApp.ts for the exact modules.X string. Typos fall through DynamicContentRenderer’s default case → empty.
Diabetes (or any specialty) tab is missing for a known-good UUID Resolver returns undefined — the legacy-seed list is missing your UUID Open screening.ts, add your UUID to the matching list, re-run the resolver test.
Saved but doesn’t show up after reload EditFlowTemplate path is cached for the session; or subClinicRef doesn’t match patientData.subClinic._id exactly Hard-reload (Cmd-Shift-R). Re-check the subdept UUID via the 🧪 SubClinic Override dropdown’s “id” badge.
401 / 403 on POST AUTH_TOKEN expired or wrong tenant Re-login: unset AUTH_TOKEN && USER=sa PASS=dev321456 ./generate-screening-tab.sh ...

Architecture summary

EditFlowTemplate (Mongo)
  type = "Screening Modal Grouping"
  subClinicRef = <subClinic._id>
  data = [{ id, type:'screening-tab', appId, name, data:{visible,editable,displayOrder,fieldOverrides} }, ...]
        │
        ▼  read by `useScreeningModalConfig` (Tier 1)
ScreeningModalConfigResult
  { customTabs: [...], visible: {...}, displayOrder: {...}, ... }
        │
        ▼
ScreeningV2 listTab effect
  appends customTabs after the 12 stock tabs, then sorts by displayOrder
        │
        ▼
RenderNonTrauma
  for each customTab → <CustomTabPanel value={tabId}><DynamicContentRenderer tabName={appId} /></CustomTabPanel>

The matrix UI at /admin/screening-modal-config writes to Supabase (Tier 2) — useful for visibility toggles and field overrides on stock tabs. For adding NEW custom tabs, prefer this kit / /admin-panel/edit-flow-management — that’s the Tier-1 source-of-truth.

Ask Anything