diff --git a/app/Controllers/Api/v1/NursingApi.php b/app/Controllers/Api/v1/NursingApi.php index d8615c8..bc4d509 100644 --- a/app/Controllers/Api/v1/NursingApi.php +++ b/app/Controllers/Api/v1/NursingApi.php @@ -728,7 +728,7 @@ class NursingApi extends BaseController // Check Today OPD appointment $opdRow = $db->table('opd_master o') - ->select('o.opd_id, o.opd_code, o.opd_no as token_no, o.doc_id, o.doc_name as doctor_name, o.apointment_date, o.opd_book_date, pr.temp, pr.pulse, pr.bp, pr.diastolic, pr.spo2, pr.weight, pr.height, pr.rr_min') + ->select('o.opd_id, o.opd_code, o.opd_no as token_no, o.doc_id, o.doc_name as doctor_name, o.apointment_date, o.opd_book_date, pr.temp, pr.pulse, pr.bp, pr.diastolic, pr.spo2, pr.weight, pr.height, pr.rr_min, pr.glucose, pr.waist') ->join('opd_prescription pr', 'pr.opd_id = o.opd_id', 'left') ->where('o.p_id', $pId) ->groupStart() @@ -795,6 +795,38 @@ class NursingApi extends BaseController $bmi = round($weightKg / ($heightM * $heightM), 2); } + // Anthropometry & Body Measurements + $waistCm = isset($post['waist_circumference_cm']) && $post['waist_circumference_cm'] !== '' ? (float) $post['waist_circumference_cm'] : null; + $hipCm = isset($post['hip_circumference_cm']) && $post['hip_circumference_cm'] !== '' ? (float) $post['hip_circumference_cm'] : null; + $waistHipRatio = null; + if ($waistCm && $waistCm > 0 && $hipCm && $hipCm > 0) { + $waistHipRatio = round($waistCm / $hipCm, 2); + } elseif (isset($post['waist_hip_ratio']) && $post['waist_hip_ratio'] !== '') { + $waistHipRatio = (float) $post['waist_hip_ratio']; + } + + // Blood Glucose & POC Lab tests + $sugarRandom = isset($post['sugar_random']) && $post['sugar_random'] !== '' ? (float) $post['sugar_random'] : null; + $sugarFasting = isset($post['sugar_fasting']) && $post['sugar_fasting'] !== '' ? (float) $post['sugar_fasting'] : null; + $sugarPp = isset($post['sugar_pp']) && $post['sugar_pp'] !== '' ? (float) $post['sugar_pp'] : null; + $hba1c = isset($post['hba1c']) && $post['hba1c'] !== '' ? (float) $post['hba1c'] : null; + $hemoglobin = isset($post['hemoglobin']) && $post['hemoglobin'] !== '' ? (float) $post['hemoglobin'] : null; + + // Pain score & Physical Activity + $painScore = isset($post['pain_score']) && $post['pain_score'] !== '' ? (int) $post['pain_score'] : null; + $dailySteps = isset($post['daily_steps']) && $post['daily_steps'] !== '' ? (int) $post['daily_steps'] : null; + $sleepHours = isset($post['sleep_hours']) && $post['sleep_hours'] !== '' ? (float) $post['sleep_hours'] : null; + $exerciseMin = isset($post['exercise_min_per_day']) && $post['exercise_min_per_day'] !== '' ? (int) $post['exercise_min_per_day'] : null; + + // Lifestyle & Social History + $dietType = ! empty($post['diet_type']) ? trim((string) $post['diet_type']) : null; + $tobaccoStatus = ! empty($post['tobacco_status']) ? trim((string) $post['tobacco_status']) : null; + $alcoholStatus = ! empty($post['alcohol_status']) ? trim((string) $post['alcohol_status']) : null; + + // Women's Health + $womenLmp = ! empty($post['women_lmp']) ? trim((string) $post['women_lmp']) : null; + $womenPregnancy = ! empty($post['women_pregnancy_status']) ? trim((string) $post['women_pregnancy_status']) : null; + $wellnessData = [ 'patient_id' => $patientId, 'uhid' => $patient['p_code'] ?? '', @@ -811,6 +843,23 @@ class NursingApi extends BaseController 'weight_kg' => $weightKg, 'height_cm' => $heightCm, 'bmi' => $bmi, + 'sugar_random' => $sugarRandom, + 'sugar_fasting' => $sugarFasting, + 'sugar_pp' => $sugarPp, + 'hba1c' => $hba1c, + 'hemoglobin' => $hemoglobin, + 'waist_circumference_cm' => $waistCm, + 'hip_circumference_cm' => $hipCm, + 'waist_hip_ratio' => $waistHipRatio, + 'daily_steps' => $dailySteps, + 'sleep_hours' => $sleepHours, + 'exercise_min_per_day' => $exerciseMin, + 'diet_type' => $dietType, + 'tobacco_status' => $tobaccoStatus, + 'alcohol_status' => $alcoholStatus, + 'women_lmp' => $womenLmp, + 'women_pregnancy_status' => $womenPregnancy, + 'pain_score' => $painScore, 'general_advice' => (string) ($post['general_advice'] ?? ''), 'diet_lifestyle_note' => (string) ($post['diet_lifestyle_note'] ?? ''), 'recorded_by' => (string) ($post['recorded_by'] ?? 'Staff Nurse'), @@ -838,6 +887,23 @@ class NursingApi extends BaseController `weight_kg` decimal(5,2) DEFAULT NULL, `height_cm` decimal(5,2) DEFAULT NULL, `bmi` decimal(5,2) DEFAULT NULL, + `sugar_random` decimal(6,2) DEFAULT NULL, + `sugar_fasting` decimal(6,2) DEFAULT NULL, + `sugar_pp` decimal(6,2) DEFAULT NULL, + `hba1c` decimal(4,2) DEFAULT NULL, + `hemoglobin` decimal(4,2) DEFAULT NULL, + `waist_circumference_cm` decimal(5,2) DEFAULT NULL, + `hip_circumference_cm` decimal(5,2) DEFAULT NULL, + `waist_hip_ratio` decimal(4,2) DEFAULT NULL, + `daily_steps` int DEFAULT NULL, + `sleep_hours` decimal(4,2) DEFAULT NULL, + `exercise_min_per_day` int DEFAULT NULL, + `diet_type` varchar(100) DEFAULT NULL, + `tobacco_status` varchar(100) DEFAULT NULL, + `alcohol_status` varchar(100) DEFAULT NULL, + `women_lmp` date DEFAULT NULL, + `women_pregnancy_status` varchar(100) DEFAULT NULL, + `pain_score` tinyint DEFAULT NULL, `general_advice` text, `diet_lifestyle_note` text, `recorded_by` varchar(120) DEFAULT 'Staff Nurse', @@ -851,6 +917,28 @@ class NursingApi extends BaseController KEY `idx_uhid` (`uhid`), KEY `idx_recorded_at` (`recorded_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + } else { + // Defensive check for newly added Option B columns + if (! $db->fieldExists('sugar_random', 'patient_wellness_records')) { + @$db->query("ALTER TABLE `patient_wellness_records` + ADD COLUMN `sugar_random` decimal(6,2) DEFAULT NULL AFTER `bmi`, + ADD COLUMN `sugar_fasting` decimal(6,2) DEFAULT NULL AFTER `sugar_random`, + ADD COLUMN `sugar_pp` decimal(6,2) DEFAULT NULL AFTER `sugar_fasting`, + ADD COLUMN `hba1c` decimal(4,2) DEFAULT NULL AFTER `sugar_pp`, + ADD COLUMN `hemoglobin` decimal(4,2) DEFAULT NULL AFTER `hba1c`, + ADD COLUMN `waist_circumference_cm` decimal(5,2) DEFAULT NULL AFTER `hemoglobin`, + ADD COLUMN `hip_circumference_cm` decimal(5,2) DEFAULT NULL AFTER `waist_circumference_cm`, + ADD COLUMN `waist_hip_ratio` decimal(4,2) DEFAULT NULL AFTER `hip_circumference_cm`, + ADD COLUMN `daily_steps` int DEFAULT NULL AFTER `waist_hip_ratio`, + ADD COLUMN `sleep_hours` decimal(4,2) DEFAULT NULL AFTER `daily_steps`, + ADD COLUMN `exercise_min_per_day` int DEFAULT NULL AFTER `sleep_hours`, + ADD COLUMN `diet_type` varchar(100) DEFAULT NULL AFTER `exercise_min_per_day`, + ADD COLUMN `tobacco_status` varchar(100) DEFAULT NULL AFTER `diet_type`, + ADD COLUMN `alcohol_status` varchar(100) DEFAULT NULL AFTER `tobacco_status`, + ADD COLUMN `women_lmp` date DEFAULT NULL AFTER `alcohol_status`, + ADD COLUMN `women_pregnancy_status` varchar(100) DEFAULT NULL AFTER `women_lmp`, + ADD COLUMN `pain_score` tinyint DEFAULT NULL AFTER `women_pregnancy_status`"); + } } $db->table('patient_wellness_records')->insert($wellnessData); @@ -877,6 +965,7 @@ class NursingApi extends BaseController } }; + // Standard Vital Signs $addVital('8480-6', 'Systolic blood pressure', $wellnessData['bp_systolic'], 'mmHg', 'mm[Hg]'); $addVital('8462-4', 'Diastolic blood pressure', $wellnessData['bp_diastolic'], 'mmHg', 'mm[Hg]'); $addVital('8867-4', 'Heart rate', $wellnessData['pulse_rate'], '/min', '/min'); @@ -886,8 +975,47 @@ class NursingApi extends BaseController $addVital('8310-5', 'Body temperature', $wellnessData['temperature_c'] ?? $wellnessData['temperature_f'], 'Cel', 'Cel'); $addVital('9279-1', 'Respiratory rate', $wellnessData['resp_rate'], '/min', '/min'); $addVital('59408-5', 'Oxygen saturation in Arterial blood by Pulse oximetry', $wellnessData['spo2'], '%', '%'); + $addVital('72514-3', 'Pain severity - 0-10 verbal numeric rating', $wellnessData['pain_score'], '{score}', '{score}'); + + // Body Measurements / Anthropometry + $addVital('56115-9', 'Waist circumference', $wellnessData['waist_circumference_cm'], 'cm', 'cm'); + $addVital('56114-2', 'Hip circumference', $wellnessData['hip_circumference_cm'], 'cm', 'cm'); + $addVital('8280-0', 'Waist to hip ratio', $wellnessData['waist_hip_ratio'], 'ratio', '{ratio}'); + + // Blood Sugar & POC Laboratory Measurements + $addVital('2339-0', 'Glucose [Mass/volume] in Blood', $wellnessData['sugar_random'], 'mg/dL', 'mg/dL'); + $addVital('1558-6', 'Fasting glucose [Mass/volume] in Blood', $wellnessData['sugar_fasting'], 'mg/dL', 'mg/dL'); + $addVital('1521-4', 'Glucose [Mass/volume] in Blood 2 hours post meal', $wellnessData['sugar_pp'], 'mg/dL', 'mg/dL'); + $addVital('4548-4', 'Hemoglobin A1c/Hemoglobin.total in Blood', $wellnessData['hba1c'], '%', '%'); + $addVital('718-7', 'Hemoglobin [Mass/volume] in Blood', $wellnessData['hemoglobin'], 'g/dL', 'g/dL'); + + // Physical Activity & Sleep + $addVital('55423-8', 'Number of steps in 24 hour Measured', $wellnessData['daily_steps'], '{steps}', '{steps}'); + $addVital('93832-4', 'Sleep duration', $wellnessData['sleep_hours'], 'h', 'h'); + $addVital('55411-3', 'Exercise duration', $wellnessData['exercise_min_per_day'], 'min/d', 'min/d'); $lifestyle = []; + if (! empty($wellnessData['diet_type'])) { + $lifestyle[] = [ + 'code' => '81663-7', + 'display' => 'Diet Type', + 'value' => $wellnessData['diet_type'], + ]; + } + if (! empty($wellnessData['tobacco_status'])) { + $lifestyle[] = [ + 'code' => '365981007', + 'display' => 'Tobacco Smoking Status', + 'value' => $wellnessData['tobacco_status'], + ]; + } + if (! empty($wellnessData['alcohol_status'])) { + $lifestyle[] = [ + 'code' => '228273003', + 'display' => 'Alcohol Consumption Status', + 'value' => $wellnessData['alcohol_status'], + ]; + } if (! empty($wellnessData['diet_lifestyle_note'])) { $lifestyle[] = [ 'code' => 'diet-lifestyle', @@ -903,6 +1031,14 @@ class NursingApi extends BaseController ]; } + $womenWellness = []; + if (! empty($wellnessData['women_lmp'])) { + $womenWellness['lmp'] = $wellnessData['women_lmp']; + } + if (! empty($wellnessData['women_pregnancy_status'])) { + $womenWellness['pregnancy_status'] = $wellnessData['women_pregnancy_status']; + } + $source = [ 'record_id' => (string) $wellnessId, 'visit_date' => date('Y-m-d', strtotime($recordedAt)), @@ -923,6 +1059,7 @@ class NursingApi extends BaseController 'name' => (string) ($wellnessData['recorded_by'] ?? 'Staff Nurse'), ], 'vitals' => $sourceVitals, + 'women_wellness' => $womenWellness, 'lifestyle' => $lifestyle, ]; diff --git a/app/Libraries/Abdm/Fhir/Generators/WellnessFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/WellnessFhirGenerator.php index 3a5454d..7999435 100644 --- a/app/Libraries/Abdm/Fhir/Generators/WellnessFhirGenerator.php +++ b/app/Libraries/Abdm/Fhir/Generators/WellnessFhirGenerator.php @@ -81,11 +81,21 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator $encounterRef = is_array($encounter) ? 'urn:uuid:' . (string) ($encounter['id'] ?? '') : null; $vitalObsRefs = []; - $examObsRefs = []; + $bodyMeasObsRefs = []; + $generalAssessObsRefs = []; + $activityObsRefs = []; $womenObsRefs = []; - $wellnessObsRefs = []; + $lifestyleObsRefs = []; + $otherObsRefs = []; - // 1. Process Vital Signs + // Body measurement LOINC codes + $bodyMeasLoincCodes = ['8302-2', '29463-7', '39156-5', '56115-9', '56114-2', '8280-0']; + // General assessment LOINC codes (blood sugar, lab POC, Hb) + $generalAssessLoincCodes = ['2339-0', '1558-6', '1521-4', '4548-4', '718-7', '25428-4', '1753-3']; + // Physical activity LOINC codes + $activityLoincCodes = ['55423-8', '93832-4', '55411-3', '41981-2']; + + // 1. Process Vital Signs & Standalone Measurements $vitalsList = (array) ($source['vitals'] ?? []); $sysBp = null; $diaBp = null; @@ -191,8 +201,7 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator $loinc = $this->codingResolver->resolveLoincForLabTest($loincCode, $display); $ucum = $this->codingResolver->resolveUnitUcUM($ucumCode ?: $unit); - $obsId = 'vital-' . $recordId . '-' . $idx; - $vitalObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; + $obsId = 'obs-vital-' . $recordId . '-' . $idx; $numericVal = is_numeric($val) ? (float) $val : null; if ($numericVal !== null) { if ($loincCode === '8310-5' || strcasecmp($display, 'Body temperature') === 0 || in_array($unit, ['Cel', 'degF', '[degF]'], true)) { @@ -203,23 +212,50 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator $numericVal = round($numericVal, 2); } elseif ($loincCode === '8302-2' || strcasecmp($display, 'Body height') === 0) { $numericVal = round($numericVal, 1); - } elseif (in_array($loincCode, ['8867-4', '59408-5', '9279-1'], true)) { + } elseif ($loincCode === '56115-9' || $loincCode === '56114-2') { + $numericVal = round($numericVal, 1); + } elseif ($loincCode === '8280-0' || $loincCode === '4548-4') { + $numericVal = round($numericVal, 2); + } elseif (in_array($loincCode, ['8867-4', '59408-5', '9279-1', '2339-0', '1558-6', '1521-4'], true)) { $numericVal = (float) round($numericVal); } else { $numericVal = round($numericVal, 2); } } + // Determine target composition section & profile + if (in_array($loincCode, $bodyMeasLoincCodes, true)) { + $categoryCode = 'vital-signs'; + $categoryDisplay = 'Vital Signs'; + $profile = 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation'; + $bodyMeasObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; + } elseif (in_array($loincCode, $generalAssessLoincCodes, true)) { + $categoryCode = 'laboratory'; + $categoryDisplay = 'Laboratory'; + $profile = 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation'; + $generalAssessObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; + } elseif (in_array($loincCode, $activityLoincCodes, true)) { + $categoryCode = 'activity'; + $categoryDisplay = 'Activity'; + $profile = 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation'; + $activityObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; + } else { + $categoryCode = 'vital-signs'; + $categoryDisplay = 'Vital Signs'; + $profile = 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/ObservationVitalSigns'; + $vitalObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; + } + $builder->addObservation([ 'resourceType' => 'Observation', 'id' => $obsId, - 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ObservationVitalSigns']], + 'meta' => ['profile' => [$profile]], 'status' => 'final', 'category' => [[ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/observation-category', - 'code' => 'vital-signs', - 'display' => 'Vital Signs', + 'code' => $categoryCode, + 'display' => $categoryDisplay, ]], ]], 'code' => [ @@ -242,8 +278,8 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator ]); } - // 2. Process Physical Examination Findings - $examList = (array) ($source['physical_examination'] ?? $source['findings'] ?? []); + // 2. Process Physical Examination / General Assessments Findings + $examList = (array) ($source['physical_examination'] ?? $source['findings'] ?? $source['general_assessments'] ?? []); foreach ($examList as $idx => $examItem) { $text = trim((string) (is_array($examItem) ? ($examItem['text'] ?? $examItem['display'] ?? '') : $examItem)); if (! $this->isMeaningfulValue($text)) { @@ -251,7 +287,7 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator } $obsId = 'exam-' . $recordId . '-' . $idx; - $examObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; + $generalAssessObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; $builder->addObservation([ 'resourceType' => 'Observation', @@ -280,7 +316,7 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator ]); } - // 3. Process Women Wellness / Reproductive Health (LMP, Gravida, Para, etc.) + // 3. Process Women Wellness / Reproductive Health (LMP, Pregnancy, etc.) $womenWellness = (array) ($source['women_wellness'] ?? []); $lmpDate = trim((string) ($womenWellness['lmp'] ?? $source['lmp'] ?? '')); if ($lmpDate !== '') { @@ -347,13 +383,23 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator // 4. Process General Wellness / Lifestyle & Diet Advice $adviceItems = (array) ($source['advice'] ?? $source['general_advice'] ?? $source['lifestyle'] ?? []); foreach ($adviceItems as $idx => $adviceText) { - $text = trim((string) (is_array($adviceText) ? ($adviceText['text'] ?? $adviceText['display'] ?? '') : $adviceText)); + $text = trim((string) (is_array($adviceText) ? ($adviceText['text'] ?? ($adviceText['value'] ?? ($adviceText['display'] ?? ''))) : $adviceText)); if (! $this->isMeaningfulValue($text)) { continue; } + $codeStr = is_array($adviceText) ? ($adviceText['code'] ?? '') : ''; + $displayStr = is_array($adviceText) ? ($adviceText['display'] ?? '') : ''; + $obsId = 'wellness-social-' . $recordId . '-' . $idx; - $wellnessObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; + + if ($codeStr === 'general-nursing' || str_contains(strtolower($displayStr), 'nursing')) { + $otherObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; + $obsTitle = 'General Nursing Observations'; + } else { + $lifestyleObsRefs[] = ['reference' => 'urn:uuid:' . $obsId]; + $obsTitle = ! empty($displayStr) ? $displayStr : 'Lifestyle & General Wellness Advice'; + } $builder->addObservation([ 'resourceType' => 'Observation', @@ -373,7 +419,7 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator 'code' => '8670-2', 'display' => 'History of Social history', ]], - 'text' => 'Lifestyle & General Wellness Advice', + 'text' => $obsTitle, ], 'subject' => ['reference' => $patientRef], 'encounter' => $encounterRef ? ['reference' => $encounterRef] : null, @@ -398,10 +444,24 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator ]; } - if (! empty($examObsRefs)) { + if (! empty($bodyMeasObsRefs)) { + $sections[] = [ + 'title' => 'Body Measurement', + 'entry' => $bodyMeasObsRefs, + ]; + } + + if (! empty($generalAssessObsRefs)) { $sections[] = [ 'title' => 'General Assessment', - 'entry' => $examObsRefs, + 'entry' => $generalAssessObsRefs, + ]; + } + + if (! empty($activityObsRefs)) { + $sections[] = [ + 'title' => 'Physical Activity', + 'entry' => $activityObsRefs, ]; } @@ -412,10 +472,17 @@ class WellnessFhirGenerator extends AbstractModuleFhirGenerator ]; } - if (! empty($wellnessObsRefs)) { + if (! empty($lifestyleObsRefs)) { $sections[] = [ 'title' => 'Lifestyle', - 'entry' => $wellnessObsRefs, + 'entry' => $lifestyleObsRefs, + ]; + } + + if (! empty($otherObsRefs)) { + $sections[] = [ + 'title' => 'Other Observations', + 'entry' => $otherObsRefs, ]; } diff --git a/public/App/Nursing/index.html b/public/App/Nursing/index.html index ad94edc..32f6f13 100644 --- a/public/App/Nursing/index.html +++ b/public/App/Nursing/index.html @@ -486,9 +486,16 @@ // ABDM M2 Wellness Vitals Modal State const [activeWellnessPatient, setActiveWellnessPatient] = useState(null); + const [wellnessModalTab, setWellnessModalTab] = useState('vitals'); // 'vitals', 'sugar', 'activity', 'lifestyle', 'women' const [wellnessForm, setWellnessForm] = useState({ temperature_f: '', pulse_rate: '', resp_rate: '', bp_systolic: '', bp_diastolic: '', spo2: '', weight_kg: '', height_cm: '', + pain_score: '', + waist_circumference_cm: '', hip_circumference_cm: '', waist_hip_ratio: '', + sugar_random: '', sugar_fasting: '', sugar_pp: '', hba1c: '', hemoglobin: '', + daily_steps: '', sleep_hours: '', exercise_min_per_day: '', + diet_type: '', tobacco_status: '', alcohol_status: '', + women_lmp: '', women_pregnancy_status: '', general_advice: '', diet_lifestyle_note: '' }); @@ -1048,6 +1055,7 @@ // ABDM M2 Wellness Actions const openWellnessModal = (patient) => { setActiveWellnessPatient(patient); + setWellnessModalTab('vitals'); const opdVitals = patient.opd_today || {}; setWellnessForm({ temperature_f: opdVitals.temp || '', @@ -1058,6 +1066,23 @@ spo2: opdVitals.spo2 || '', weight_kg: opdVitals.weight || '', height_cm: opdVitals.height || '', + pain_score: '', + waist_circumference_cm: opdVitals.waist || '', + hip_circumference_cm: '', + waist_hip_ratio: '', + sugar_random: opdVitals.glucose || '', + sugar_fasting: '', + sugar_pp: '', + hba1c: '', + hemoglobin: '', + daily_steps: '', + sleep_hours: '', + exercise_min_per_day: '', + diet_type: '', + tobacco_status: '', + alcohol_status: '', + women_lmp: '', + women_pregnancy_status: '', general_advice: '', diet_lifestyle_note: '' }); @@ -1086,6 +1111,12 @@ setWellnessForm({ temperature_f: '', pulse_rate: '', resp_rate: '', bp_systolic: '', bp_diastolic: '', spo2: '', weight_kg: '', height_cm: '', + pain_score: '', + waist_circumference_cm: '', hip_circumference_cm: '', waist_hip_ratio: '', + sugar_random: '', sugar_fasting: '', sugar_pp: '', hba1c: '', hemoglobin: '', + daily_steps: '', sleep_hours: '', exercise_min_per_day: '', + diet_type: '', tobacco_status: '', alcohol_status: '', + women_lmp: '', women_pregnancy_status: '', general_advice: '', diet_lifestyle_note: '' }); } else { @@ -2490,7 +2521,7 @@ )} {/* ------------------------------------------------------------- - MODAL 2: ABDM M2 WELLNESS & VITALS ENTRY + MODAL 2: ABDM M2 WELLNESS & VITALS ENTRY (COMPREHENSIVE SUITE) ------------------------------------------------------------- */} {activeWellnessPatient && (
@@ -2501,7 +2532,7 @@
Record ABDM M2 Wellness & Vitals
- Stores standardized vital signs observation bundle + NRCES FHIR R4 Compliant Comprehensive Health & Wellness Suite
@@ -2510,80 +2541,369 @@
{/* Patient Context Banner */}
-
+
- {activeWellnessPatient.fullName || activeWellnessPatient.p_fname} -
+ {activeWellnessPatient.fullName || activeWellnessPatient.p_fname} + {activeWellnessPatient.gender ? activeWellnessPatient.gender.toUpperCase() : 'PATIENT'} + {activeWellnessPatient.age && {activeWellnessPatient.age} Yrs} +
UHID: {activeWellnessPatient.p_code || activeWellnessPatient.uhid} {activeWellnessPatient.mphone1 && ` • Phone: ${activeWellnessPatient.mphone1}`}
- {activeWellnessPatient.abha_id && ( - - ABHA: {activeWellnessPatient.abha_id} - - )} -
-
- - {/* Vitals Form Grid */} -
-
- -
- setWellnessForm({...wellnessForm, bp_systolic: e.target.value})} /> - / - setWellnessForm({...wellnessForm, bp_diastolic: e.target.value})} /> +
+ {activeWellnessPatient.abha_id ? ( + + ABHA: {activeWellnessPatient.abha_id} + + ) : ( + + ABHA Not Linked + + )}
-
- - setWellnessForm({...wellnessForm, pulse_rate: e.target.value})} /> -
-
- - setWellnessForm({...wellnessForm, spo2: e.target.value})} /> -
-
- - setWellnessForm({...wellnessForm, temperature_f: e.target.value})} /> -
-
- - setWellnessForm({...wellnessForm, resp_rate: e.target.value})} /> -
-
- - setWellnessForm({...wellnessForm, height_cm: e.target.value})} /> -
-
- - setWellnessForm({...wellnessForm, weight_kg: e.target.value})} /> -
-
- - 0 - ? (parseFloat(wellnessForm.weight_kg) / Math.pow(parseFloat(wellnessForm.height_cm) / 100, 2)).toFixed(2) - : '-' - } - /> -
- -
- - setWellnessForm({...wellnessForm, diet_lifestyle_note: e.target.value})} /> -
- -
- - setWellnessForm({...wellnessForm, general_advice: e.target.value})} /> -
+ + {/* Category Tab Switcher */} +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • + {String(activeWellnessPatient.gender || '').toLowerCase() !== 'male' && ( +
  • + +
  • + )} +
