feat(abdm): expand WellnessRecord to full comprehensive suite (Option B) with segmented tabs and labs

This commit is contained in:
DevSoftTech Server 2026-10-07 10:15:42 +05:30
parent 0b1d545a3c
commit 0441ebe08a
4 changed files with 749 additions and 156 deletions

View File

@ -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,
];

View File

@ -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,
];
}

View File

@ -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 && (
<div className="modal show d-block" style={{backgroundColor: 'rgba(0,0,0,0.65)', zIndex: 1060}}>
@ -2501,7 +2532,7 @@
<i className="bi bi-heart-pulse-fill fs-5"></i>
<div>
<h6 className="modal-title fw-bold mb-0">Record ABDM M2 Wellness &amp; Vitals</h6>
<small className="text-white-50" style={{fontSize: '11px'}}>Stores standardized vital signs observation bundle</small>
<small className="text-white-50" style={{fontSize: '11px'}}>NRCES FHIR R4 Compliant Comprehensive Health &amp; Wellness Suite</small>
</div>
</div>
<button className="btn-close btn-close-white" onClick={() => setActiveWellnessPatient(null)}></button>
@ -2510,80 +2541,369 @@
<div className="modal-body p-3">
{/* Patient Context Banner */}
<div className="p-2.5 rounded-3 bg-light border mb-3">
<div className="d-flex justify-content-between align-items-start flex-wrap gap-1">
<div className="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<strong className="fs-6 text-dark">{activeWellnessPatient.fullName || activeWellnessPatient.p_fname}</strong>
<div className="small text-muted">
<strong className="fs-6 text-dark me-2">{activeWellnessPatient.fullName || activeWellnessPatient.p_fname}</strong>
<span className="badge bg-secondary me-1">{activeWellnessPatient.gender ? activeWellnessPatient.gender.toUpperCase() : 'PATIENT'}</span>
{activeWellnessPatient.age && <span className="badge bg-light text-dark border me-1">{activeWellnessPatient.age} Yrs</span>}
<div className="small text-muted mt-0.5">
UHID: <span className="text-danger fw-bold">{activeWellnessPatient.p_code || activeWellnessPatient.uhid}</span>
{activeWellnessPatient.mphone1 && ` • Phone: ${activeWellnessPatient.mphone1}`}
</div>
</div>
{activeWellnessPatient.abha_id && (
<span className="badge bg-success">
<i className="bi bi-shield-check me-1"></i> ABHA: {activeWellnessPatient.abha_id}
</span>
)}
</div>
</div>
{/* Vitals Form Grid */}
<div className="row g-2 mb-3">
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">BP Sys/Dia (mmHg)</label>
<div className="input-group input-group-sm">
<input type="number" className="form-control text-center" placeholder="120" value={wellnessForm.bp_systolic} onChange={e => setWellnessForm({...wellnessForm, bp_systolic: e.target.value})} />
<span className="input-group-text">/</span>
<input type="number" className="form-control text-center" placeholder="80" value={wellnessForm.bp_diastolic} onChange={e => setWellnessForm({...wellnessForm, bp_diastolic: e.target.value})} />
<div>
{activeWellnessPatient.abha_id ? (
<span className="badge bg-success">
<i className="bi bi-shield-check me-1"></i> ABHA: {activeWellnessPatient.abha_id}
</span>
) : (
<span className="badge bg-secondary-subtle text-secondary border">
<i className="bi bi-shield-exclamation me-1"></i> ABHA Not Linked
</span>
)}
</div>
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Pulse Rate (bpm)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="72" value={wellnessForm.pulse_rate} onChange={e => setWellnessForm({...wellnessForm, pulse_rate: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">SpO2 %</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="98" value={wellnessForm.spo2} onChange={e => setWellnessForm({...wellnessForm, spo2: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Temp °F</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="98.6" value={wellnessForm.temperature_f} onChange={e => setWellnessForm({...wellnessForm, temperature_f: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Resp Rate (/min)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="18" value={wellnessForm.resp_rate} onChange={e => setWellnessForm({...wellnessForm, resp_rate: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Height (cm)</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="165" value={wellnessForm.height_cm} onChange={e => setWellnessForm({...wellnessForm, height_cm: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Weight (kg)</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="70" value={wellnessForm.weight_kg} onChange={e => setWellnessForm({...wellnessForm, weight_kg: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Calculated BMI</label>
<input
type="text"
className="form-control form-control-sm text-center bg-light fw-bold"
readOnly
value={
wellnessForm.height_cm && wellnessForm.weight_kg && parseFloat(wellnessForm.height_cm) > 0
? (parseFloat(wellnessForm.weight_kg) / Math.pow(parseFloat(wellnessForm.height_cm) / 100, 2)).toFixed(2)
: '-'
}
/>
</div>
<div className="col-12 col-md-6">
<label className="form-label small mb-1 fw-bold">Diet &amp; Lifestyle Guidance (ABDM)</label>
<input type="text" className="form-control form-control-sm" placeholder="e.g. Low sodium diet, 30 min daily walking" value={wellnessForm.diet_lifestyle_note} onChange={e => setWellnessForm({...wellnessForm, diet_lifestyle_note: e.target.value})} />
</div>
<div className="col-12 col-md-6">
<label className="form-label small mb-1 fw-bold">General Nursing Observations</label>
<input type="text" className="form-control form-control-sm" placeholder="e.g. Patient asymptomatic, alert and oriented" value={wellnessForm.general_advice} onChange={e => setWellnessForm({...wellnessForm, general_advice: e.target.value})} />
</div>
</div>
{/* Category Tab Switcher */}
<ul className="nav nav-pills nav-fill bg-light p-1 rounded-3 mb-3 border small">
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'vitals' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('vitals')}
>
<i className="bi bi-heart-pulse-fill text-danger"></i>
<span>Vitals &amp; Triage</span>
{(wellnessForm.bp_systolic || wellnessForm.pulse_rate || wellnessForm.temperature_f) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'sugar' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('sugar')}
>
<i className="bi bi-droplet-half text-danger"></i>
<span>Sugar &amp; Labs</span>
{(wellnessForm.sugar_random || wellnessForm.sugar_fasting || wellnessForm.sugar_pp || wellnessForm.hba1c || wellnessForm.hemoglobin) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'activity' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('activity')}
>
<i className="bi bi-person-bounding-box text-primary"></i>
<span>Waist &amp; Activity</span>
{(wellnessForm.waist_circumference_cm || wellnessForm.hip_circumference_cm || wellnessForm.daily_steps || wellnessForm.sleep_hours) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'lifestyle' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('lifestyle')}
>
<i className="bi bi-apple text-success"></i>
<span>Lifestyle &amp; Notes</span>
{(wellnessForm.diet_type || wellnessForm.tobacco_status || wellnessForm.diet_lifestyle_note || wellnessForm.general_advice) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
{String(activeWellnessPatient.gender || '').toLowerCase() !== 'male' && (
<li className="nav-item">
<button
type="button"
className={`nav-link py-1.5 px-2 fw-semibold d-flex align-items-center justify-content-center gap-1 ${wellnessModalTab === 'women' ? 'active shadow-sm' : 'text-secondary'}`}
onClick={() => setWellnessModalTab('women')}
>
<i className="bi bi-gender-female text-warning"></i>
<span>Women's Health</span>
{(wellnessForm.women_lmp || wellnessForm.women_pregnancy_status) && (
<span className="badge rounded-pill bg-success p-1 ms-1" style={{fontSize: '7px'}}>✓</span>
)}
</button>
</li>
)}
</ul>
{/* TAB 1: VITALS & TRIAGE */}
{wellnessModalTab === 'vitals' && (
<div className="row g-2.5 mb-2">
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">BP Sys/Dia (mmHg)</label>
<div className="input-group input-group-sm">
<input type="number" className="form-control text-center" placeholder="120" value={wellnessForm.bp_systolic} onChange={e => setWellnessForm({...wellnessForm, bp_systolic: e.target.value})} />
<span className="input-group-text px-1">/</span>
<input type="number" className="form-control text-center" placeholder="80" value={wellnessForm.bp_diastolic} onChange={e => setWellnessForm({...wellnessForm, bp_diastolic: e.target.value})} />
</div>
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Pulse Rate (bpm)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="72" value={wellnessForm.pulse_rate} onChange={e => setWellnessForm({...wellnessForm, pulse_rate: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">SpO2 %</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="98" value={wellnessForm.spo2} onChange={e => setWellnessForm({...wellnessForm, spo2: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Temp °F</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="98.6" value={wellnessForm.temperature_f} onChange={e => setWellnessForm({...wellnessForm, temperature_f: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Resp Rate (/min)</label>
<input type="number" className="form-control form-control-sm text-center" placeholder="18" value={wellnessForm.resp_rate} onChange={e => setWellnessForm({...wellnessForm, resp_rate: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Height (cm)</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="165" value={wellnessForm.height_cm} onChange={e => setWellnessForm({...wellnessForm, height_cm: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Weight (kg)</label>
<input type="number" step="0.1" className="form-control form-control-sm text-center" placeholder="70" value={wellnessForm.weight_kg} onChange={e => setWellnessForm({...wellnessForm, weight_kg: e.target.value})} />
</div>
<div className="col-6 col-md-3">
<label className="form-label small mb-1 fw-bold">Calculated BMI</label>
<div className="input-group input-group-sm">
<input
type="text"
className="form-control form-control-sm text-center bg-light fw-bold"
readOnly
value={
wellnessForm.height_cm && wellnessForm.weight_kg && parseFloat(wellnessForm.height_cm) > 0
? (parseFloat(wellnessForm.weight_kg) / Math.pow(parseFloat(wellnessForm.height_cm) / 100, 2)).toFixed(2)
: '-'
}
/>
</div>
</div>
<div className="col-12 mt-2">
<label className="form-label small mb-1 fw-bold d-flex justify-content-between">
<span><i className="bi bi-emoji-frown me-1 text-warning"></i> Pain Severity Score (0 - 10 VRS)</span>
<span className="text-muted fw-normal">
{wellnessForm.pain_score === '' ? 'Not assessed' : (
wellnessForm.pain_score == 0 ? '0 - No Pain' :
wellnessForm.pain_score <= 3 ? `${wellnessForm.pain_score} - Mild Pain` :
wellnessForm.pain_score <= 6 ? `${wellnessForm.pain_score} - Moderate Pain` :
`${wellnessForm.pain_score} - Severe Pain`
)}
</span>
</label>
<div className="d-flex gap-1 flex-wrap">
{[0,1,2,3,4,5,6,7,8,9,10].map(val => (
<button
key={val}
type="button"
className={`btn btn-sm flex-fill py-1 px-1.5 ${wellnessForm.pain_score === String(val) ? (val === 0 ? 'btn-success text-white' : val <= 3 ? 'btn-info text-white' : val <= 6 ? 'btn-warning text-dark' : 'btn-danger text-white') : 'btn-outline-secondary'}`}
onClick={() => setWellnessForm({...wellnessForm, pain_score: wellnessForm.pain_score === String(val) ? '' : String(val)})}
>
{val}
</button>
))}
</div>
</div>
</div>
)}
{/* TAB 2: SUGAR & POC LABS */}
{wellnessModalTab === 'sugar' && (
<div className="row g-2.5 mb-2">
<div className="col-12">
<div className="alert alert-info py-1.5 px-3 small d-flex align-items-center gap-2 mb-2">
<i className="bi bi-info-circle-fill fs-6 text-primary"></i>
<span>Point-of-care capillary blood sugar &amp; POC lab values (NRCES <strong>GeneralAssessment</strong> slice)</span>
</div>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Random Blood Sugar (RBS)</label>
<div className="input-group input-group-sm">
<input type="number" step="1" className="form-control text-center" placeholder="110" value={wellnessForm.sugar_random} onChange={e => setWellnessForm({...wellnessForm, sugar_random: e.target.value})} />
<span className="input-group-text">mg/dL</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: &lt; 140 mg/dL</small>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Fasting Blood Sugar (FBS)</label>
<div className="input-group input-group-sm">
<input type="number" step="1" className="form-control text-center" placeholder="95" value={wellnessForm.sugar_fasting} onChange={e => setWellnessForm({...wellnessForm, sugar_fasting: e.target.value})} />
<span className="input-group-text">mg/dL</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: 70 - 100 mg/dL</small>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Post-Prandial (PPBS)</label>
<div className="input-group input-group-sm">
<input type="number" step="1" className="form-control text-center" placeholder="130" value={wellnessForm.sugar_pp} onChange={e => setWellnessForm({...wellnessForm, sugar_pp: e.target.value})} />
<span className="input-group-text">mg/dL</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: &lt; 140 mg/dL (2h post meal)</small>
</div>
<div className="col-6 col-md-6">
<label className="form-label small mb-1 fw-bold">HbA1c (Glycated Hemoglobin)</label>
<div className="input-group input-group-sm">
<input type="number" step="0.1" className="form-control text-center" placeholder="5.6" value={wellnessForm.hba1c} onChange={e => setWellnessForm({...wellnessForm, hba1c: e.target.value})} />
<span className="input-group-text">%</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: &lt; 5.7% | Prediabetes: 5.7-6.4% | Diabetes: ≥ 6.5%</small>
</div>
<div className="col-6 col-md-6">
<label className="form-label small mb-1 fw-bold">POC Hemoglobin (Hb)</label>
<div className="input-group input-group-sm">
<input type="number" step="0.1" className="form-control text-center" placeholder="13.5" value={wellnessForm.hemoglobin} onChange={e => setWellnessForm({...wellnessForm, hemoglobin: e.target.value})} />
<span className="input-group-text">g/dL</span>
</div>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: 12.0 - 17.5 g/dL</small>
</div>
</div>
)}
{/* TAB 3: WAIST, HIP & ACTIVITY */}
{wellnessModalTab === 'activity' && (
<div className="row g-2.5 mb-2">
<div className="col-12">
<h6 className="fw-bold small text-primary mb-1 border-bottom pb-1"><i className="bi bi-rulers me-1"></i> Body Circumferences (NRCES BodyMeasurement)</h6>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Waist Circumference</label>
<div className="input-group input-group-sm">
<input type="number" step="0.1" className="form-control text-center" placeholder="85" value={wellnessForm.waist_circumference_cm} onChange={e => setWellnessForm({...wellnessForm, waist_circumference_cm: e.target.value})} />
<span className="input-group-text">cm</span>
</div>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Hip Circumference</label>
<div className="input-group input-group-sm">
<input type="number" step="0.1" className="form-control text-center" placeholder="95" value={wellnessForm.hip_circumference_cm} onChange={e => setWellnessForm({...wellnessForm, hip_circumference_cm: e.target.value})} />
<span className="input-group-text">cm</span>
</div>
</div>
<div className="col-12 col-md-4">
<label className="form-label small mb-1 fw-bold">Waist-to-Hip Ratio (WHR)</label>
<input
type="text"
className="form-control form-control-sm text-center bg-light fw-bold"
readOnly
value={
wellnessForm.waist_circumference_cm && wellnessForm.hip_circumference_cm && parseFloat(wellnessForm.hip_circumference_cm) > 0
? (parseFloat(wellnessForm.waist_circumference_cm) / parseFloat(wellnessForm.hip_circumference_cm)).toFixed(2)
: '-'
}
/>
<small className="text-muted" style={{fontSize: '10px'}}>Normal: Men &lt; 0.90 | Women &lt; 0.85</small>
</div>
<div className="col-12 mt-2">
<h6 className="fw-bold small text-success mb-1 border-bottom pb-1"><i className="bi bi-activity me-1"></i> Physical Activity &amp; Sleep (NRCES PhysicalActivity)</h6>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Daily Steps (24h)</label>
<div className="input-group input-group-sm">
<input type="number" step="100" className="form-control text-center" placeholder="8000" value={wellnessForm.daily_steps} onChange={e => setWellnessForm({...wellnessForm, daily_steps: e.target.value})} />
<span className="input-group-text">steps</span>
</div>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Sleep Duration</label>
<div className="input-group input-group-sm">
<input type="number" step="0.5" className="form-control text-center" placeholder="7.5" value={wellnessForm.sleep_hours} onChange={e => setWellnessForm({...wellnessForm, sleep_hours: e.target.value})} />
<span className="input-group-text">hours</span>
</div>
</div>
<div className="col-12 col-md-4">
<label className="form-label small mb-1 fw-bold">Exercise Duration</label>
<div className="input-group input-group-sm">
<input type="number" step="5" className="form-control text-center" placeholder="30" value={wellnessForm.exercise_min_per_day} onChange={e => setWellnessForm({...wellnessForm, exercise_min_per_day: e.target.value})} />
<span className="input-group-text">min/day</span>
</div>
</div>
</div>
)}
{/* TAB 4: LIFESTYLE & GUIDANCE */}
{wellnessModalTab === 'lifestyle' && (
<div className="row g-2.5 mb-2">
<div className="col-12 col-md-4">
<label className="form-label small mb-1 fw-bold">Diet Pattern (ABDM)</label>
<select className="form-select form-select-sm" value={wellnessForm.diet_type} onChange={e => setWellnessForm({...wellnessForm, diet_type: e.target.value})}>
<option value="">Select diet...</option>
<option value="Balanced Diet">Balanced Diet</option>
<option value="Vegetarian">Vegetarian</option>
<option value="Lacto-Vegetarian">Lacto-Vegetarian</option>
<option value="Non-Vegetarian">Non-Vegetarian</option>
<option value="Vegan">Vegan</option>
<option value="Diabetic Diet">Diabetic Diet (Low GI)</option>
<option value="Low Sodium / DASH Diet">Low Sodium / DASH Diet</option>
<option value="Renal Diet">Renal Diet</option>
</select>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Tobacco / Smoking</label>
<select className="form-select form-select-sm" value={wellnessForm.tobacco_status} onChange={e => setWellnessForm({...wellnessForm, tobacco_status: e.target.value})}>
<option value="">Select status...</option>
<option value="Never Smoked">Never Smoked</option>
<option value="Former / Ex-Smoker">Former / Ex-Smoker</option>
<option value="Current Smoker (Occasional)">Current Smoker (Occasional)</option>
<option value="Current Smoker (Daily)">Current Smoker (Daily)</option>
<option value="Tobacco Chewer">Tobacco Chewer</option>
</select>
</div>
<div className="col-6 col-md-4">
<label className="form-label small mb-1 fw-bold">Alcohol Consumption</label>
<select className="form-select form-select-sm" value={wellnessForm.alcohol_status} onChange={e => setWellnessForm({...wellnessForm, alcohol_status: e.target.value})}>
<option value="">Select status...</option>
<option value="Lifetime Non-Drinker">Lifetime Non-Drinker</option>
<option value="Former Drinker">Former Drinker</option>
<option value="Occasional / Social Drinker">Occasional / Social Drinker</option>
<option value="Regular Drinker">Regular Drinker</option>
</select>
</div>
<div className="col-12">
<label className="form-label small mb-1 fw-bold">Diet &amp; Lifestyle Guidance (ABDM)</label>
<input type="text" className="form-control form-control-sm" placeholder="e.g. Low sodium diet, 30 min daily brisk walking, adequate hydration" value={wellnessForm.diet_lifestyle_note} onChange={e => setWellnessForm({...wellnessForm, diet_lifestyle_note: e.target.value})} />
</div>
<div className="col-12">
<label className="form-label small mb-1 fw-bold">General Nursing Observations</label>
<input type="text" className="form-control form-control-sm" placeholder="e.g. Patient conscious, oriented to time/place, vitals within acceptable limits" value={wellnessForm.general_advice} onChange={e => setWellnessForm({...wellnessForm, general_advice: e.target.value})} />
</div>
</div>
)}
{/* TAB 5: WOMEN'S HEALTH */}
{wellnessModalTab === 'women' && (
<div className="row g-2.5 mb-2">
<div className="col-12 col-md-6">
<label className="form-label small mb-1 fw-bold">Last Menstrual Period (LMP Date)</label>
<input type="date" className="form-control form-control-sm" value={wellnessForm.women_lmp} onChange={e => setWellnessForm({...wellnessForm, women_lmp: e.target.value})} />
</div>
<div className="col-12 col-md-6">
<label className="form-label small mb-1 fw-bold">Pregnancy / Reproductive Status</label>
<select className="form-select form-select-sm" value={wellnessForm.women_pregnancy_status} onChange={e => setWellnessForm({...wellnessForm, women_pregnancy_status: e.target.value})}>
<option value="">Select reproductive status...</option>
<option value="Not Pregnant">Not Pregnant</option>
<option value="Pregnant - 1st Trimester (1 - 12 wks)">Pregnant - 1st Trimester (1 - 12 wks)</option>
<option value="Pregnant - 2nd Trimester (13 - 26 wks)">Pregnant - 2nd Trimester (13 - 26 wks)</option>
<option value="Pregnant - 3rd Trimester (27 - 40+ wks)">Pregnant - 3rd Trimester (27 - 40+ wks)</option>
<option value="Postpartum (< 6 weeks post delivery)">Postpartum (&lt; 6 weeks post delivery)</option>
<option value="Lactating / Breastfeeding">Lactating / Breastfeeding</option>
<option value="Perimenopausal">Perimenopausal</option>
<option value="Postmenopausal">Postmenopausal</option>
</select>
</div>
</div>
)}
</div>
<div className="modal-footer p-2 bg-light d-flex justify-content-between">
@ -2592,7 +2912,7 @@
</button>
<div className="d-flex gap-2">
<button className="btn btn-secondary btn-sm" onClick={() => setActiveWellnessPatient(null)}>Cancel</button>
<button className="btn btn-success btn-sm fw-bold px-3" onClick={handleSaveWellness}>
<button className="btn btn-success btn-sm fw-bold px-3 shadow-sm" onClick={handleSaveWellness}>
<i className="bi bi-check-circle-fill me-1"></i> Save ABDM Wellness Vitals
</button>
</div>
@ -2708,11 +3028,11 @@
)}
{/* -------------------------------------------------------------
MODAL 4: PATIENT VITALS & WELLNESS HISTORY
MODAL 4: PATIENT VITALS & WELLNESS HISTORY (COMPREHENSIVE)
------------------------------------------------------------- */}
{wellnessHistoryPatient && (
<div className="modal show d-block" style={{backgroundColor: 'rgba(0,0,0,0.65)', zIndex: 1070}}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-dialog modal-dialog-centered modal-xl">
<div className="modal-content rounded-4 border-0 shadow-lg overflow-hidden">
<div className="modal-header bg-dark text-white py-2.5">
<div className="d-flex align-items-center gap-2">
@ -2744,11 +3064,11 @@
<thead className="table-light">
<tr>
<th>Date &amp; Time</th>
<th>BP</th>
<th>Pulse</th>
<th>Temp</th>
<th>SpO2</th>
<th>BMI</th>
<th>Vitals (BP / Pulse / SpO2 / Temp)</th>
<th>Body / BMI</th>
<th>Blood Glucose (mg/dL)</th>
<th>POC Labs</th>
<th>Activity / Lifestyle</th>
<th>Staff</th>
<th>Advice / Remarks</th>
</tr>
@ -2757,13 +3077,57 @@
{wellnessHistoryList.map(rec => (
<tr key={rec.id}>
<td className="fw-semibold text-nowrap">{formatDateTime(rec.recorded_at)}</td>
<td><span className="badge bg-light text-dark border">{rec.bp_systolic ? `${rec.bp_systolic}/${rec.bp_diastolic}` : '-'}</span></td>
<td>{rec.pulse_rate ? `${rec.pulse_rate} bpm` : '-'}</td>
<td><span className="badge bg-danger-subtle text-danger">{rec.temperature_f ? `${rec.temperature_f} °F` : '-'}</span></td>
<td>{rec.spo2 ? `${rec.spo2}%` : '-'}</td>
<td><span className="badge bg-info-subtle text-info">{rec.bmi || '-'}</span></td>
<td>
<div className="d-flex flex-wrap gap-1 align-items-center">
{rec.bp_systolic && <span className="badge bg-light text-dark border">BP {rec.bp_systolic}/{rec.bp_diastolic}</span>}
{rec.pulse_rate && <span className="badge bg-light text-dark border">{rec.pulse_rate} bpm</span>}
{rec.spo2 && <span className="badge bg-info-subtle text-info border border-info-subtle">{rec.spo2}% O2</span>}
{rec.temperature_f && <span className="badge bg-danger-subtle text-danger border border-danger-subtle">{rec.temperature_f}°F</span>}
{rec.pain_score !== null && rec.pain_score !== undefined && rec.pain_score !== '' && (
<span className={`badge ${rec.pain_score == 0 ? 'bg-success-subtle text-success' : rec.pain_score <= 3 ? 'bg-info-subtle text-info' : 'bg-warning-subtle text-warning-emphasis'}`}>
Pain: {rec.pain_score}/10
</span>
)}
</div>
</td>
<td>
<div>
{rec.bmi && <span className="badge bg-secondary-subtle text-secondary me-1">BMI {rec.bmi}</span>}
{rec.weight_kg && <small className="text-muted">{rec.weight_kg}kg </small>}
{rec.height_cm && <small className="text-muted">{rec.height_cm}cm</small>}
</div>
{(rec.waist_circumference_cm || rec.waist_hip_ratio) && (
<small className="text-muted d-block" style={{fontSize: '11px'}}>
{rec.waist_circumference_cm ? `Waist: ${rec.waist_circumference_cm}cm` : ''}
{rec.waist_hip_ratio ? ` • WHR: ${rec.waist_hip_ratio}` : ''}
</small>
)}
</td>
<td>
<div className="d-flex flex-wrap gap-1">
{rec.sugar_random && <span className="badge bg-warning-subtle text-warning-emphasis border">RBS: {rec.sugar_random}</span>}
{rec.sugar_fasting && <span className="badge bg-primary-subtle text-primary border">FBS: {rec.sugar_fasting}</span>}
{rec.sugar_pp && <span className="badge bg-secondary-subtle text-secondary border">PP: {rec.sugar_pp}</span>}
{!rec.sugar_random && !rec.sugar_fasting && !rec.sugar_pp && <span className="text-muted">-</span>}
</div>
</td>
<td>
<div className="d-flex flex-wrap gap-1">
{rec.hemoglobin && <span className="badge bg-danger-subtle text-danger border">Hb: {rec.hemoglobin} g/dL</span>}
{rec.hba1c && <span className="badge bg-success-subtle text-success border">A1c: {rec.hba1c}%</span>}
{!rec.hemoglobin && !rec.hba1c && <span className="text-muted">-</span>}
</div>
</td>
<td>
<div className="small">
{rec.diet_type && <span className="badge bg-light text-dark border me-1">{rec.diet_type}</span>}
{rec.daily_steps && <span className="text-muted me-1">{rec.daily_steps} steps</span>}
{rec.sleep_hours && <span className="text-muted">{rec.sleep_hours}h sleep</span>}
{!rec.diet_type && !rec.daily_steps && !rec.sleep_hours && <span className="text-muted">-</span>}
</div>
</td>
<td><small className="text-muted">{rec.recorded_by || 'Staff'}</small></td>
<td><small>{rec.general_advice || rec.diet_lifestyle_note || '-'}</small></td>
<td><small className="text-truncate d-inline-block" style={{maxWidth: '180px'}} title={rec.general_advice || rec.diet_lifestyle_note}>{rec.general_advice || rec.diet_lifestyle_note || '-'}</small></td>
</tr>
))}
</tbody>

View File

@ -1,68 +1,93 @@
<?php
require_once 'd:/Workplace/HMS_CI4_OLD/vendor/autoload.php';
defined('FCPATH') || define('FCPATH', 'd:/Workplace/HMS_CI4_OLD/public/');
require_once 'd:/Workplace/HMS_CI4_OLD/vendor/codeigniter4/framework/system/Test/bootstrap.php';
use App\Libraries\Abdm\Fhir\FhirGeneratorFactory;
$factory = new FhirGeneratorFactory();
$generator = $factory->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";
}