TGICS/backend/src/questionnaire.js

333 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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,
};