/** * TGICS patient questionnaire — stable codes + branching rules. */ const opt = (value, label) => ({ value, label }); const QUESTIONS = { gender: { code: 'gender', label: 'Gender', type: 'single', required: true, options: [ opt('male', 'Male'), opt('female', 'Female'), opt('prefer_not_to_say', 'Prefer not to say'), ], }, patient_age: { code: 'patient_age', label: 'Age (in completed years)', type: 'number', required: true, min: 0, max: 120, }, exposure_history: { code: 'exposure_history', label: 'History of exposure?', type: 'multi', required: true, options: [ opt('never_smoker', 'Never smoker'), opt('ex_smoker', 'Ex-smoker'), opt('current_smoker', 'Current smoker'), opt('pulmonary_tb_contact', 'Suffered or ever came in contact with a patient who had Pulmonary TB'), opt('indoor_air_pollution', 'Current exposure to any sources of Indoor Air Pollution (eg: Incense Sticks, Mosquito Coil, Loban, Dhoop, Smoke from Tandoor/ cooking, Smoker in the family, fungus on walls (grey-black patches on wall))'), ], }, occupation_exposure: { code: 'occupation_exposure', label: 'Work/occupation exposed to', type: 'multi', required: true, options: [ opt('smoke', 'Smoke'), opt('fumes', 'Fumes'), opt('dust', 'Dust'), opt('gases', 'Gases'), opt('none', 'None'), ], }, family_history: { code: 'family_history', label: 'Family history', type: 'multi', required: true, options: [ opt('allergic_dermatitis_rhinitis_migraine', 'Allergic Dermatitis, Allergic Rhinitis, Migraine'), opt('asthma', 'Asthma'), opt('gerd', 'GERD (Gastroesophageal Reflux Disease)'), opt('tb', 'TB'), opt('rheumatic', 'Rheumatic diseases (arthritis, heart disease)'), opt('cardiovascular', 'Cardiovascular diseases'), opt('no_specific', 'No specific'), ], }, past_disease_history: { code: 'past_disease_history', label: 'Past disease history of patient', type: 'multi', required: true, options: [ opt('allergic_dermatitis_rhinitis_migraine', 'Allergic Dermatitis, Allergic Rhinitis, Migraine'), opt('asthma', 'Asthma'), opt('copd', 'COPD'), opt('tb', 'TB'), opt('sinus_issues', 'Sinus issues (Post Nasal Drip)'), opt('gerd', 'GERD (Gastroesophageal Reflux Disease)'), opt('rheumatic', 'Rheumatic diseases (arthritis, heart disease)'), opt('other_lung_disease', 'Any other lung disease'), opt('no_history', 'No history of any disease'), ], }, organ_system: { code: 'organ_system', label: 'What organ/system is involved for which the patient is visiting the doctor today?', type: 'multi', required: true, options: [ opt('skin', 'Skin'), opt('brain_spinal', 'Brain & spinal cord'), opt('ent', 'Ear/Nose/Throat'), opt('heart', 'Heart disease'), opt('lungs', 'Lungs & breathing tubes'), opt('abdomen', 'Abdomen'), opt('kidney', 'Kidney'), opt('pregnancy_reproductive', 'Pregnancy & reproductive issues'), opt('muscles_bones_joints', 'Muscles/Bones/Joints'), ], }, ailment: { code: 'ailment', label: 'For which ailment is the patient visiting the Doctor today?', type: 'multi', required: true, options: [ opt('fever', 'Fever'), opt('high_bp', 'High blood pressure'), opt('diabetes', 'Diabetes'), opt('anaemia', 'Anaemia'), opt('stomach_upset', 'Stomach upset (vomiting / loose motions)'), opt('cold_throat', 'Cold (runny nose) &/or throat irritation/pain'), opt('thyroid', 'Thyroid'), opt('rashes', 'Rashes (any where)'), opt('swelling', 'Swelling on feet / body'), opt('breathlessness', 'Breathlessness'), opt('backache_weakness', 'Back ache / Weakness / Joint pain / Body ache'), opt('foreign_body', 'Foreign body in eye/ear/nose/throat'), opt('physical_injury', 'Physical injury'), opt('others', 'Others (please specify briefly)'), ], otherField: 'ailment_other', }, has_cough: { code: 'has_cough', label: 'Does the patient complain of cough?', type: 'single', required: true, options: [opt('yes', 'Yes'), opt('no', 'No')], }, cough_duration: { code: 'cough_duration', label: 'What is the duration of cough?', type: 'single', required: true, options: [ opt('lt_7_days', 'Less than 7 days'), opt('7_days_to_3_weeks', '7 days to <3 weeks'), opt('3_weeks_to_2_months', '3 weeks to 2 months'), opt('gt_2_months', 'More than 2 months'), ], }, cough_type: { code: 'cough_type', label: 'Is the cough dry or wet (does the patient expel out sputum/mucus/phlegm during coughing)?', type: 'single', required: true, options: [ opt('dry', 'Dry'), opt('wet', 'Wet (with sputum/mucus/phlegm)'), ], }, sputum_quantity: { code: 'sputum_quantity', label: 'How much is the quantity of sputum/mucus/phlegm?', type: 'single', required: true, options: [ opt('scanty', 'Scanty (very less sputum/mucus/phlegm production/expulsion even after coughing)'), opt('moderate', 'Moderate (some amount of sputum/mucus/phlegm production/expulsion after coughing)'), opt('excess', 'Excess (too much sputum/mucus/phlegm production/expulsion even with or without cough)'), opt('sometimes_excess_sometimes_less', 'Sometimes excess and sometimes less'), opt('more_early_morning', 'More in early morning'), ], }, sputum_colour: { code: 'sputum_colour', label: 'What is the colour of the sputum/mucus/phlegm?', type: 'single', required: true, options: [ opt('white', 'White'), opt('yellow', 'Yellow'), opt('green', 'Green'), opt('brownish', 'Brownish'), opt('pink', 'Pink'), opt('blood_tinged', 'Blood tinged'), ], }, cough_associated: { code: 'cough_associated', label: 'Is cough associated with?', type: 'multi', required: true, options: [ opt('fever', 'Fever'), opt('weight_loss', 'Weight loss'), opt('loss_of_appetite', 'Loss of appetite'), opt('evening_rise_fever', 'Evening rise of fever'), opt('difficulty_breathing', 'Difficulty in breathing'), opt('wheeze', 'Whistling sound from chest/ wheeze'), opt('chest_pain', 'Chest pain'), opt('hoarseness', 'Hoarseness of voice'), opt('sinusitis', 'Sinusitis (Post Nasal Drip)'), opt('acid_reflux', 'Acid Reflux/Regurgitation'), opt('night_sweats', 'Night sweats'), opt('others', 'Others (please specify)'), opt('none', 'None of the above'), ], otherField: 'cough_associated_other', }, cough_diagnosis: { code: 'cough_diagnosis', label: 'What is the diagnosis for cough?', type: 'single', required: true, options: [ opt('common_cold', 'Common cold'), opt('sinusitis', 'Sinusitis'), opt('allergic_cough', 'Allergic Cough'), opt('allergic_rhinitis', 'Allergic Rhinitis'), opt('asthma', 'Asthma'), opt('drug_induced', 'Drug induced cough'), opt('acidity_gerd', 'Acidity induced cough (GERD)'), opt('post_nasal_drip', 'Post Nasal Drip (Upper Airway Cough Syndrome)'), opt('tb', 'TB'), opt('copd', 'COPD'), opt('others', 'Others (please specify)'), opt('not_sure', 'Not sure of diagnosis'), ], otherField: 'cough_diagnosis_other', }, }; function optionValues(question) { return new Set(question.options.map((o) => o.value)); } function validateAnswers(answers) { if (!answers || typeof answers !== 'object') { return { ok: false, message: 'Answers are required.' }; } const requireSingle = (code) => { const q = QUESTIONS[code]; const val = answers[code]; if (val === undefined || val === null || val === '') { return `${q.label} is required.`; } if (!optionValues(q).has(String(val))) { return `Invalid value for ${q.label}.`; } return null; }; const requireMulti = (code) => { const q = QUESTIONS[code]; const val = answers[code]; if (!Array.isArray(val) || val.length === 0) { return `${q.label} is required.`; } const allowed = optionValues(q); for (const v of val) { if (!allowed.has(String(v))) { return `Invalid value for ${q.label}.`; } } return null; }; const requireOther = (code) => { const q = QUESTIONS[code]; if (!q.otherField) return null; const val = answers[code]; const selected = Array.isArray(val) ? val : [val]; if (selected.includes('others')) { const other = String(answers[q.otherField] || '').trim(); if (!other) { return `Please specify details for ${q.label}.`; } } return null; }; let err = requireSingle('gender') || null; if (err) return { ok: false, message: err }; const age = Number(answers.patient_age); if (!Number.isFinite(age) || age < 0 || age > 120) { return { ok: false, message: 'Valid patient age is required (0–120).' }; } for (const code of [ 'exposure_history', 'occupation_exposure', 'family_history', 'past_disease_history', 'organ_system', 'ailment', ]) { err = requireMulti(code) || requireOther(code); if (err) return { ok: false, message: err }; } err = requireSingle('has_cough'); if (err) return { ok: false, message: err }; const hasCough = String(answers.has_cough) === 'yes'; if (!hasCough) { return { ok: true, hasCough: false }; } err = requireSingle('cough_duration') || requireSingle('cough_type'); if (err) return { ok: false, message: err }; const isWet = String(answers.cough_type) === 'wet'; if (isWet) { err = requireSingle('sputum_quantity') || requireSingle('sputum_colour'); if (err) return { ok: false, message: err }; } err = requireMulti('cough_associated') || requireOther('cough_associated') || requireSingle('cough_diagnosis') || requireOther('cough_diagnosis'); if (err) return { ok: false, message: err }; return { ok: true, hasCough: true }; } module.exports = { QUESTIONS, validateAnswers, };