Screening Modal AI Authoring Kit
Single-source recipe for an AI or human to drop new tabs into the screening modal.
Single-source recipe for an AI (or human) to drop new tabs into the screening modal (
ซักประวัติผู้ป่วยdialog insideFutureOrderCheckedIn) 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
-
Pick a subClinic UUID from
web/scripts/screening-tab-catalog.json→subDepts[]. Or paste any productionsubClinic._id. -
Pick one or more module IDs from
modulesByCategory._recommended_for_screening_tabs(or grep the full 293-module list). -
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 } } -
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 withAPI_BASE=https://your-host/api/. Default login issa / dev321456(PH demo). Override withUSER=… PASS=…or supplyAUTH_TOKEN=eyJ.... -
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 viaDynamicContentRenderer.
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>
- Pick a test patient (Sandbox Adult Male is fine — the demo bypasses encounter fetch).
- Confirm the new tab appears in the strip.
- Click it — body should render the chosen DynamicCoreApp module.
- 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.