+ + {/* TAB 1: VITALS & TRIAGE */} + {wellnessModalTab === 'vitals' && ( +
+
+ +
+ setWellnessForm({...wellnessForm, bp_systolic: e.target.value})} /> + / + setWellnessForm({...wellnessForm, bp_diastolic: e.target.value})} /> +
+
+
+ + setWellnessForm({...wellnessForm, pulse_rate: e.target.value})} /> +
+
+ + setWellnessForm({...wellnessForm, spo2: e.target.value})} /> +
+
+ + setWellnessForm({...wellnessForm, temperature_f: e.target.value})} /> +
+ +
+ + setWellnessForm({...wellnessForm, resp_rate: e.target.value})} /> +
+
+ + setWellnessForm({...wellnessForm, height_cm: e.target.value})} /> +
+
+ + setWellnessForm({...wellnessForm, weight_kg: e.target.value})} /> +
+
+ +
+ 0 + ? (parseFloat(wellnessForm.weight_kg) / Math.pow(parseFloat(wellnessForm.height_cm) / 100, 2)).toFixed(2) + : '-' + } + /> +
+
+ +
+ +
+ {[0,1,2,3,4,5,6,7,8,9,10].map(val => ( + + ))} +
+
+
+ )} + + {/* TAB 2: SUGAR & POC LABS */} + {wellnessModalTab === 'sugar' && ( +
+
+
+ + Point-of-care capillary blood sugar & POC lab values (NRCES GeneralAssessment slice) +
+
+
+ +
+ setWellnessForm({...wellnessForm, sugar_random: e.target.value})} /> + mg/dL +
+ Normal: < 140 mg/dL +
+
+ +
+ setWellnessForm({...wellnessForm, sugar_fasting: e.target.value})} /> + mg/dL +
+ Normal: 70 - 100 mg/dL +
+
+ +
+ setWellnessForm({...wellnessForm, sugar_pp: e.target.value})} /> + mg/dL +
+ Normal: < 140 mg/dL (2h post meal) +
+
+ +
+ setWellnessForm({...wellnessForm, hba1c: e.target.value})} /> + % +
+ Normal: < 5.7% | Prediabetes: 5.7-6.4% | Diabetes: ≥ 6.5% +
+
+ +
+ setWellnessForm({...wellnessForm, hemoglobin: e.target.value})} /> + g/dL +
+ Normal: 12.0 - 17.5 g/dL +
+
+ )} + + {/* TAB 3: WAIST, HIP & ACTIVITY */} + {wellnessModalTab === 'activity' && ( +
+
+
Body Circumferences (NRCES BodyMeasurement)
+
+
+ +
+ setWellnessForm({...wellnessForm, waist_circumference_cm: e.target.value})} /> + cm +
+
+
+ +
+ setWellnessForm({...wellnessForm, hip_circumference_cm: e.target.value})} /> + cm +
+
+
+ + 0 + ? (parseFloat(wellnessForm.waist_circumference_cm) / parseFloat(wellnessForm.hip_circumference_cm)).toFixed(2) + : '-' + } + /> + Normal: Men < 0.90 | Women < 0.85 +
+ +
+
Physical Activity & Sleep (NRCES PhysicalActivity)
+
+
+ +
+ setWellnessForm({...wellnessForm, daily_steps: e.target.value})} /> + steps +
+
+
+ +
+ setWellnessForm({...wellnessForm, sleep_hours: e.target.value})} /> + hours +
+
+
+ +
+ setWellnessForm({...wellnessForm, exercise_min_per_day: e.target.value})} /> + min/day +
+
+
+ )} + + {/* TAB 4: LIFESTYLE & GUIDANCE */} + {wellnessModalTab === 'lifestyle' && ( +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + setWellnessForm({...wellnessForm, diet_lifestyle_note: e.target.value})} /> +
+ +
+ + setWellnessForm({...wellnessForm, general_advice: e.target.value})} /> +
+
+ )} + + {/* TAB 5: WOMEN'S HEALTH */} + {wellnessModalTab === 'women' && ( +
+
+ + setWellnessForm({...wellnessForm, women_lmp: e.target.value})} /> +
+
+ + +
+
+ )}
@@ -2592,7 +2912,7 @@
-
@@ -2708,11 +3028,11 @@ )} {/* ------------------------------------------------------------- - MODAL 4: PATIENT VITALS & WELLNESS HISTORY + MODAL 4: PATIENT VITALS & WELLNESS HISTORY (COMPREHENSIVE) ------------------------------------------------------------- */} {wellnessHistoryPatient && (
-
+
@@ -2744,11 +3064,11 @@ Date & Time - BP - Pulse - Temp - SpO2 - BMI + Vitals (BP / Pulse / SpO2 / Temp) + Body / BMI + Blood Glucose (mg/dL) + POC Labs + Activity / Lifestyle Staff Advice / Remarks @@ -2757,13 +3077,57 @@ {wellnessHistoryList.map(rec => ( {formatDateTime(rec.recorded_at)} - {rec.bp_systolic ? `${rec.bp_systolic}/${rec.bp_diastolic}` : '-'} - {rec.pulse_rate ? `${rec.pulse_rate} bpm` : '-'} - {rec.temperature_f ? `${rec.temperature_f} °F` : '-'} - {rec.spo2 ? `${rec.spo2}%` : '-'} - {rec.bmi || '-'} + +
+ {rec.bp_systolic && BP {rec.bp_systolic}/{rec.bp_diastolic}} + {rec.pulse_rate && {rec.pulse_rate} bpm} + {rec.spo2 && {rec.spo2}% O2} + {rec.temperature_f && {rec.temperature_f}°F} + {rec.pain_score !== null && rec.pain_score !== undefined && rec.pain_score !== '' && ( + + Pain: {rec.pain_score}/10 + + )} +
+ + +
+ {rec.bmi && BMI {rec.bmi}} + {rec.weight_kg && {rec.weight_kg}kg } + {rec.height_cm && {rec.height_cm}cm} +
+ {(rec.waist_circumference_cm || rec.waist_hip_ratio) && ( + + {rec.waist_circumference_cm ? `Waist: ${rec.waist_circumference_cm}cm` : ''} + {rec.waist_hip_ratio ? ` • WHR: ${rec.waist_hip_ratio}` : ''} + + )} + + +
+ {rec.sugar_random && RBS: {rec.sugar_random}} + {rec.sugar_fasting && FBS: {rec.sugar_fasting}} + {rec.sugar_pp && PP: {rec.sugar_pp}} + {!rec.sugar_random && !rec.sugar_fasting && !rec.sugar_pp && -} +
+ + +
+ {rec.hemoglobin && Hb: {rec.hemoglobin} g/dL} + {rec.hba1c && A1c: {rec.hba1c}%} + {!rec.hemoglobin && !rec.hba1c && -} +
+ + +
+ {rec.diet_type && {rec.diet_type}} + {rec.daily_steps && {rec.daily_steps} steps} + {rec.sleep_hours && {rec.sleep_hours}h sleep} + {!rec.diet_type && !rec.daily_steps && !rec.sleep_hours && -} +
+ {rec.recorded_by || 'Staff'} - {rec.general_advice || rec.diet_lifestyle_note || '-'} + {rec.general_advice || rec.diet_lifestyle_note || '-'} ))} diff --git a/scratch/test_wellness_bundle.php b/scratch/test_wellness_bundle.php index a62cdf2..0326378 100644 --- a/scratch/test_wellness_bundle.php +++ b/scratch/test_wellness_bundle.php @@ -1,68 +1,93 @@ wellness(); +require 'vendor/autoload.php'; +define('FCPATH', __DIR__ . '/../public/'); +defined('ENVIRONMENT') || define('ENVIRONMENT', 'development'); +require __DIR__ . '/../app/Config/Paths.php'; +$paths = new Config\Paths(); +require $paths->systemDirectory . '/Boot.php'; +CodeIgniter\Boot::bootConsole($paths); $source = [ - 'record_id' => '33710', - 'session_id' => '0', - 'visit_date' => '2026-09-06', - 'completed_at' => '2026-09-06T18:11:14+05:30', + 'record_id' => '101', + 'visit_date' => '2026-10-07', + 'completed_at' => date(DATE_ATOM), + 'hfr_id' => 'IN0510000828', 'patient' => [ - 'id' => '15350', - 'uhid' => '15350', - 'name' => 'DEVENDER SINGH 0', - 'gender' => 'male', - 'dob' => '1979-03-28', - 'abha_id' => '91510165305101', - 'abha_address' => 'singhdevender0328@sbx', - 'mobile' => '+919720958717', + 'id' => 15353, + 'name' => 'DHAIRYA SINGH BISHT', + 'gender' => 'female', + 'dob' => '1995-05-15', + 'mobile' => '9720958717', + 'abha_id' => '91310013085603', + 'abha_address' => '91310013085603@sbx', + 'p_code' => 'P26081015353', ], 'practitioner' => [ 'id' => '1', - 'name' => 'Dr. Sanjay Kumar', - ], - 'organization' => [ - 'id' => 'IN0510000828', - 'name' => 'DevSoft Tech', + 'name' => 'Rita (NUR-805)', ], 'vitals' => [ - ['code' => '8480-6', 'display' => 'Systolic blood pressure', 'value' => 120, 'unit' => 'mmHg', 'ucum_code' => 'mm[Hg]'], - ['code' => '8462-4', 'display' => 'Diastolic blood pressure', 'value' => 80, 'unit' => 'mmHg', 'ucum_code' => 'mm[Hg]'], - ['code' => '8867-4', 'display' => 'Heart rate', 'value' => 30, 'unit' => '/min', 'ucum_code' => '/min'], - ['code' => '8310-5', 'display' => 'Body temperature', 'value' => 36.666666666666664, 'unit' => 'Cel', 'ucum_code' => 'Cel'], - ['code' => '9279-1', 'display' => 'Respiratory rate', 'value' => 20, 'unit' => '/min', 'ucum_code' => '/min'], - ['code' => '59408-5', 'display' => 'Oxygen saturation in Arterial blood by Pulse oximetry', 'value' => 95, 'unit' => '%', 'ucum_code' => '%'], + // Standard Vitals + ['loinc_code' => '8480-6', 'display' => 'Systolic blood pressure', 'value' => 120, 'unit' => 'mmHg', 'ucum_code' => 'mm[Hg]'], + ['loinc_code' => '8462-4', 'display' => 'Diastolic blood pressure', 'value' => 80, 'unit' => 'mmHg', 'ucum_code' => 'mm[Hg]'], + ['loinc_code' => '8867-4', 'display' => 'Heart rate', 'value' => 72, 'unit' => '/min', 'ucum_code' => '/min'], + ['loinc_code' => '8302-2', 'display' => 'Body height', 'value' => 165, 'unit' => 'cm', 'ucum_code' => 'cm'], + ['loinc_code' => '29463-7', 'display' => 'Body weight', 'value' => 68, 'unit' => 'kg', 'ucum_code' => 'kg'], + ['loinc_code' => '39156-5', 'display' => 'Body Mass Index', 'value' => 24.98, 'unit' => 'kg/m2', 'ucum_code' => 'kg/m2'], + ['loinc_code' => '8310-5', 'display' => 'Body temperature', 'value' => 37, 'unit' => 'Cel', 'ucum_code' => 'Cel'], + ['loinc_code' => '9279-1', 'display' => 'Respiratory rate', 'value' => 18, 'unit' => '/min', 'ucum_code' => '/min'], + ['loinc_code' => '59408-5', 'display' => 'Oxygen saturation in Arterial blood by Pulse oximetry', 'value' => 98, 'unit' => '%', 'ucum_code' => '%'], + ['loinc_code' => '72514-3', 'display' => 'Pain severity - 0-10 verbal numeric rating', 'value' => 2, 'unit' => '{score}', 'ucum_code' => '{score}'], + + // Anthropometry / Body Measurements + ['loinc_code' => '56115-9', 'display' => 'Waist circumference', 'value' => 82.5, 'unit' => 'cm', 'ucum_code' => 'cm'], + ['loinc_code' => '56114-2', 'display' => 'Hip circumference', 'value' => 98.0, 'unit' => 'cm', 'ucum_code' => 'cm'], + ['loinc_code' => '8280-0', 'display' => 'Waist to hip ratio', 'value' => 0.84, 'unit' => 'ratio', 'ucum_code' => '{ratio}'], + + // Blood Glucose & POC Labs + ['loinc_code' => '2339-0', 'display' => 'Glucose [Mass/volume] in Blood', 'value' => 110, 'unit' => 'mg/dL', 'ucum_code' => 'mg/dL'], + ['loinc_code' => '1558-6', 'display' => 'Fasting glucose [Mass/volume] in Blood', 'value' => 92, 'unit' => 'mg/dL', 'ucum_code' => 'mg/dL'], + ['loinc_code' => '1521-4', 'display' => 'Glucose [Mass/volume] in Blood 2 hours post meal', 'value' => 128, 'unit' => 'mg/dL', 'ucum_code' => 'mg/dL'], + ['loinc_code' => '4548-4', 'display' => 'Hemoglobin A1c/Hemoglobin.total in Blood', 'value' => 5.4, 'unit' => '%', 'ucum_code' => '%'], + ['loinc_code' => '718-7', 'display' => 'Hemoglobin [Mass/volume] in Blood', 'value' => 13.8, 'unit' => 'g/dL', 'ucum_code' => 'g/dL'], + + // Physical Activity & Sleep + ['loinc_code' => '55423-8', 'display' => 'Number of steps in 24 hour Measured', 'value' => 8500, 'unit' => '{steps}', 'ucum_code' => '{steps}'], + ['loinc_code' => '93832-4', 'display' => 'Sleep duration', 'value' => 7.5, 'unit' => 'h', 'ucum_code' => 'h'], + ['loinc_code' => '55411-3', 'display' => 'Exercise duration', 'value' => 30, 'unit' => 'min/d', 'ucum_code' => 'min/d'], ], - 'physical_examination' => [ - 'Complaints: Abdominal muscle pain, Acute Q fever\nComplaint Duration (Days): 1 days\nComplaint Severity: mild', - 'Diagnosis: viral infection', - 'Investigation: Pathology : CBC, LFT, KFT\nX-Ray : X-Ray Chest (PA View)', + 'women_wellness' => [ + 'lmp' => '2026-09-20', + 'pregnancy_status' => 'Not Pregnant', ], - 'advice' => [ - 'Smoking status: Yes', - 'Alcohol use: Yes', + 'lifestyle' => [ + ['code' => '81663-7', 'display' => 'Diet Type', 'value' => 'Vegetarian'], + ['code' => '365981007', 'display' => 'Tobacco Smoking Status', 'value' => 'Never Smoked'], + ['code' => '228273003', 'display' => 'Alcohol Consumption Status', 'value' => 'Lifetime Non-Drinker'], + ['code' => 'diet-lifestyle', 'display' => 'Diet & Lifestyle Guidance', 'value' => '30 min daily walking, balanced hydration'], + ['code' => 'general-nursing', 'display' => 'General Nursing Observations', 'value' => 'Patient conscious, alert and vitals stable'], ], ]; -$output = $generator->generate($source); -$bundle = $output['fhir_bundle']; - -// Let's inspect the bundle entry 0 (Composition) -echo "=== COMPOSITION IN TEST BUNDLE ===\n"; -$comp = $bundle['entry'][0]['resource']; -file_put_contents('scratch/updated_wellness_bundle.json', json_encode($bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); -echo "Saved to scratch/updated_wellness_bundle.json\n"; - - - - - - +$gen = new \App\Libraries\Abdm\Fhir\Generators\WellnessFhirGenerator(); +$res = $gen->generate($source); +echo "HI_TYPE: " . ($res['hi_type'] ?? '') . "\n"; +echo "CARE_CONTEXT: " . ($res['care_context_reference'] ?? '') . "\n"; +echo "DISPLAY: " . ($res['care_context_display'] ?? '') . "\n"; +echo "BUNDLE ENTRIES: " . count($res['fhir_bundle']['entry'] ?? []) . "\n"; +$composition = null; +$sections = []; +foreach ($res['fhir_bundle']['entry'] ?? [] as $entry) { + if (($entry['resource']['resourceType'] ?? '') === 'Composition') { + $composition = $entry['resource']; + foreach ($composition['section'] ?? [] as $sec) { + $sections[] = ($sec['title'] ?? 'Untitled') . ' (' . count($sec['entry'] ?? []) . ' refs)'; + } + } +} +echo "COMPOSITION SECTIONS (" . count($sections) . "):\n - " . implode("\n - ", $sections) . "\n"; +echo "VALIDATION: valid=" . (($res['validation']['valid'] ?? false) ? 'YES' : 'NO') . ", errors=" . count($res['validation']['errors'] ?? []) . "\n"; +if (!empty($res['validation']['errors'])) { + echo "ERRORS: " . json_encode($res['validation']['errors']) . "\n"; +}