From 97fa8aa76e51ea20c7de99bca518764565192cd7 Mon Sep 17 00:00:00 2001 From: Lissan <150966211+DataForSolution@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:51:14 -0400 Subject: [PATCH 01/10] feat(order-helper): add public rules loader to default layout --- _layouts/default.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/_layouts/default.html b/_layouts/default.html index dc864fc..cb8ebb3 100644 --- a/_layouts/default.html +++ b/_layouts/default.html @@ -69,6 +69,15 @@ {% if page.custom_css %} {% endif %} +{% if page.url contains '/order-helper/' %} + + +{% endif %} {% if page.url contains '/order-helper/' %} From 4388e1c1cca0ad22c43a7b6292cacbea683d8e18 Mon Sep 17 00:00:00 2001 From: Lissan <150966211+DataForSolution@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:49:29 -0400 Subject: [PATCH 02/10] feat(order-helper): add non-destructive rules loader shim + cascades --- order-helper/app.js | 120 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/order-helper/app.js b/order-helper/app.js index 2ca2150..b1d427f 100644 --- a/order-helper/app.js +++ b/order-helper/app.js @@ -714,3 +714,123 @@ } })(); +/* ===== OH loader shim (non-destructive) ===== */ +(() => { + // Wait for DOM if this script isn’t loaded with `defer` + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { once: true }); + } else { + init(); + } + + function init() { + // Namespace (won't clobber your existing code) + window.OH = window.OH || {}; + const $ = (s) => document.querySelector(s); + const setText = (el, msg) => { if (el) el.textContent = msg; }; + + // Elements (adjust IDs only if yours differ) + const els = { + status: $('#status'), + modality: $('#modality'), + region: $('#region'), + bodyPart: $('#bodyPart'), + contrast: $('#contrast'), + laterality: $('#laterality'), + context: $('#context'), + }; + + // Use the exact meta path (you already include ?v=20250913) + const RULES_URL = + document.querySelector('meta[name="oh-rules-path"]')?.content || + '/order-helper/data/rules.json'; + + const FALLBACK = Object.freeze({ + schema_version: '1.1', + modalities: { + 'PET/CT': { + regions: ['Skull base to mid-thigh', 'Whole body'], + body_parts: ['Head/Neck', 'Chest', 'Abdomen/Pelvis'], + contrast: ['None'], + laterality: ['N/A'], + contexts: ['Staging','Restaging','Treatment response','Surveillance','Acute'], + }, + 'CT': { + regions: ['Head/Brain','Chest','Abdomen/Pelvis'], + body_parts: ['Head','Chest','Abdomen','Pelvis'], + contrast: ['None','IV','Oral','IV + Oral'], + laterality: ['N/A','Right','Left','Bilateral'], + contexts: ['Acute','Follow-up','Staging'], + }, + 'MRI': { + regions: ['Brain','Spine','MSK'], + body_parts: ['Brain','Cervical','Lumbar','Hip'], + contrast: ['None','Gadolinium'], + laterality: ['N/A','Right','Left','Bilateral'], + contexts: ['Acute','Follow-up','Staging'], + } + } + }); + + const validate = (cat) => !!(cat && typeof cat === 'object' && cat.modalities && typeof cat.modalities === 'object'); + + function setOptions(selectEl, items, placeholder) { + if (!selectEl) return; + const list = Array.isArray(items) ? items : []; + selectEl.innerHTML = ''; + const ph = document.createElement('option'); + ph.value = ''; + ph.textContent = placeholder || 'Select…'; + ph.disabled = true; ph.selected = true; + selectEl.appendChild(ph); + for (const v of list) { + const opt = document.createElement('option'); + opt.value = v; opt.textContent = v; + selectEl.appendChild(opt); + } + } + + function bindCascades(cat) { + if (!els.modality) return; + const modalities = Object.keys(cat.modalities || {}); + setOptions(els.modality, modalities, 'Select modality…'); + + els.modality.addEventListener('change', () => { + const m = els.modality.value; + const spec = (cat.modalities || {})[m] || {}; + setOptions(els.region, spec.regions, 'Select region…'); + setOptions(els.bodyPart, spec.body_parts, 'Select body part…'); + setOptions(els.contrast, spec.contrast, 'Select contrast…'); + setOptions(els.laterality, spec.laterality, 'Select laterality…'); + setOptions(els.context, spec.contexts, 'Select context…'); + }); + } + + async function loadCatalog() { + setText(els.status, 'Loading rules…'); + try { + const res = await fetch(RULES_URL, { cache: 'no-store' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const json = await res.json(); + if (!validate(json)) throw new Error('Invalid rules schema'); + setText(els.status, 'Rules loaded.'); + return json; + } catch (err) { + console.warn('[OH] rules load failed, using fallback:', err); + setText(els.status, 'Using built-in defaults (rules.json unavailable).'); + return FALLBACK; + } + } + + // Expose for the rest of your app (non-breaking) + window.OH.loadCatalog = loadCatalog; + + // Boot: load then bind; announce readiness + loadCatalog().then(cat => { + window.OH.catalog = cat; + bindCascades(cat); + document.dispatchEvent(new CustomEvent('oh:catalog-ready', { detail: { catalog: cat } })); + }); + } +})(); + From 5bbb661b02c12fdadcee4ef9d05196ec70921104 Mon Sep 17 00:00:00 2001 From: Lissan <150966211+DataForSolution@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:54:51 -0400 Subject: [PATCH 03/10] fix(order-helper): normalize rules.json (dash mojibake, array/comma/braces), validate JSON --- order-helper/data/rules.json | 697 +++++++++++++++++++++++++++++++++++ 1 file changed, 697 insertions(+) diff --git a/order-helper/data/rules.json b/order-helper/data/rules.json index f1c8284..584776a 100644 --- a/order-helper/data/rules.json +++ b/order-helper/data/rules.json @@ -1,3 +1,4 @@ +<<<<<<< HEAD { "schema_version": "2.0", "generated_at": "2025-09-15", @@ -70,21 +71,87 @@ "Hip | Hip | None": ["73501"] } }, +======= + +{ + "schema_version": "1.1", + "generated_at": "2025-08-19", + "modalities": { + "X-Ray": { + "regions": [ + "Chest", + "Abdomen (KUB)", + "Pelvis", + "Skull", + "Cervical spine", + "Thoracic spine", + "Lumbar spine", + "Scoliosis series", + "Upper extremity", + "Lower extremity", + "Shoulder", + "Elbow", + "Wrist/Hand", + "Hip", + "Knee", + "Ankle/Foot", + "Skeleton survey" + ], + "contexts": [ + "Acute", + "Trauma", + "Follow-up", + "Infection", + "Metastatic survey", + "Pre-operative", + "Post-operative" + ], + "conditions": [ + "Fracture", + "Dislocation", + "Osteoarthritis", + "Evaluation for pneumonia", + "Effusion", + "Foreign body", + "Osteomyelitis", + "Metastasis", + "Scoliosis" + ], + "indication_templates": [ + "X-Ray {region} — {context} for {condition}", + "X-Ray {region} — rule out {condition}" + ], + "notes": [ + "Use two views (minimum) for trauma if possible.", + "Skeleton survey used for myeloma or metastatic disease patterns (consider bone scan if broader sensitivity is needed).", + "No contrast used for plain radiography." + ] +} +>>>>>>> 6490a75 (fix(order-helper): normalize rules.json (dash mojibake, array/comma/braces), validate JSON) "CT": { "regions": [ "Head/Brain", "Sinuses", +<<<<<<< HEAD "Maxillofacial/Facial bones", "Temporal bones/IAC", "Neck", "Chest", "Low-dose Lung (Screening)", +======= + "Maxillofacial/Facial Bones", + "Temporal Bones/IAC", + "Neck", + "Chest", + "Low‑Dose Lung CT (Screening)", +>>>>>>> 6490a75 (fix(order-helper): normalize rules.json (dash mojibake, array/comma/braces), validate JSON) "Abdomen", "Pelvis", "Abdomen/Pelvis", "CT Urogram", "CT Enterography", +<<<<<<< HEAD "Spine - Cervical", "Spine - Thoracic", "Spine - Lumbar", @@ -601,4 +668,634 @@ ] } +======= + "Spine – Cervical", + "Spine – Thoracic", + "Spine – Lumbar", + "Upper Extremity", + "Lower Extremity", + "Cardiac Coronary CTA", + "Angiography – Head/Neck CTA", + "Angiography – Chest CTA (PE)", + "Angiography – Aorta CTA", + "Angiography – Run‑off CTA (LE)" + ], + "contrast_options": [ + { "value": "with_iv", "label": "With IV contrast" }, + { "value": "without_iv", "label": "Without IV contrast" }, + { "value": "oral", "label": "Add oral contrast (abd/pelvis)" }, + { "value": "none", "label": "None / Not applicable" } + ], + "contexts": [ + "Initial evaluation", + "Acute symptoms", + "Follow‑up", + "Pre‑operative planning", + "Post‑operative complication", + "Oncology staging", + "Oncology restaging / surveillance", + "Trauma", + "Screening", + "Infection / inflammation" + ], + "conditions": [ + "Headache (sudden / thunderclap)", + "Head trauma", + "Stroke symptoms / TIA", + "Sinusitis", + "Neck mass", + "Pulmonary embolism suspected", + "Aortic dissection / aneurysm suspected", + "Lung nodule", + "Pneumonia complication", + "Abdominal pain RLQ (appendicitis)", + "Kidney stone / renal colic", + "Pancreatitis", + "Liver lesion characterization", + "Diverticulitis", + "Inflammatory bowel disease flare", + "Bowel obstruction", + "Post‑op abdomen", + "Hematuria", + "Cancer staging (specify primary)", + "Metastatic disease restaging", + "Spine trauma", + "Cervical radiculopathy", + "Spinal stenosis", + "Extremity fracture", + "Suspected osteomyelitis", + "Peripheral arterial disease (LE run‑off)" + ], + "indication_templates": [ + "CT {region} – {context} for {condition}", + "CT {region} – rule out {condition}", + "CT {region} {contrast_text} – {context} ({condition})" + ], + "contrast_recommendations": [ + { "match": ["kidney stone", "renal colic"], "suggest": "without_iv" }, + { "match": ["appendicitis", "rlq"], "suggest": "with_iv" }, + { "match": ["pe", "pulmonary embolism"], "suggest": "with_iv" }, + { "match": ["aortic", "dissection", "aneurysm"], "suggest": "with_iv" }, + { "match": ["liver lesion", "pancreatitis"], "suggest": "with_iv" }, + { "match": ["bowel obstruction"], "suggest": "without_iv" }, + { "match": ["trauma"], "suggest": "with_iv" }, + { "match": ["low‑dose lung ct", "screening"], "suggest": "without_iv" } + ], + "notes": [ + "CTA exams require IV contrast.", + "Oral contrast is site‑dependent for abdomen/pelvis—offered as an option.", + "This tool structures ordering; it is not medical advice." + ] + } + }, + "records": [ + { + "id": "PET-SB-MT-ONC", + "modality": "PET/CT", + "study_name": "FDG PET/CT — Skull Base to Mid-Thigh", + "cpt": ["78815"], + "icd10_common": ["C34.90", "C18.9", "C50.919", "C43.9", "C32.9", "C81.90", "C85.90"], + "contrast": "Radiotracer (± IV contrast on CT)", + "header_coverage": "Skull base → mid-thigh", + "contexts": ["staging", "restaging", "treatment response", "surveillance"], + "keywords": ["lymphoma", "nsclc", "lung cancer", "breast cancer", "colorectal", "colon cancer", "melanoma", "head and neck", "hnscc", "gastric", "pancreatic"], + "reasons": [ + "FDG PET/CT for {context} of {condition}; evaluate disease extent, nodal involvement, and FDG-avid distant metastases." + ], + "prep": "Fast 4–6 h; avoid strenuous exercise 24 h; glucose <200 mg/dL.", + "supporting_docs": [ + "Clinic note documenting diagnosis and clinical question.", + "Prior imaging/report.", + "Therapy timeline (chemo/radiation/surgery) and relevant labs." + ], + "flags": [ + "Recent G-CSF can increase marrow uptake.", + "Hyperglycemia lowers tumor-to-background contrast." + ], + "prior_auth": { "requires_prior_auth": true, "step_imaging_required": true }, + "tags": ["oncology-general"] + }, + { + "id": "PET-WHOLEBODY", + "modality": "PET/CT", + "study_name": "FDG PET/CT — Whole Body (Vertex to Toes)", + "cpt": ["78816"], + "icd10_common": ["C43.9", "C49.9", "C90.00", "M31.7", "R50.9"], + "contrast": "Radiotracer (± IV contrast on CT)", + "header_coverage": "Vertex → toes", + "contexts": ["staging", "restaging", "surveillance"], + "keywords": ["melanoma", "myeloma", "sarcoma", "vasculitis", "fever of unknown origin", "fuo"], + "reasons": [ + "FDG PET/CT whole body for {context} of {condition}; evaluate for extremity involvement and FDG-avid metastatic/inflammatory disease." + ], + "prep": "Standard FDG fasting; keep patient warm to limit brown fat uptake.", + "supporting_docs": ["Referring note with suspicion/diagnosis.", "Biopsy/pathology if available.", "Prior imaging for correlation."], + "flags": ["Include extremities for melanoma/myeloma.", "Consider inflammatory patterns in vasculitis/FOU."], + "prior_auth": { "requires_prior_auth": true }, + "tags": ["whole-body"] + }, + { + "id": "PET-BRAIN-FDG", + "modality": "PET", + "study_name": "FDG Brain PET", + "cpt": ["78608"], + "icd10_common": ["F03.90", "G30.9", "G31.09", "G40.909"], + "contrast": "Radiotracer only", + "header_coverage": "Brain", + "contexts": ["dementia", "epilepsy"], + "keywords": ["alzheim", "frontotemporal", "ftd", "epilepsy", "seizure", "temporal lobe"], + "reasons": [ + "FDG brain PET to evaluate cerebral metabolic patterns in {condition}; correlate with clinical and prior imaging." + ], + "prep": "Quiet, dim environment pre-injection; NPO; epilepsy timing per protocol.", + "supporting_docs": ["Neurology note with symptoms & question.", "Prior MRI/EEG."], + "flags": ["FDG patterns vary by dementia subtype.", "Medication/timing affects epilepsy localization."], + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true }, + "tags": ["neuro"] + }, + { + "id": "PET-CARDIAC-FDG-VIABILITY", + "modality": "PET", + "study_name": "PET Cardiac FDG — Viability", + "cpt": ["78459", "78491", "78492"], + "icd10_common": ["I25.10", "I42.9"], + "contrast": "Radiotracer only", + "header_coverage": "Heart", + "contexts": ["viability"], + "keywords": ["viability", "ischemic cardiomyopathy", "hibernating myocardium"], + "reasons": [ + "FDG PET to assess myocardial viability in ischemic cardiomyopathy; correlate with perfusion and echocardiography." + ], + "prep": "Glucose loading/insulin protocol per SOP; coordinate with perfusion.", + "supporting_docs": ["Cardiology note on revascularization decision.", "Echo/perfusion/coronary imaging reports."], + "flags": ["Glycemic control critical for image quality."], + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true }, + "tags": ["cardiac"] + }, + { + "id": "PET-INFECTION-SB-MT", + "modality": "PET/CT", + "study_name": "FDG PET/CT — Infection/Inflammation (Skull Base to Mid-Thigh)", + "cpt": ["78815"], + "icd10_common": ["M86.9", "T84.59XA", "I33.0", "R50.9"], + "contrast": "Radiotracer (± IV contrast on CT)", + "header_coverage": "Skull base → mid-thigh", + "contexts": ["suspected infection"], + "keywords": ["osteomyelitis", "prosthetic joint", "infection", "endocarditis", "fuo"], + "reasons": [ + "FDG PET/CT to evaluate suspected infection/inflammation related to {condition}; assess extent and possible sites of involvement." + ], + "prep": "Standard FDG fasting; review recent antibiotics.", + "supporting_docs": ["Clinical notes with symptoms/duration.", "WBC, CRP/ESR, cultures.", "Prior imaging for comparison."], + "flags": ["Prosthesis may show inflammatory uptake; interpret with context."], + "prior_auth": { "requires_prior_auth": true }, + "tags": ["infection"] + }, + { + "id": "PET-PSMA", + "modality": "PET/CT", + "study_name": "PSMA PET/CT — Prostate Cancer", + "cpt": ["78815", "78816"], + "icd10_common": ["C61", "R97.21"], + "contrast": "Radiotracer (PSMA) ± IV contrast CT", + "header_coverage": "Skull base → mid-thigh or whole body", + "contexts": ["staging", "biochemical recurrence"], + "keywords": ["prostate", "psa"], + "reasons": [ + "PSMA PET/CT for {context} in prostate cancer; localize recurrence/metastases to guide therapy." + ], + "prep": "Hydration; void frequently; follow tracer-specific guidance.", + "supporting_docs": ["PSA level and trend.", "Prior therapy and pathology."], + "flags": [], + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true }, + "tags": ["prostate"] + }, + { + "id": "PET-DOTATATE", + "modality": "PET/CT", + "study_name": "DOTATATE PET/CT — Neuroendocrine Tumor", + "cpt": ["78815", "78816"], + "icd10_common": ["C7A.8", "C7B.8", "D3A.8"], + "contrast": "Radiotracer (Ga-68/Lu-177) ± IV contrast CT", + "header_coverage": "Skull base → mid-thigh or whole body", + "contexts": ["staging", "restaging", "surveillance"], + "keywords": ["NET", "neuroendocrine tumor", "carcinoid"], + "reasons": [ + "DOTATATE PET/CT to stage/restage neuroendocrine tumor and evaluate metastases/recurrence." + ], + "prep": "Hydration; withhold long-acting somatostatin analogs per protocol.", + "supporting_docs": ["Pathology and tumor grade if known.", "Prior imaging for comparison."], + "flags": [], + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true }, + "tags": ["NET"] + }, + { + "id": "CT-HEAD-WO", + "modality": "CT", + "study_name": "CT Head without Contrast", + "cpt": ["70450"], + "icd10_common": ["I63.9", "S06.9X9A", "R51.9", "R42"], + "contrast": "Without", + "header_coverage": "Head", + "reasons": ["Acute stroke/TIA evaluation.", "Head trauma.", "Severe headache/new neuro deficit."], + "prep": "Remove metal; pregnancy screen PRN.", + "prior_auth": { "requires_prior_auth": false }, + "tags": ["neuro", "emergency"] + }, + { + "id": "CTA-HEAD-NECK", + "modality": "CT", + "study_name": "CTA Head & Neck (Stroke/Aneurysm)", + "cpt": ["70496", "70498"], + "icd10_common": ["I65.23", "I67.1", "I63.9", "G45.9"], + "contrast": "With (arterial bolus)", + "header_coverage": "Head & Neck arteries", + "reasons": ["Large-vessel occlusion/aneurysm/stenosis evaluation."], + "prep": "Creatinine per policy; 18–20G IV.", + "prior_auth": { "requires_prior_auth": true }, + "tags": ["neuro", "vascular"] + }, + { + "id": "CT-PE-CTPA", + "modality": "CT", + "study_name": "CT Pulmonary Angiography (PE Protocol)", + "cpt": ["71275"], + "icd10_common": ["I26.99", "R06.02", "R07.9", "R79.1"], + "contrast": "With (arterial bolus)", + "header_coverage": "Chest (PE protocol)", + "reasons": ["Suspected pulmonary embolism with elevated clinical probability or D-dimer."], + "prep": "Check creatinine; allergy premedication if needed.", + "prior_auth": { "requires_prior_auth": true }, + "tags": ["chest", "vascular"] + }, + { + "id": "CT-ABD-PEL-W", + "modality": "CT", + "study_name": "CT Abdomen/Pelvis with Contrast", + "cpt": ["74177"], + "icd10_common": ["R10.9", "K56.60", "C18.9", "K57.30", "N20.0"], + "contrast": "With", + "header_coverage": "Abdomen & Pelvis", + "reasons": ["Acute abdomen pain/obstruction.", "Cancer staging.", "Suspected appendicitis/diverticulitis."], + "prep": "NPO 3–4 h; oral contrast per protocol.", + "prior_auth": { "requires_prior_auth": true }, + "tags": ["abdomen", "oncology"] + }, + { + "id": "CT-UROGRAM", + "modality": "CT", + "study_name": "CT Urogram (A/P w & w/o)", + "cpt": ["74178"], + "icd10_common": ["R31.9", "R31.0", "N28.89", "C67.9"], + "contrast": "With & Without", + "header_coverage": "Kidneys, ureters, bladder", + "reasons": ["Hematuria evaluation (risk-stratified).", "Suspected upper tract urothelial carcinoma."], + "prep": "Hydration; renal labs per policy.", + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true }, + "tags": ["gu"] + }, + { + "id": "MRI-BRAIN-WWO", + "modality": "MRI", + "study_name": "MRI Brain w & w/o Contrast", + "cpt": ["70553"], + "icd10_common": ["G35", "R56.9", "G45.9", "C71.9", "F03.90"], + "contrast": "With & Without", + "header_coverage": "Brain", + "reasons": ["Tumor, MS, seizures, subacute stroke, dementia workup."], + "prep": "Metal screen; GFR if CKD.", + "prior_auth": { "requires_prior_auth": true }, + "tags": ["neuro"] + }, + { + "id": "MRI-IAC", + "modality": "MRI", + "study_name": "MRI IAC / Temporal Bone", + "cpt": ["70553"], + "icd10_common": ["H91.90", "H93.19", "D33.3", "H81.399"], + "contrast": "With & Without", + "header_coverage": "IACs/CPA", + "reasons": ["Asymmetric SNHL, vestibular schwannoma, complex vertigo."], + "prep": "Standard MRI screening; GFR as needed.", + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true }, + "tags": ["neuro", "ent"] + }, + { + "id": "MRI-TMJ", + "modality": "MRI", + "study_name": "MRI Temporomandibular Joints", + "cpt": ["70336"], + "icd10_common": ["M26.609", "R68.84"], + "contrast": "Without (± With if tumor/infection)", + "header_coverage": "Bilateral TMJs with open/closed mouth", + "reasons": ["TMJ dysfunction, locking, pain refractory to conservative care."], + "prep": "Remove dental metal; bite blocks per protocol.", + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true, "step_imaging_required": true }, + "tags": ["msk", "ent", "dental"] + }, + { + "id": "MRI-BRACHIAL-PLEXUS", + "modality": "MRI", + "study_name": "MRI Brachial Plexus w & w/o", + "cpt": ["73223"], + "icd10_common": ["G54.0", "S14.3XXA", "C47.3"], + "contrast": "With & Without", + "header_coverage": "Roots to cords", + "reasons": ["Traction injury, tumor, radiation plexopathy."], + "prep": "Consider sedation if needed.", + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true }, + "tags": ["neuro", "msk"] + }, + { + "id": "MR-NEUROGRAPHY-LE", + "modality": "MRI", + "study_name": "MR Neurography — Lower Extremity", + "cpt": ["73720"], + "icd10_common": ["G57.00", "G57.10", "G57.90"], + "contrast": "Without (± With for mass/inflammation)", + "header_coverage": "Target nerve course", + "reasons": ["Entrapment/neuropathy after failed conservative therapy; traumatic nerve injury."], + "prep": "Standard MRI screening.", + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true, "step_imaging_required": true }, + "tags": ["neuro", "msk"] + }, + { + "id": "MRI-LSPINE-WWO", + "modality": "MRI", + "study_name": "MRI Lumbar Spine w & w/o", + "cpt": ["72158"], + "icd10_common": ["M54.16", "M48.061", "G95.9", "M51.36"], + "contrast": "With & Without", + "header_coverage": "Lumbar spine", + "reasons": ["Radiculopathy/stenosis with neuro deficits or failed 4–6 wks conservative therapy; infection; tumor."], + "prep": "Metal screen; GFR if CKD.", + "prior_auth": { "requires_prior_auth": true, "step_imaging_required": true }, + "tags": ["spine"] + }, + { + "id": "MRI-PROSTATE-PI-RADS", + "modality": "MRI", + "study_name": "MRI Prostate (PI-RADS) w & w/o", + "cpt": ["72197"], + "icd10_common": ["R97.20", "C61"], + "contrast": "With & Without", + "header_coverage": "Prostate & seminal vesicles", + "reasons": ["Elevated PSA/biochemical recurrence; staging prior to therapy."], + "prep": "NPO 3–4 h; rectal prep per site.", + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true }, + "tags": ["gu", "oncology"] + }, + { + "id": "US-ABD-COMP", + "modality": "Ultrasound", + "study_name": "Abdominal Ultrasound — Complete", + "cpt": ["76700"], + "icd10_common": ["R10.11", "K80.20", "K76.0", "K74.60"], + "contrast": "None", + "header_coverage": "Abdomen", + "reasons": ["RUQ pain/gallstones; liver disease; abnormal LFTs."], + "prep": "NPO 6–8 h.", + "prior_auth": { "requires_prior_auth": false }, + "tags": ["abdomen"] + }, + { + "id": "US-PELVIC-TV", + "modality": "Ultrasound", + "study_name": "Pelvic Ultrasound (Transabdominal + Transvaginal)", + "cpt": ["76856", "76830"], + "icd10_common": ["N93.9", "N83.209", "D25.9", "R10.2", "Z32.01"], + "contrast": "None", + "header_coverage": "Pelvis", + "reasons": ["AUB, pelvic pain, ovarian cyst, fibroids, early pregnancy viability."], + "prep": "Full bladder for TA; empty for TV; document LMP.", + "prior_auth": { "requires_prior_auth": false }, + "tags": ["gyn"] + }, + { + "id": "US-CAROTID", + "modality": "Ultrasound", + "study_name": "Carotid Duplex (Bilateral)", + "cpt": ["93880"], + "icd10_common": ["I65.23", "G45.9", "R09.89"], + "contrast": "None", + "header_coverage": "Carotid arteries", + "reasons": ["TIA/stroke symptoms, carotid bruit/stenosis follow-up."], + "prep": "None.", + "prior_auth": { "requires_prior_auth": false }, + "tags": ["vascular"] + }, + { + "id": "US-LE-VENOUS", + "modality": "Ultrasound", + "study_name": "Lower Extremity Venous Duplex (DVT) — Bilateral", + "cpt": ["93970"], + "icd10_common": ["I82.4Z3", "M79.89", "R22.40"], + "contrast": "None", + "header_coverage": "LE veins", + "reasons": ["Leg swelling/pain; rule out DVT."], + "prep": "None.", + "prior_auth": { "requires_prior_auth": false }, + "tags": ["vascular"] + }, + { + "id": "XR-CHEST-2V", + "modality": "X-Ray", + "study_name": "Chest X-ray (2 Views)", + "cpt": ["71046"], + "icd10_common": ["R05.9", "R06.02", "R07.9", "J18.9"], + "contrast": "None", + "header_coverage": "Chest", + "reasons": ["Cough/SOB/chest pain; pneumonia; trauma baseline."], + "prep": "None.", + "prior_auth": { "requires_prior_auth": false }, + "tags": ["chest"] + }, + { + "id": "XR-KUB", + "modality": "X-Ray", + "study_name": "Abdomen X-ray (KUB)", + "cpt": ["74018"], + "icd10_common": ["R10.9", "K59.00", "R14.0", "N20.0", "K56.60"], + "contrast": "None", + "header_coverage": "Abdomen", + "reasons": ["Obstruction/constipation/stone follow-up."], + "prep": "None.", + "prior_auth": { "requires_prior_auth": false }, + "tags": ["abdomen"] + }, + { + "id": "NM-HIDA", + "modality": "Nuclear Medicine", + "study_name": "HIDA (± Ejection Fraction)", + "cpt": ["78226", "78227"], + "icd10_common": ["K81.0", "K82.8", "R10.11"], + "contrast": "Radiotracer", + "header_coverage": "Hepatobiliary", + "reasons": ["Acute cholecystitis; biliary dyskinesia (with EF)."], + "prep": "NPO 4–6 h; withhold opiates 4–6 h.", + "prior_auth": { "requires_prior_auth": true }, + "tags": ["hepatobiliary"] + }, + { + "id": "NM-BONE-WB", + "modality": "Nuclear Medicine", + "study_name": "Bone Scan — Whole Body", + "cpt": ["78306", "78320"], + "icd10_common": ["C79.51", "M84.48XA", "M86.9"], + "contrast": "Radiotracer", + "header_coverage": "Whole skeleton", + "reasons": ["Bone metastases evaluation; occult fractures; osteomyelitis."], + "prep": "Hydration; frequent voiding.", + "prior_auth": { "requires_prior_auth": true }, + "tags": ["msk", "oncology"] + }, + { + "id": "IR-PARACENTESIS", + "modality": "IR", + "study_name": "US-Guided Paracentesis", + "cpt": ["49083"], + "icd10_common": ["R18.8", "K70.31", "K74.60"], + "contrast": "None", + "header_coverage": "Peritoneal cavity", + "reasons": ["Diagnostic or therapeutic removal of ascites."], + "prep": "Consent; INR/platelets; anticoagulant plan.", + "prior_auth": { "requires_prior_auth": false }, + "tags": ["procedure"] + }, + { + "id": "IR-LUNG-BIOPSY", + "modality": "IR", + "study_name": "CT-Guided Lung Biopsy", + "cpt": ["32408"], + "icd10_common": ["R91.1", "C34.90"], + "contrast": "None", + "header_coverage": "Target lung lesion", + "reasons": ["Tissue diagnosis of pulmonary nodule/mass."], + "prep": "Consent; INR/platelets; post-procedure CXR per policy.", + "prior_auth": { "requires_prior_auth": true, "specialist_referral_required": true }, + "tags": ["procedure", "oncology"] + } + ] +} + +[ + { + "modality": "PET/CT", + "region": "Skull base to mid-thigh", + "contexts": ["staging", "restaging", "treatment response", "surveillance"], + "keywords": ["lymphoma", "nsclc", "lung cancer", "breast cancer", "colorectal", "colon cancer", "melanoma", "head and neck", "hnscc", "gastric", "pancreatic"], + "header": "PET/CT Skull Base to Mid-Thigh", + "reasons": [ + "FDG PET/CT for {context} of {condition}; evaluate extent of disease, nodal involvement, and FDG-avid distant metastases." + ], + "prep_notes": [ + "Fast 4–6 hours; avoid strenuous exercise for 24 hours.", + "Check blood glucose per facility protocol; avoid recent high-dose steroids if possible." + ], + "supporting_docs": [ + "Recent clinic note documenting diagnosis and clinical question.", + "Prior imaging/report if available.", + "Therapy timeline (chemo/radiation/surgery) and relevant labs." + ], + "flags": [ + "Recent G-CSF can increase marrow uptake.", + "Hyperglycemia may reduce FDG tumor-to-background contrast." + ], + "tags": ["oncology-general"] + }, + { + "modality": "PET/CT", + "region": "Whole body", + "contexts": ["staging", "restaging", "surveillance"], + "keywords": ["melanoma", "myeloma", "sarcoma", "vasculitis", "fever of unknown origin", "fuo"], + "header": "PET/CT Whole Body", + "reasons": [ + "FDG PET/CT whole body for {context} of {condition}; evaluate for extra-axial/extremity involvement and FDG-avid metastatic or inflammatory disease." + ], + "prep_notes": [ + "Standard FDG fasting instructions.", + "Ensure patient warmth to limit brown fat uptake when possible." + ], + "supporting_docs": [ + "Referring note with suspicion/diagnosis.", + "Any biopsy/pathology available.", + "Prior imaging for correlation." + ], + "flags": [ + "Consider coverage of extremities for melanoma/myeloma.", + "Consider inflammatory patterns in vasculitis/FOU." + ], + "tags": ["whole-body"] + }, + { + "modality": "PET", + "region": "Brain", + "contexts": ["dementia", "epilepsy"], + "keywords": ["alzheim", "dementia", "frontotemporal", "ftd", "epilepsy", "seizure", "temporal lobe"], + "header": "PET Brain FDG", + "reasons": [ + "FDG brain PET to evaluate cerebral metabolic patterns in {condition}; correlate with clinical and prior imaging." + ], + "prep_notes": [ + "Quiet, dim environment pre-injection.", + "For epilepsy protocols, follow ictal/interictal timing per local procedure." + ], + "supporting_docs": [ + "Neurology note describing symptoms and clinical question.", + "Prior MRI/EEG as applicable." + ], + "flags": [ + "FDG patterns vary by dementia subtype.", + "Medication/timing can affect epilepsy localization." + ], + "tags": ["neuro"] + }, + { + "modality": "PET", + "region": "Cardiac", + "contexts": ["viability"], + "keywords": ["viability", "ischemic cardiomyopathy", "hibernating myocardium"], + "header": "PET Cardiac FDG Viability", + "reasons": [ + "FDG PET to assess myocardial viability in ischemic cardiomyopathy; correlate with perfusion and echocardiographic findings." + ], + "prep_notes": [ + "Cardiac viability glucose loading/insulin protocol per local SOP.", + "Coordinate with perfusion imaging if performed." + ], + "supporting_docs": [ + "Cardiology note with revascularization question.", + "Prior echo/perfusion/coronary imaging reports." + ], + "flags": [ + "Glycemic control critical for image quality.", + "Confirm compatibility with current therapies." + ], + "tags": ["cardiac"] + }, + { + "modality": "PET/CT", + "region": "Skull base to mid-thigh", + "contexts": ["suspected infection"], + "keywords": ["osteomyelitis", "prosthetic joint", "infection", "endocarditis", "fever of unknown origin", "fuo"], + "header": "PET/CT Skull Base to Mid-Thigh", + "reasons": [ + "FDG PET/CT to evaluate suspected infection/inflammation related to {condition}; assess extent of disease and potential sites of involvement." + ], + "prep_notes": [ + "Standard FDG fasting; review recent antibiotic therapy that may impact findings." + ], + "supporting_docs": [ + "Clinical notes with symptoms/duration.", + "Relevant labs (WBC, CRP/ESR), culture results if available.", + "Prior imaging for comparison." + ], + "flags": [ + "Device/prosthesis can show inflammatory uptake; interpret in clinical context.", + "Consider tailored coverage if peripheral involvement suspected." + ], + "tags": ["infection"] + } +] +>>>>>>> 6490a75 (fix(order-helper): normalize rules.json (dash mojibake, array/comma/braces), validate JSON) From 24aa3704cbbd62a6a0b12cb11ca6da3b20b1a8bf Mon Sep 17 00:00:00 2001 From: Lissan <150966211+DataForSolution@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:40:55 -0400 Subject: [PATCH 04/10] OH: robust app init (new rules loader + fallbacks); live preview always on --- order-helper/app.js | 963 ++++++++-------------------------------- order-helper/index.html | 357 +++++++++------ 2 files changed, 398 insertions(+), 922 deletions(-) diff --git a/order-helper/app.js b/order-helper/app.js index b1d427f..a57065f 100644 --- a/order-helper/app.js +++ b/order-helper/app.js @@ -1,836 +1,251 @@ - - -/* ===== OH loader shim (non-destructive) ===== */ -(() => { - // Wait for DOM if this script isn’t loaded with `defer` - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init, { once: true }); + // Run after parse + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); } else { init(); } - - function init() { - // Namespace (won't clobber your existing code) - window.OH = window.OH || {}; - const $ = (s) => document.querySelector(s); - const setText = (el, msg) => { if (el) el.textContent = msg; }; - - // Elements (adjust IDs only if yours differ) - const els = { - status: $('#status'), - modality: $('#modality'), - region: $('#region'), - bodyPart: $('#bodyPart'), - contrast: $('#contrast'), - laterality: $('#laterality'), - context: $('#context'), - }; - - // Use the exact meta path (you already include ?v=20250913) - const RULES_URL = - document.querySelector('meta[name="oh-rules-path"]')?.content || - '/order-helper/data/rules.json'; - - const FALLBACK = Object.freeze({ - schema_version: '1.1', - modalities: { - 'PET/CT': { - regions: ['Skull base to mid-thigh', 'Whole body'], - body_parts: ['Head/Neck', 'Chest', 'Abdomen/Pelvis'], - contrast: ['None'], - laterality: ['N/A'], - contexts: ['Staging','Restaging','Treatment response','Surveillance','Acute'], - }, - 'CT': { - regions: ['Head/Brain','Chest','Abdomen/Pelvis'], - body_parts: ['Head','Chest','Abdomen','Pelvis'], - contrast: ['None','IV','Oral','IV + Oral'], - laterality: ['N/A','Right','Left','Bilateral'], - contexts: ['Acute','Follow-up','Staging'], - }, - 'MRI': { - regions: ['Brain','Spine','MSK'], - body_parts: ['Brain','Cervical','Lumbar','Hip'], - contrast: ['None','Gadolinium'], - laterality: ['N/A','Right','Left','Bilateral'], - contexts: ['Acute','Follow-up','Staging'], - } - } - }); - - const validate = (cat) => !!(cat && typeof cat === 'object' && cat.modalities && typeof cat.modalities === 'object'); - - function setOptions(selectEl, items, placeholder) { - if (!selectEl) return; - const list = Array.isArray(items) ? items : []; - selectEl.innerHTML = ''; - const ph = document.createElement('option'); - ph.value = ''; - ph.textContent = placeholder || 'Select…'; - ph.disabled = true; ph.selected = true; - selectEl.appendChild(ph); - for (const v of list) { - const opt = document.createElement('option'); - opt.value = v; opt.textContent = v; - selectEl.appendChild(opt); - } - } - - function bindCascades(cat) { - if (!els.modality) return; - const modalities = Object.keys(cat.modalities || {}); - setOptions(els.modality, modalities, 'Select modality…'); - - els.modality.addEventListener('change', () => { - const m = els.modality.value; - const spec = (cat.modalities || {})[m] || {}; - setOptions(els.region, spec.regions, 'Select region…'); - setOptions(els.bodyPart, spec.body_parts, 'Select body part…'); - setOptions(els.contrast, spec.contrast, 'Select contrast…'); - setOptions(els.laterality, spec.laterality, 'Select laterality…'); - setOptions(els.context, spec.contexts, 'Select context…'); - }); - } - - async function loadCatalog() { - setText(els.status, 'Loading rules…'); - try { - const res = await fetch(RULES_URL, { cache: 'no-store' }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const json = await res.json(); - if (!validate(json)) throw new Error('Invalid rules schema'); - setText(els.status, 'Rules loaded.'); - return json; - } catch (err) { - console.warn('[OH] rules load failed, using fallback:', err); - setText(els.status, 'Using built-in defaults (rules.json unavailable).'); - return FALLBACK; - } - } - - // Expose for the rest of your app (non-breaking) - window.OH.loadCatalog = loadCatalog; - - // Boot: load then bind; announce readiness - loadCatalog().then(cat => { - window.OH.catalog = cat; - bindCascades(cat); - document.dispatchEvent(new CustomEvent('oh:catalog-ready', { detail: { catalog: cat } })); - }); - } })(); - diff --git a/order-helper/index.html b/order-helper/index.html index d3a7867..20d97a4 100644 --- a/order-helper/index.html +++ b/order-helper/index.html @@ -2,24 +2,26 @@ layout: default title: OraDigit Order Helper permalink: /order-helper/ -description: Evidence-based PET/CT, CT, MRI, US, X-Ray — structured order helper with validation and PDF export. No PHI. Educational use only. -oh_loader: true +description: Evidence-based PET/CT, CT, and MRI order helper with validation and export. No PHI. Educational use only. --- - - - + + + @@ -30,200 +32,259 @@ if (!s) return; try { const url = document.querySelector('meta[name="oh-rules-path"]')?.content; - if (!url) { s.textContent = 'Missing oh-rules-path meta.'; s.className = 'oh-status error'; return; } + if (!url) { s.textContent = 'Missing oh-rules-path meta.'; s.className = 'status error'; return; } s.textContent = 'Boot check… fetching rules…'; const r = await fetch(url, { cache: 'no-store' }); - if (r.ok) { s.textContent = 'Boot check OK. Loading app…'; s.className = 'oh-status success'; } - else { s.textContent = `Rules fetch failed (${r.status}). Check path/case: ` + url; s.className = 'oh-status error'; } + if (r.ok) { s.textContent = 'Boot check OK. Loading app…'; s.className = 'status success'; } + else { s.textContent = `Rules fetch failed (${r.status}). Check path/case: ` + url; s.className = 'status error'; } } catch (e) { s.textContent = 'Rules fetch error: ' + e.message; - s.className = 'oh-status error'; + s.className = 'status error'; } }); -
-
+
+

OraDigit Order Helper

-

- Structure payer-ready imaging orders. Select modality and context; we’ll generate a professional - clinical indication, show prep/flags, and export a PDF. All client-side. No PHI. +

+ Structure clear, payer-ready imaging orders. Select modality and context; we’ll generate + a professional clinical indication and surface prep/flags. All client-side. No PHI.

-
Initializing…
- -
-
+
+ + -
-
+ -
+
- Regions, body parts, contrast, and contexts adapt to your selection. -
- - -
-
- - -
-
- - -
+ Regions, contexts, and conditions adapt to your selection.
- -
-
- - -
-
- - -
+ +
+ +
- -
-
- - - e.g., Staging, Restaging, Treatment response, Surveillance, Acute -
-
- - -
-
+ +
+ - -
-
- - - -
-
- - - -
-
+ +
- -
- - -
+ + - -
- - + Click one or more (e.g., staging, restaging, surveillance, acute, follow-up).
- -
-
- - -
-
- - -
+ +
+ + + + Start typing to see common conditions for the chosen modality.
-
- - -
+ + -
- - + +
+ +
- -
- - - - - +
+ + + +
- -
- + + + - - +<<<<<<< HEAD + + + + + - - - +======= + + +>>>>>>> 47d41e0 (OH: switch to data/ (case fix); update index + app (live preview & loader)) From b40b0bd8adf06502e9171ee07bda0d3364e74753 Mon Sep 17 00:00:00 2001 From: Lissan <150966211+DataForSolution@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:24:13 -0400 Subject: [PATCH 05/10] =?UTF-8?q?OH:=20rebuild=20UI=E2=80=94populate=20reg?= =?UTF-8?q?ions/contexts/conditions;=20robust=20rules=20loader=20+=20live?= =?UTF-8?q?=20preview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- order-helper/app.js | 413 ++++++++++++++++++++++---------------------- 1 file changed, 204 insertions(+), 209 deletions(-) diff --git a/order-helper/app.js b/order-helper/app.js index a57065f..0eebf3b 100644 --- a/order-helper/app.js +++ b/order-helper/app.js @@ -1,251 +1,246 @@ -/* OraDigit Order Helper – robust init + live preview - - Reads rules URL from - - Works if rules.json is missing or has a different shape - - Populates Region, Context chips, Condition suggestions - - Keeps the right-side Order Preview live -*/ - (() => { - const $ = (s) => document.querySelector(s); - - const FALLBACK = { - CONTEXTS: [ - "Staging", - "Restaging", - "Treatment response", - "Surveillance", - "Suspected recurrence", - "Infection / inflammation", - "Viability", - ], - REGIONS: { - "PET/CT": ["Whole body", "Head & neck", "Thorax/Chest", "Abdomen/Pelvis", "Brain"], - CT: ["Head", "Neck", "Chest", "Abdomen", "Pelvis", "Abdomen/Pelvis", "Angio/PE", "Sinus", "Spine", "Extremity"], - MRI: ["Brain", "C-spine", "T-spine", "L-spine", "Shoulder", "Knee", "Hip", "Abdomen", "Pelvis", "Prostate", "Cardiac"], - }, - CONDITIONS: { - CT: ["Renal colic", "Pulmonary embolism", "Appendicitis", "Diverticulitis", "Kidney stone"], - MRI: ["Multiple sclerosis", "Lumbar radiculopathy", "Rotator cuff tear", "Meniscal tear", "Hip labral tear"], - "PET/CT": ["NSCLC", "Lymphoma", "Breast cancer", "Colorectal cancer", "Melanoma"], - }, + const qs = (s) => document.querySelector(s); + const setStatus = (msg, cls = 'status success') => { + const s = qs('#status'); if (s) { s.textContent = msg; s.className = cls; } }; - function setStatus(msg, kind = "success") { - const s = $("#status"); - if (!s) return; - s.textContent = msg; - s.className = `status ${kind}`; - } + // Catch sync + async errors in one place + window.addEventListener('error', e => setStatus('JavaScript error: ' + (e.message || 'Unknown'), 'status error')); + window.addEventListener('unhandledrejection', e => setStatus('App error: ' + (e.reason?.message || e.reason || 'Unknown'), 'status error')); - function rulesURL() { - const meta = document.querySelector('meta[name="oh-rules-path"]'); - const url = meta?.content?.trim(); - return url || "/order-helper/data/rules.json"; - } + document.addEventListener('DOMContentLoaded', init); - async function loadRules() { + async function init() { try { - const r = await fetch(rulesURL(), { cache: "no-store" }); - if (!r.ok) throw new Error(`HTTP ${r.status}`); - const json = await r.json(); - setStatus("Rules loaded.", "success"); - return json; + // 1) Load rules.json from the meta tag (case-safe) + const rulesPath = document.querySelector('meta[name="oh-rules-path"]')?.content; + let rules = null; + try { + const r = await fetch(rulesPath, { cache: 'no-store' }); + if (r.ok) rules = await r.json(); + } catch (_) { /* fall through to defaults */ } + if (!rules) { rules = defaultRules(); setStatus('Using built-in defaults (rules.json not found).', 'status warn'); } + + // 2) Normalize rules into a simple catalog + const catalog = normalizeRules(rules); + + // 3) Build the UI + wire events + buildUI(catalog); + + setStatus('Rules loaded.'); } catch (e) { - setStatus("Using built-in defaults (rules not available).", "warn"); - return null; + setStatus('Init failed: ' + e.message, 'status error'); } } - // Try multiple likely shapes, return array or null - function listFromRules(rules, modality, key) { - if (!rules) return null; + // ---------- Rules handling ---------- + function defaultRules() { + return { + modalities: { + "PET/CT": { + regions: ["Skull base to mid-thigh","Whole body","Brain","Head/Neck","Chest","Abdomen/Pelvis"], + contexts: ["Staging","Restaging","Treatment response","Surveillance","Suspected recurrence","Infection / inflammation","Viability"], + conditions: ["NSCLC","Lymphoma","Colorectal cancer","Melanoma","Head & neck cancer"] + }, + "CT": { + regions: ["Head","Neck","Chest","Abdomen","Pelvis","Abdomen/Pelvis","Angio chest (PE)"], + contexts: ["Acute","Chronic","Follow-up"], + conditions: ["Renal colic","PE","Appendicitis","Pancreatitis"] + }, + "MRI": { + regions: ["Brain","Cervical spine","Thoracic spine","Lumbar spine","Abdomen","Pelvis"], + contexts: ["Acute","Follow-up","Problem solving"], + conditions: ["MS","Seizure","Stroke","Back pain","Prostate cancer"] + } + } + }; + } + + function normalizeRules(r) { + // Accept either {modalities:{...}} or top-level { "PET/CT": {...}, ... } + const src = r.modalities ? r.modalities : { "PET/CT": r["PET/CT"], "CT": r["CT"], "MRI": r["MRI"] }; + const out = { modalities: {} }; + + for (const [mod, spec] of Object.entries(src || {})) { + if (!spec) continue; + const regions = Array.isArray(spec.regions) ? spec.regions + : Array.isArray(spec?.regions?.list) ? spec.regions.list + : Object.keys(spec.regions || {}); + const contexts = spec.contexts || spec.context || ["Staging","Restaging","Treatment response","Surveillance","Suspected recurrence"]; + const conditions = spec.conditions || spec.condition || []; + out.modalities[mod] = { + regions: [...new Set(regions)].filter(Boolean), + contexts: [...new Set(contexts)].filter(Boolean), + conditions: [...new Set(conditions)].filter(Boolean), + }; + } + return out; + } - // Common shapes we support: - // 1) { modalities: { "CT": { regions:[...], contexts:[...], conditions:[...] } } } - if (rules.modalities?.[modality]?.[key]) return rules.modalities[modality][key]; + // ---------- UI wiring ---------- + function buildUI(cat) { + const modalitySel = qs('#modality'); + const regionSel = qs('#region'); + const ctxChips = qs('#contextChips'); + const ctxSelect = qs('#context'); // hidden + * - CT contrast auto-suggestions + * - Indication builder + basic study suggestions + */ + +(function () { + "use strict"; + + // -------- Config / Rules path -------- + const RULES_URL = + document.querySelector('meta[name="oh-rules-path"]')?.content || + "./data/rules.json"; + + // -------- Elements -------- + const els = { + status: document.getElementById("status"), + form: document.getElementById("orderForm"), + modality: document.getElementById("modality"), + region: document.getElementById("region"), + context: document.getElementById("context"), // hidden mirror select (optional) + contextChips: document.getElementById("contextChips"), // primary UI + condition: document.getElementById("condition"), + conditionList: document.getElementById("conditionList"), + indication: document.getElementById("indication"), + contrastGroup: document.getElementById("contrastGroup"), + oral: document.getElementById("oralContrast"), + + // Results area (optional on page) + outHeader: document.getElementById("outHeader"), + outReason: document.getElementById("outReason"), + outPrep: document.getElementById("outPrep"), + outDocs: document.getElementById("outDocs"), + outFlags: document.getElementById("outFlags"), + results: document.getElementById("results"), + copyReasonBtn: document.getElementById("copyReasonBtn"), + copyAllBtn: document.getElementById("copyAllBtn"), + printBtn: document.getElementById("printBtn"), + suggestions: document.getElementById("suggestions"), + errMsg: document.getElementById("errMsg"), + dbg: document.getElementById("dbg"), + }; + + // -------- Fallbacks (so UI still works if rules.json fails/empty) -------- + const FALLBACK_RULES = { + modalities: { + "PET/CT": { + regions: [ + "Skull base to mid-thigh", + "Whole body", + "Head/Neck", + "Chest", + "Abdomen/Pelvis", + "Cardiac viability" + ], + contexts: [ + "Staging","Restaging","Treatment response","Surveillance","Suspected recurrence","Infection / inflammation","Viability" + ], + conditions: [ + "DLBCL","Hodgkin lymphoma","NSCLC","Melanoma","Colorectal cancer", + "Head and neck SCC","Fever of unknown origin","Cardiac viability" + ], + indication_templates: [ + "FDG PET/CT {region} – {context} for {condition}", + "FDG PET/CT {region} – evaluate {condition}", + "FDG PET/CT {region}{contrast_text} – {context} ({condition})" + ] + }, + CT: { + regions: [ + "Head/Brain","Sinuses","Maxillofacial/Facial Bones","Temporal Bones/IAC","Neck","Chest", + "Low-Dose Lung CT (Screening)","Abdomen","Pelvis","Abdomen/Pelvis","CT Urogram","CT Enterography", + "Spine – Cervical","Spine – Thoracic","Spine – Lumbar","Upper Extremity","Lower Extremity", + "Cardiac Coronary CTA","Angiography – Head/Neck CTA","Angiography – Chest CTA (PE)", + "Angiography – Aorta CTA","Angiography – Run-off CTA (LE)" + ], + contexts: [ + "Staging","Restaging","Treatment response","Surveillance","Initial evaluation","Acute symptoms", + "Follow-up","Pre-operative planning","Post-operative complication","Trauma","Screening","Infection / inflammation" + ], + conditions: [ + "Headache (sudden / thunderclap)","Head trauma","Stroke symptoms / TIA","Sinusitis","Neck mass", + "Pulmonary embolism suspected","Aortic dissection / aneurysm suspected","Lung nodule","Pneumonia complication", + "Abdominal pain RLQ (appendicitis)","Kidney stone / renal colic","Pancreatitis","Liver lesion characterization", + "Diverticulitis","Inflammatory bowel disease flare","Bowel obstruction","Post-op abdomen","Hematuria", + "Cancer staging (specify primary)","Metastatic disease restaging","Spine trauma","Cervical radiculopathy", + "Spinal stenosis","Extremity fracture","Suspected osteomyelitis","Peripheral arterial disease (LE run-off)" + ], + indication_templates: [ + "CT {region} – {context} for {condition}", + "CT {region} – rule out {condition}", + "CT {region}{contrast_text} – {context} ({condition})" + ], + contrast_recommendations: [ + { match:["kidney stone","renal colic"], suggest:"without_iv" }, + { match:["appendicitis","rlq"], suggest:"with_iv" }, + { match:["pe","pulmonary embolism"], suggest:"with_iv" }, + { match:["aortic","dissection","aneurysm"], suggest:"with_iv" }, + { match:["liver lesion","pancreatitis"], suggest:"with_iv" }, + { match:["bowel obstruction"], suggest:"without_iv" }, + { match:["trauma"], suggest:"with_iv" }, + { match:["low-dose lung ct","screening"], suggest:"without_iv" } + ] + } + }, + records: [] // keep empty; site-specific rules.json will populate + }; document.addEventListener('DOMContentLoaded', init); + async function init() { try { // 1) Load rules.json from the meta tag (case-safe) @@ -35,6 +146,155 @@ // ---------- Rules handling ---------- function defaultRules() { + + // -------- Utils -------- + function setStatus(msg, level = "info") { + if (!els.status) return; + els.status.textContent = msg; + els.status.className = + "status " + + (level === "ok" || level === "success" + ? "success" + : level === "warn" + ? "warn" + : level === "error" + ? "error" + : ""); + } + + const titleCase = (s) => + (s || "").replace(/\w\S*/g, (t) => t.charAt(0).toUpperCase() + t.slice(1)); + + function fillSelect(selectEl, values, placeholder = "Select…") { + if (!selectEl) return; + selectEl.innerHTML = ""; + const ph = document.createElement("option"); + ph.value = ""; + ph.textContent = placeholder; + selectEl.appendChild(ph); + (values || []).forEach((v) => { + const opt = document.createElement("option"); + opt.value = v; + opt.textContent = v; + selectEl.appendChild(opt); + }); + } + + function fillDatalist(datalistEl, items) { + if (!datalistEl) return; + datalistEl.innerHTML = ""; + (items || []).forEach((v) => { + const opt = document.createElement("option"); + opt.value = v; + datalistEl.appendChild(opt); + }); + } + + function showContrast(show) { + if (!els.contrastGroup) return; + els.contrastGroup.classList.toggle("hidden", !show); + if (!show) { + const checked = + els.contrastGroup.querySelector('input[type=radio]:checked'); + if (checked) checked.checked = false; + if (els.oral) els.oral.checked = false; + } + } + + function contrastTextFromForm() { + if (!els.contrastGroup || els.contrastGroup.classList.contains("hidden")) + return ""; + const radio = els.contrastGroup.querySelector('input[type=radio]:checked'); + const oral = els.oral?.checked ? " + oral contrast" : ""; + if (!radio) return oral ? "(" + oral.trim() + ")" : ""; + if (radio.value === "with_iv") return "(with IV contrast" + oral + ")"; + if (radio.value === "without_iv") return "(without IV contrast" + oral + ")"; + return oral ? "(" + oral.trim() + ")" : ""; + } + + // -------- Chips helpers (with keyboard support) -------- + function renderContextChips(contexts) { + if (!els.contextChips) return; + els.contextChips.innerHTML = ""; + (contexts || []).forEach((label) => { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "oh-chip"; + btn.textContent = label; + btn.setAttribute("aria-pressed", "false"); + btn.setAttribute("aria-label", `Toggle context ${label}`); + btn.setAttribute("tabindex", "0"); + els.contextChips.appendChild(btn); + }); + } + + function getSelectedContextsFromChips() { + if (!els.contextChips) return []; + return Array.from( + els.contextChips.querySelectorAll('.oh-chip[aria-pressed="true"]') + ) + .map((el) => (el.textContent || "").trim()) + .filter(Boolean); + } + + function mirrorChipsToHiddenSelect() { + if (!els.context) return; + const selected = getSelectedContextsFromChips(); + els.context.innerHTML = ""; + selected.forEach((label) => { + const opt = document.createElement("option"); + opt.value = label; + opt.textContent = label; + opt.selected = true; + els.context.appendChild(opt); + }); + } + + // -------- Rules loading -------- + function looksLikeRules(obj) { + // Minimal sanity check + if (!obj || typeof obj !== "object") return false; + // accept either {modalities:{}} or at least records:[] + const hasModalities = + obj.modalities && typeof obj.modalities === "object"; + const hasRecords = Array.isArray(obj.records); + return hasModalities || hasRecords; + } + + async function loadRules() { + try { + const res = await fetch(RULES_URL, { cache: "no-store" }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const json = await res.json(); + if (!looksLikeRules(json)) throw new Error("Invalid rules schema"); + RULES = json; + setStatus("Rules loaded.", "success"); + } catch (e) { + console.warn("Failed to load rules.json, using fallback", e); + RULES = FALLBACK_RULES; + setStatus( + "Using built-in fallback rules (could not fetch rules.json).", + "warn" + ); + } + } + + function getModalityNode(modality) { + return RULES?.modalities?.[modality] || null; + } + + // If no modalities entry, derive contexts/regions from records (best effort) + function deriveFromRecords(modality) { + const recs = (RULES?.records || []).filter((r) => + (r.modality || "").toUpperCase().includes(modality.toUpperCase()) + ); + const setC = new Set(); + const setR = new Set(); + recs.forEach((r) => { + (r.contexts || []).forEach((c) => setC.add(titleCase(c))); + if (r.header_coverage) setR.add(r.header_coverage); + }); +>>>>>>> d058311 (Update Order Helper: fix context preview and improve app.js rules handling) return { modalities: { "PET/CT": { @@ -56,6 +316,7 @@ }; } + function normalizeRules(r) { // Accept either {modalities:{...}} or top-level { "PET/CT": {...}, ... } const src = r.modalities ? r.modalities : { "PET/CT": r["PET/CT"], "CT": r["CT"], "MRI": r["MRI"] }; @@ -89,20 +350,28 @@ const clear = (el) => { while (el && el.firstChild) el.removeChild(el.firstChild); }; const makeOpt = (v, t = v) => { const o = document.createElement('option'); o.value = v; o.textContent = t; return o; }; +======= + // -------- Populate UI for modality -------- + function populateForModality(modality) { + const node = getModalityNode(modality) || deriveFromRecords(modality); + fillSelect(els.region, node.regions || [], "Select region…"); function renderForMod(mod) { const spec = cat.modalities[mod] || { regions: [], contexts: [], conditions: [] }; + // Regions clear(regionSel); regionSel.append(makeOpt('', 'Select region…')); (spec.regions || []).forEach(r => regionSel.append(makeOpt(r))); + fillDatalist(els.conditionList, node.conditions || []); // Context chips + hidden select clear(ctxChips); clear(ctxSelect); (spec.contexts || []).forEach(label => { const opt = makeOpt(label); opt.selected = false; ctxSelect.append(opt); + const b = document.createElement('button'); b.type = 'button'; b.className = 'chip'; @@ -116,6 +385,153 @@ syncPreview(); }); ctxChips.append(b); + + // Reset textual fields + if (els.condition) els.condition.value = ""; + if (els.indication) els.indication.value = ""; + + // Ask any external preview sync to re-render + try { + document.dispatchEvent(new Event("input", { bubbles: true })); + } catch {} + } + + // -------- Contrast suggestions for CT -------- + function suggestContrastIfCT(modalityNode, conditionText, regionText) { + if (!modalityNode?.contrast_recommendations) return; + const text = `${conditionText || ""} ${regionText || ""}`.toLowerCase(); + for (const rule of modalityNode.contrast_recommendations) { + const allMatch = rule.match.every((token) => text.includes(token)); + if (allMatch) { + const target = els.contrastGroup?.querySelector( + `input[type=radio][value="${rule.suggest}"]` + ); + if (target) target.checked = true; + break; + } + } + // Guardrail: any CTA should be with IV + if ((regionText || "").toLowerCase().includes("cta")) { + const withIV = els.contrastGroup?.querySelector( + 'input[type=radio][value="with_iv"]' + ); + if (withIV) withIV.checked = true; + } + } + + // -------- Indication builder -------- + function buildIndication(modalityNode, modality) { + if (!els.indication) return; + const region = els.region?.value || ""; + const contexts = els.contextChips + ? getSelectedContextsFromChips().join(", ") + : (() => { + const sel = els.context; + return sel + ? [...sel.selectedOptions].map((o) => o.textContent.trim()).join(", ") + : ""; + })(); + const condition = els.condition?.value || ""; + const contrast_text = contrastTextFromForm(); // e.g., "(with IV contrast + oral contrast)" + const templates = + modalityNode?.indication_templates || + (modality === "CT" + ? ["CT {region} – {context} for {condition}"] + : modality === "PET/CT" + ? ["FDG PET/CT {region} – {context} for {condition}"] + : ["{region} – {context} for {condition}"]); + // Prefer a template that includes {contrast_text} if present + const t = + (contrast_text && templates.find((x) => x.includes("{contrast_text}"))) || + templates[0]; + + const out = t + .replace("{region}", region) + .replace("{context}", contexts) + .replace("{condition}", condition) + .replace("{contrast_text}", contrast_text ? ` ${contrast_text}` : ""); + els.indication.value = out.trim(); + } + + // -------- Basic record matcher (suggest studies) -------- + function scoreRecord(rec, modality, region, contexts, condition) { + if (!(rec.modality || "").toUpperCase().includes(modality.toUpperCase())) + return -1; + let s = 0; + if ( + rec.header_coverage && + region && + rec.header_coverage.toLowerCase().includes(region.toLowerCase()) + ) + s += 2; + (rec.contexts || []).forEach((c) => { + if (contexts.some((ctx) => ctx.toLowerCase() === (c || "").toLowerCase())) + s += 2; + }); + (rec.keywords || []).forEach((k) => { + if (condition && condition.toLowerCase().includes((k || "").toLowerCase())) + s += 2; + }); + if ( + (rec.tags || []).includes("oncology-general") && + condition && + /c\d\d|malig|tumor|cancer/i.test(condition) + ) + s += 1; + return s; + } + + function suggestStudies(modality, region, contexts, condition) { + if (!els.suggestions) return; + const recs = RULES?.records || []; + const scored = recs + .map((r) => ({ r, s: scoreRecord(r, modality, region, contexts, condition) })) + .filter((x) => x.s >= 0) + .sort((a, b) => b.s - a.s) + .slice(0, 5); + + els.suggestions.innerHTML = ""; + if (!scored.length) { + const li = document.createElement("li"); + li.className = "muted"; + li.textContent = "No specific suggestions. Adjust context/condition."; + els.suggestions.appendChild(li); + return; + } + + scored.forEach(({ r }) => { + const li = document.createElement("li"); + const cpts = (r.cpt || []).join(", "); + li.innerHTML = `${r.study_name || r.header_coverage || "Suggested study"} ${cpts ? "[" + cpts + "]" : ""}`; + li.title = (r.reasons || [])[0] || ""; + els.suggestions.appendChild(li); + }); + } + + // -------- Results panel fill -------- + function fillResults(topRec, contextStr, conditionStr) { + if (!els.results || !topRec) return; + const header = + topRec.study_name || topRec.header_coverage || "Suggested Study"; + if (els.outHeader) + els.outHeader.textContent = `${header} — CPT: ${(topRec.cpt || []).join( + ", " + )}`; + + if (els.outReason) { + const tmpl = (topRec.reasons || [])[0] || "{context} {condition}"; + els.outReason.value = tmpl + .replace("{context}", contextStr || "") + .replace("{condition}", conditionStr || ""); + } + + function fillUL(ul, arr) { + if (!ul) return; + ul.innerHTML = ""; + (arr || []).forEach((t) => { + const li = document.createElement("li"); + li.textContent = t; + ul.appendChild(li); }); // Conditions datalist @@ -144,6 +560,7 @@ document.querySelectorAll('input[name="contrast"],#oralContrast').forEach(i => i.addEventListener('change', syncPreview)); qs('#indication')?.addEventListener('input', syncPreview); + // Suggest Order → computes a simple recommendation & fills results qs('#orderForm')?.addEventListener('submit', e => { e.preventDefault(); @@ -160,6 +577,138 @@ // First render renderForMod(modalitySel.value || 'PET/CT'); + + // -------- Event wiring -------- + function wireEvents() { + // Modality change -> repopulate + els.modality?.addEventListener("change", () => { + const modality = els.modality.value; + populateForModality(modality); + const node = getModalityNode(modality) || (FALLBACK_RULES.modalities[modality] || null); + buildIndication(node, modality); + }); + + // Region / Condition input -> suggest contrast if CT and rebuild indication + ["change", "input"].forEach((evt) => { + els.region?.addEventListener(evt, () => { + if (els.modality?.value === "CT") { + const node = getModalityNode("CT") || FALLBACK_RULES.modalities.CT; + suggestContrastIfCT(node, els.condition?.value, els.region?.value); + } + buildIndication(getModalityNode(els.modality?.value) || (FALLBACK_RULES.modalities[els.modality?.value] || null), els.modality?.value); + }); + els.condition?.addEventListener(evt, () => { + if (els.modality?.value === "CT") { + const node = getModalityNode("CT") || FALLBACK_RULES.modalities.CT; + suggestContrastIfCT(node, els.condition?.value, els.region?.value); + } + buildIndication(getModalityNode(els.modality?.value) || (FALLBACK_RULES.modalities[els.modality?.value] || null), els.modality?.value); + }); + }); + + // Contrast change -> rebuild indication + els.contrastGroup?.addEventListener("change", () => { + if (els.modality?.value === "CT") { + buildIndication( + getModalityNode("CT") || FALLBACK_RULES.modalities.CT, + "CT" + ); + } + }); + + // Chips: click + keyboard toggle + document.addEventListener("click", (e) => { + const chip = e.target.closest("#contextChips .oh-chip"); + if (!chip) return; + const cur = chip.getAttribute("aria-pressed") === "true"; + chip.setAttribute("aria-pressed", cur ? "false" : "true"); + mirrorChipsToHiddenSelect(); + buildIndication(getModalityNode(els.modality?.value) || (FALLBACK_RULES.modalities[els.modality?.value] || null), els.modality?.value); + }); + document.addEventListener("keydown", (e) => { + const chip = e.target.closest("#contextChips .oh-chip"); + if (!chip) return; + if (e.key === " " || e.key === "Enter") { + e.preventDefault(); + chip.click(); + } + }); + + // Form submit -> suggest order + fill results + els.form?.addEventListener("submit", (e) => { + e.preventDefault(); + const modality = els.modality?.value || ""; + const region = els.region?.value || ""; + const contexts = els.contextChips + ? getSelectedContextsFromChips() + : els.context + ? [...els.context.selectedOptions].map((o) => o.value) + : []; + const condition = els.condition?.value || ""; + + // Suggestions + suggestStudies(modality, region, contexts, condition); + + // Fill results with top hit if available + const recs = RULES?.records || []; + const ranked = recs + .map((r) => ({ r, s: scoreRecord(r, modality, region, contexts, condition) })) + .filter((x) => x.s >= 0) + .sort((a, b) => b.s - a.s); + fillResults(ranked[0]?.r, contexts.join(", "), condition); + + // Status message + setStatus("Order suggested below. Review, copy, or print.", "success"); + }); + + // Copy buttons + els.copyReasonBtn?.addEventListener("click", async () => { + const v = els.outReason?.value?.trim(); + if (!v) return; + try { + await navigator.clipboard.writeText(v); + setStatus("Reason copied to clipboard.", "success"); + } catch { + setStatus("Unable to copy reason. Select and copy manually.", "warn"); + } + }); + + els.copyAllBtn?.addEventListener("click", async () => { + const parts = []; + if (els.outHeader) parts.push(els.outHeader.textContent); + if (els.outReason?.value) parts.push("Reason: " + els.outReason.value); + if (els.outPrep?.children?.length) + parts.push( + "Prep: " + + Array.from(els.outPrep.children) + .map((li) => li.textContent) + .join("; ") + ); + if (els.outDocs?.children?.length) + parts.push( + "Docs: " + + Array.from(els.outDocs.children) + .map((li) => li.textContent) + .join("; ") + ); + if (els.outFlags?.children?.length) + parts.push( + "Flags: " + + Array.from(els.outFlags.children) + .map((li) => li.textContent) + .join("; ") + ); + const text = parts.join("\n"); + if (!text.trim()) return; + try { + await navigator.clipboard.writeText(text); + setStatus("All details copied to clipboard.", "success"); + } catch { + setStatus("Unable to copy. Select and copy manually.", "warn"); + } + }); + + els.printBtn?.addEventListener("click", () => window.print()); } function selectedContexts() { @@ -182,6 +731,7 @@ if (oral) txt += ' + oral'; $('#pv-contrast').textContent = txt; } else { + $('#pv-contrast').textContent = '—'; } @@ -242,5 +792,134 @@ outReason.value = indication; qs('#indication').value = indication; syncPreview(); + } + + const node = + getModalityNode(current) || (FALLBACK_RULES.modalities[current] || null); + buildIndication(node, current); + } + + if (els.dbg) + els.dbg.textContent = `[OH] Ready (${new Date().toLocaleString()})`; + })(); +})(); + +/* ===== OH loader shim (non-destructive) ===== */ +(() => { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { once: true }); + } else { + init(); + } + + function init() { + window.OH = window.OH || {}; + const $ = (s) => document.querySelector(s); + const setText = (el, msg) => { if (el) el.textContent = msg; }; + + const els = { + status: $('#status'), + modality: $('#modality'), + region: $('#region'), + bodyPart: $('#bodyPart'), + contrast: $('#contrast'), + laterality: $('#laterality'), + context: $('#context'), + }; + + const RULES_URL = + document.querySelector('meta[name="oh-rules-path"]')?.content || + '/order-helper/data/rules.json'; + + const FALLBACK = Object.freeze({ + schema_version: '1.1', + modalities: { + 'PET/CT': { + regions: ['Skull base to mid-thigh', 'Whole body'], + body_parts: ['Head/Neck', 'Chest', 'Abdomen/Pelvis'], + contrast: ['None'], + laterality: ['N/A'], + contexts: ['Staging','Restaging','Treatment response','Surveillance','Acute'], + }, + 'CT': { + regions: ['Head/Brain','Chest','Abdomen/Pelvis'], + body_parts: ['Head','Chest','Abdomen','Pelvis'], + contrast: ['None','IV','Oral','IV + Oral'], + laterality: ['N/A','Right','Left','Bilateral'], + contexts: ['Acute','Follow-up','Staging'], + }, + 'MRI': { + regions: ['Brain','Spine','MSK'], + body_parts: ['Brain','Cervical','Lumbar','Hip'], + contrast: ['None','Gadolinium'], + laterality: ['N/A','Right','Left','Bilateral'], + contexts: ['Acute','Follow-up','Staging'], + } + } + }); + + const validate = (cat) => !!(cat && typeof cat === 'object' && cat.modalities && typeof cat.modalities === 'object'); + + function setOptions(selectEl, items, placeholder) { + if (!selectEl) return; + const list = Array.isArray(items) ? items : []; + selectEl.innerHTML = ''; + const ph = document.createElement('option'); + ph.value = ''; + ph.textContent = placeholder || 'Select…'; + ph.disabled = true; ph.selected = true; + selectEl.appendChild(ph); + for (const v of list) { + const opt = document.createElement('option'); + opt.value = v; opt.textContent = v; + selectEl.appendChild(opt); + } + } + + function bindCascades(cat) { + if (!els.modality) return; + const modalities = Object.keys(cat.modalities || {}); + setOptions(els.modality, modalities, 'Select modality…'); + + els.modality.addEventListener('change', () => { + const m = els.modality.value; + const spec = (cat.modalities || {})[m] || {}; + setOptions(els.region, spec.regions, 'Select region…'); + setOptions(els.bodyPart, spec.body_parts, 'Select body part…'); + setOptions(els.contrast, spec.contrast, 'Select contrast…'); + setOptions(els.laterality, spec.laterality, 'Select laterality…'); + setOptions(els.context, spec.contexts, 'Select context…'); + }); + + // optional: auto-select the first modality to avoid empty UI + if (els.modality.options.length > 1) { + els.modality.selectedIndex = 1; + els.modality.dispatchEvent(new Event('change')); + } + } + + async function loadCatalog() { + setText(els.status, 'Loading rules…'); + try { + const res = await fetch(RULES_URL, { cache: 'no-store' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const json = await res.json(); + if (!validate(json)) throw new Error('Invalid rules schema'); + setText(els.status, 'Rules loaded.'); + return json; + } catch (err) { + console.warn('[OH] rules load failed, using fallback:', err); + setText(els.status, 'Using built-in defaults (rules.json unavailable).'); + return FALLBACK; + } + } + + window.OH.loadCatalog = loadCatalog; + + loadCatalog().then(cat => { + window.OH.catalog = cat; + bindCascades(cat); + document.dispatchEvent(new CustomEvent('oh:catalog-ready', { detail: { catalog: cat } })); + }); } })(); diff --git a/order-helper/index.html b/order-helper/index.html index 20d97a4..5e45f97 100644 --- a/order-helper/index.html +++ b/order-helper/index.html @@ -234,6 +234,7 @@

ICD-10 Suggestions

// Keep preview in sync with the form & chips const syncPreview = () => { +<<<<<<< HEAD const $s = (sel) => document.querySelector(sel); $s('#pv-modality').textContent = $s('#modality')?.value || '—'; $s('#pv-region').textContent = $s('#region')?.value || '—'; @@ -243,6 +244,32 @@

ICD-10 Suggestions

$s('#pv-context').textContent = ctxOpt.length ? ctxOpt.join(', ') : '—'; $s('#pv-condition').textContent = $s('#condition')?.value || '—'; +======= + $('#pv-modality').textContent = $('#modality')?.value || '—'; + $('#pv-region').textContent = $('#region')?.value || '—'; + + // NEW: show all selected contexts + { const sel = document.getElementById('context'); + const ctx = sel ? [...sel.selectedOptions].map(o => o.textContent.trim()).filter(Boolean) : []; + document.getElementById('pv-context').textContent = ctx.length ? ctx.join(', ') : '—'; + } + + $('#pv-condition').textContent = $('#condition')?.value || '—'; + + const grp = document.getElementById('contrastGroup'); + if (grp && !grp.classList.contains('hidden')) { + const r = grp.querySelector('input[type=radio]:checked'); + const oral = document.getElementById('oralContrast')?.checked; + let txt = r ? (r.value === 'with_iv' ? 'With IV contrast' : 'Without IV contrast') : '—'; + if (oral) txt += ' + oral'; + document.getElementById('pv-contrast').textContent = txt; + } else { + document.getElementById('pv-contrast').textContent = '—'; + } + + document.getElementById('pv-indication').textContent = document.getElementById('indication')?.value?.trim() || '—'; +}; +>>>>>>> d058311 (Update Order Helper: fix context preview and improve app.js rules handling) const grp = document.getElementById('contrastGroup'); if (grp && !grp.classList.contains('hidden')) { From dd968a6cc5a42cb083329089eab423e5d6239580 Mon Sep 17 00:00:00 2001 From: Lissan <150966211+DataForSolution@users.noreply.github.com> Date: Fri, 5 Sep 2025 20:15:11 -0400 Subject: [PATCH 07/10] Order Helper: unify app.js (rules auto-merge + ICD-10) and align with updated index --- order-helper/app.js | 189 ++++++++++++++++++++++++++++++++++++++++ order-helper/index.html | 13 +++ 2 files changed, 202 insertions(+) diff --git a/order-helper/app.js b/order-helper/app.js index b9ff0e8..b546324 100644 --- a/order-helper/app.js +++ b/order-helper/app.js @@ -322,6 +322,7 @@ const src = r.modalities ? r.modalities : { "PET/CT": r["PET/CT"], "CT": r["CT"], "MRI": r["MRI"] }; const out = { modalities: {} }; +<<<<<<< HEAD for (const [mod, spec] of Object.entries(src || {})) { if (!spec) continue; const regions = Array.isArray(spec.regions) ? spec.regions @@ -334,6 +335,194 @@ contexts: [...new Set(contexts)].filter(Boolean), conditions: [...new Set(conditions)].filter(Boolean), }; +======= + fillSelect(els.region, node.regions || [], "Select region…"); + + if (els.contextChips) { + renderContextChips(node.contexts || []); + mirrorChipsToHiddenSelect(); + } else { + fillSelect(els.context, node.contexts || [], "Select context…"); + } + + fillDatalist(els.conditionList, node.conditions || []); + + // Contrast only for CT + showContrast(modality === "CT"); + + // Reset textual fields + if (els.condition) els.condition.value = ""; + if (els.indication) els.indication.value = ""; + + // Ask any external preview sync to re-render + try { document.dispatchEvent(new Event("input", { bubbles: true })); } catch {} + } + + // -------- Contrast suggestions for CT -------- + function suggestContrastIfCT(modalityNode, conditionText, regionText) { + if (!modalityNode?.contrast_recommendations) return; + const text = `${conditionText || ""} ${regionText || ""}`.toLowerCase(); + for (const rule of modalityNode.contrast_recommendations) { + const allMatch = rule.match.every((token) => text.includes(token)); + if (allMatch) { + const target = els.contrastGroup?.querySelector( + `input[type=radio][value="${rule.suggest}"]` + ); + if (target) target.checked = true; + break; + } + } + // Guardrail: any CTA should be with IV + if ((regionText || "").toLowerCase().includes("cta")) { + const withIV = els.contrastGroup?.querySelector( + 'input[type=radio][value="with_iv"]' + ); + if (withIV) withIV.checked = true; + } + } + + // -------- Indication builder -------- + function buildIndication(modalityNode, modality) { + if (!els.indication) return; + const region = els.region?.value || ""; + const contexts = els.contextChips + ? getSelectedContextsFromChips().join(", ") + : (() => { + const sel = els.context; + return sel ? [...sel.selectedOptions].map((o) => o.textContent.trim()).join(", ") : ""; + })(); + const condition = els.condition?.value || ""; + const contrast_text = contrastTextFromForm(); + const templates = + modalityNode?.indication_templates || + (modality === "CT" + ? ["CT {region} – {context} for {condition}"] + : modality === "PET/CT" + ? ["FDG PET/CT {region} – {context} for {condition}"] + : ["{region} – {context} for {condition}"]); + const t = + (contrast_text && templates.find((x) => x.includes("{contrast_text}"))) || + templates[0]; + + const out = t + .replace("{region}", region) + .replace("{context}", contexts) + .replace("{condition}", condition) + .replace("{contrast_text}", contrast_text ? ` ${contrast_text}` : ""); + els.indication.value = out.trim(); + } + + // -------- Basic record matcher (suggest studies) -------- + function scoreRecord(rec, modality, region, contexts, condition) { + if (!(rec.modality || "").toUpperCase().includes(modality.toUpperCase())) + return -1; + let s = 0; + if ( + rec.header_coverage && + region && + rec.header_coverage.toLowerCase().includes(region.toLowerCase()) + ) s += 2; + (rec.contexts || []).forEach((c) => { + if (contexts.some((ctx) => ctx.toLowerCase() === (c || "").toLowerCase())) + s += 2; + }); + (rec.keywords || []).forEach((k) => { + if (condition && condition.toLowerCase().includes((k || "").toLowerCase())) + s += 2; + }); + if ( + (rec.tags || []).includes("oncology-general") && + condition && /c\d\d|malig|tumor|cancer/i.test(condition) + ) s += 1; + return s; + } + + function suggestStudies(modality, region, contexts, condition) { + if (!els.suggestions) return; + const recs = RULES?.records || []; + const scored = recs + .map((r) => ({ r, s: scoreRecord(r, modality, region, contexts, condition) })) + .filter((x) => x.s >= 0) + .sort((a, b) => b.s - a.s) + .slice(0, 5); + + els.suggestions.innerHTML = ""; + if (!scored.length) { + const li = document.createElement("li"); + li.className = "muted"; + li.textContent = "No specific suggestions. Adjust context/condition."; + els.suggestions.appendChild(li); + return; + } + + scored.forEach(({ r }) => { + const li = document.createElement("li"); + const cpts = (r.cpt || []).join(", "); + li.innerHTML = `${r.study_name || r.header_coverage || "Suggested study"} ${cpts ? "[" + cpts + "]" : ""}`; + li.title = (r.reasons || [])[0] || ""; + els.suggestions.appendChild(li); + }); + } + + // -------- ICD-10 suggestions (lightweight helper) -------- + // NOTE: Verify final billing codes per ICD-10-CM and payer policy. + const ICD_RULES = [ + // PET/CT oncology + { tokens: ["dlbcl","lymphoma","hodgkin","nhl"], codes: [ + { code:"C83.30", label:"Diffuse large B-cell lymphoma, unspecified site" }, + { code:"C81.90", label:"Hodgkin lymphoma, unspecified, unspecified site" } + ]}, + { tokens: ["nsclc","lung cancer","pulmonary nodule","lung"], codes: [ + { code:"C34.90", label:"Malignant neoplasm of unspecified part of unspecified lung" }, + { code:"R91.1", label:"Solitary pulmonary nodule" } + ]}, + { tokens: ["melanoma"], codes: [{ code:"C43.9", label:"Malignant melanoma of skin, unspecified" }]}, + { tokens: ["colorectal","colon cancer"], codes: [{ code:"C18.9", label:"Malignant neoplasm of colon, unspecified" }]}, + { tokens: ["hnscc","head and neck"], codes: [{ code:"C76.0", label:"Malignant neoplasm of head, face and neck" }]}, + { tokens: ["fever of unknown origin","fuo"], codes: [{ code:"R50.9", label:"Fever, unspecified" }]}, + { tokens: ["viability","ischemic cardiomyopathy","myocardial"], codes: [{ code:"I25.5", label:"Ischemic cardiomyopathy" }]}, + + // CT common + { tokens: ["pulmonary embolism","pe"], codes: [{ code:"I26.99", label:"Other pulmonary embolism without acute cor pulmonale" }]}, + { tokens: ["appendicitis","rlq"], codes: [{ code:"K35.80", label:"Unspecified acute appendicitis" }]}, + { tokens: ["renal colic","kidney stone","flank pain","hematuria"], codes: [ + { code:"N20.0", label:"Calculus of kidney" }, + { code:"N23", label:"Unspecified renal colic" } + ]}, + { tokens: ["pneumonia"], codes: [{ code:"J18.9", label:"Pneumonia, unspecified organism" }]}, + + // MRI neuro + { tokens: ["stroke","cerebral infarction"], codes: [{ code:"I63.9", label:"Cerebral infarction, unspecified" }]}, + { tokens: ["tia"], codes: [{ code:"G45.9", label:"Transient cerebral ischemic attack, unspecified" }]}, + { tokens: ["intracranial hemorrhage","ich"], codes: [{ code:"I62.9", label:"Nontraumatic intracranial hemorrhage, unspecified" }]}, + + // MRI spine + { tokens: ["cervical radiculopathy"], codes: [{ code:"M54.12", label:"Radiculopathy, cervical region" }]}, + { tokens: ["lumbar radiculopathy","sciatica"], codes: [{ code:"M54.16", label:"Radiculopathy, lumbar region" }]}, + { tokens: ["spinal stenosis","stenosis"], codes: [{ code:"M48.061", label:"Spinal stenosis, lumbar region w/o neurogenic claudication" }]}, + { tokens: ["disc herniation"], codes: [{ code:"M51.26", label:"Other intervertebral disc displacement, lumbar region" }]}, + + // Ortho + { tokens: ["meniscal tear"], codes: [{ code:"S83.209A", label:"Tear of unsp meniscus, unsp knee, initial encounter" }]}, + { tokens: ["acl tear"], codes: [{ code:"S83.511A", label:"Sprain of ACL of right knee, initial encounter" }]}, + { tokens: ["rotator cuff"], codes: [{ code:"M75.100", label:"Unspecified rotator cuff tear or rupture, not specified as traumatic" }]} + ]; + + function suggestICD10(text) { + const t = (text || "").toLowerCase(); + const out = []; + const seen = new Set(); + for (const rule of ICD_RULES) { + if (rule.tokens.some(tok => t.includes(tok))) { + for (const c of rule.codes) { + if (!seen.has(c.code)) { + out.push(c); + seen.add(c.code); + } + } + } + if (out.length >= 6) break; // keep tidy +>>>>>>> 1a5762e (Order Helper: unify app.js (rules auto-merge + ICD-10) and align with updated index) } return out; } diff --git a/order-helper/index.html b/order-helper/index.html index 5e45f97..ef7a073 100644 --- a/order-helper/index.html +++ b/order-helper/index.html @@ -15,13 +15,19 @@ window.addEventListener('error', e => { const s = document.getElementById('status'); <<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 1a5762e (Order Helper: unify app.js (rules auto-merge + ICD-10) and align with updated index) if (s) { s.textContent = 'JavaScript error: ' + (e.message || 'Unknown'); s.className = 'status error'; } +<<<<<<< HEAD ======= if (s) { s.textContent = 'JavaScript error: ' + (e.message || 'Unknown'); s.className = 'status error'; } >>>>>>> 47d41e0 (OH: switch to data/ (case fix); update index + app (live preview & loader)) +======= +>>>>>>> 1a5762e (Order Helper: unify app.js (rules auto-merge + ICD-10) and align with updated index) }); @@ -226,6 +232,9 @@

ICD-10 Suggestions

<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> 1a5762e (Order Helper: unify app.js (rules auto-merge + ICD-10) and align with updated index) +<<<<<<< HEAD ======= >>>>>>> 47d41e0 (OH: switch to data/ (case fix); update index + app (live preview & loader)) +======= + +>>>>>>> 1a5762e (Order Helper: unify app.js (rules auto-merge + ICD-10) and align with updated index) From 44584d6197e94cfa2ee75a8a61a4a56d81a93f36 Mon Sep 17 00:00:00 2001 From: Lissan <150966211+DataForSolution@users.noreply.github.com> Date: Tue, 23 Sep 2025 08:39:50 -0400 Subject: [PATCH 08/10] Fix: hook app.js to dropdowns; ensure rules.json loads on boot --- order-helper/index.html | 75 ++++++++++------------------------------- 1 file changed, 18 insertions(+), 57 deletions(-) diff --git a/order-helper/index.html b/order-helper/index.html index ef7a073..a4b0dad 100644 --- a/order-helper/index.html +++ b/order-helper/index.html @@ -7,42 +7,38 @@ - - - - + + +

OraDigit Order Helper

@@ -71,7 +70,7 @@

OraDigit Order Helper

- + `r`n Regions, contexts, and conditions adapt to your selection. @@ -231,10 +230,7 @@

ICD-10 Suggestions

}); -<<<<<<< HEAD -<<<<<<< HEAD -======= ->>>>>>> 1a5762e (Order Helper: unify app.js (rules auto-merge + ICD-10) and align with updated index) + -<<<<<<< HEAD -======= - - ->>>>>>> 47d41e0 (OH: switch to data/ (case fix); update index + app (live preview & loader)) -======= - ->>>>>>> 1a5762e (Order Helper: unify app.js (rules auto-merge + ICD-10) and align with updated index) From a6d03c049bd849f7bf098cea9afc75949a4178a2 Mon Sep 17 00:00:00 2001 From: Lissan <150966211+DataForSolution@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:20:00 -0400 Subject: [PATCH 09/10] Fix: cleaned up index.html with resolved conflicts and valid rules hook --- order-helper/index.html | 222 ++++++---------------------------------- 1 file changed, 34 insertions(+), 188 deletions(-) diff --git a/order-helper/index.html b/order-helper/index.html index a4b0dad..7499db8 100644 --- a/order-helper/index.html +++ b/order-helper/index.html @@ -6,29 +6,31 @@ --- + + - + + - - -
-
-

OraDigit Order Helper

-

- Structure clear, payer-ready imaging orders. Select modality and context; we’ll generate - a professional clinical indication and surface prep/flags. All client-side. No PHI. -

-
- -
- -
- -
- -
- - - Regions, contexts, and conditions adapt to your selection. -
- - -
- - -
- - -
- - - -
- - - - - Click one or more (e.g., staging, restaging, surveillance, acute, follow-up). -
- - -
- - - - Start typing to see common conditions for the chosen modality. -
- - - - - -
- - -
- -
- - - -
-
-
- - - -
- - - - - - - - - -
- - + - + - - + - + - From 460b4edfce28f75928eb0e0d75acfd81e42e8bdf Mon Sep 17 00:00:00 2001 From: Lissan <150966211+DataForSolution@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:26:33 -0400 Subject: [PATCH 10/10] Resolve merge conflicts: cleaned index.html with correct rules hook --- order-helper/index.html | 153 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 3 deletions(-) diff --git a/order-helper/index.html b/order-helper/index.html index 7499db8..203b4b0 100644 --- a/order-helper/index.html +++ b/order-helper/index.html @@ -21,16 +21,19 @@ }); + - - +
+
+

OraDigit Order Helper

+

+ Structure clear, payer-ready imaging orders. Select modality and context; we’ll generate + a professional clinical indication and surface prep/flags. All client-side. No PHI. +

+
+ +
+ +
+ +
+ +
+ + + Regions, contexts, and conditions adapt to your selection. +
+ + +
+ + +
+ + +
+ +
+ + Click one or more (e.g., staging, restaging, surveillance, acute, follow-up). +
+ + +
+ + + + Start typing to see common conditions for the chosen modality. +
+ + + + + +
+ + +
+ +
+ + + +
+
+
+ + + +
+ + + + + + + + +
- +