diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 774f607..d772259 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -6469,6 +6469,25 @@ class AbdmGateway extends BaseController } } } elseif ($taskType === 'wellness_record_publish') { + if ($this->db->tableExists('patient_wellness_records')) { + $wRec = $this->db->table('patient_wellness_records')->where('id', $taskEntityId)->get(1)->getRowArray(); + if (! empty($wRec)) { + $vDate = ! empty($wRec['recorded_at']) ? $wRec['recorded_at'] : date('Y-m-d'); + $dateStr = date('d M Y', strtotime((string) $vDate)); + $cleanDate = date('Ymd', strtotime((string) $vDate)); + $ccRef = ! empty($wRec['care_context_reference']) ? $wRec['care_context_reference'] : ('WELLNESS-' . $patientId . '-W' . $taskEntityId . '-' . $cleanDate); + $display = 'Wellness Record - ' . $dateStr; + $addContext([ + 'careContextId' => $ccRef, + 'referenceNumber' => $ccRef, + 'display' => $display, + 'record_type' => 'WellnessRecord', + 'patient_id' => $patientId, + 'is_fhir_ready' => true, + 'is_primary' => true, + ]); + } + } if ($this->db->tableExists('opd_prescription')) { $wopd = $this->db->table('opd_prescription')->where('id', $taskEntityId)->get(1)->getRowArray(); if (! empty($wopd)) { @@ -9244,7 +9263,7 @@ class AbdmGateway extends BaseController }; } - private function pushAdditionalHiRecord(array $payload, int $patientId, string $abhaId, string $consentHandle) + public function pushAdditionalHiRecord(array $payload, int $patientId, string $abhaId, string $consentHandle = '') { $abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId); $abhaNumber = $abhaIdentity['abha_id']; @@ -9394,6 +9413,42 @@ class AbdmGateway extends BaseController return null; } + // 1. Check patient_wellness_records first (New M2 Comprehensive Wellness & Vitals) + if ($this->db->tableExists('patient_wellness_records')) { + $wBuilder = $this->db->table('patient_wellness_records')->where('patient_id', $patientId); + if ($opdId > 0) { + $wBuilder->groupStart() + ->where('id', $opdId) + ->orWhere('patient_id', $patientId) + ->groupEnd(); + } + $wRow = $wBuilder->orderBy('id', 'DESC')->get(1)->getRowArray(); + if (! empty($wRow)) { + $bundle = ! empty($wRow['fhir_bundle_json']) ? json_decode((string) $wRow['fhir_bundle_json'], true) : []; + $visitDate = date('Y-m-d', strtotime((string) ($wRow['recorded_at'] ?? 'now'))); + $entityId = (string) $wRow['id']; + $cleanDate = date('Ymd', strtotime($visitDate)); + $ccRef = ! empty($wRow['care_context_reference']) + ? (string) $wRow['care_context_reference'] + : ('WELLNESS-' . $patientId . '-W' . $entityId . '-' . $cleanDate); + $ccDisplay = 'Wellness Record - ' . date('d/m/Y', strtotime($visitDate)); + + if (! empty($bundle)) { + return [ + 'hi_type' => 'WellnessRecord', + 'entity_type' => 'wellness', + 'entity_id' => $entityId, + 'patient_id' => $patientId, + 'patient_name' => $this->patientDisplayName($patientRow), + 'visit_date' => $visitDate, + 'care_context_reference' => $ccRef, + 'care_context_display' => $ccDisplay, + 'bundle' => $bundle, + ]; + } + } + } + $row = []; if ($this->db->tableExists('opd_prescription')) { $builder = $this->db->table('opd_prescription')->where('p_id', $patientId); diff --git a/app/Controllers/AbdmTaskBoard.php b/app/Controllers/AbdmTaskBoard.php index 545bc02..a6351b5 100644 --- a/app/Controllers/AbdmTaskBoard.php +++ b/app/Controllers/AbdmTaskBoard.php @@ -1776,6 +1776,71 @@ class AbdmTaskBoard extends BaseController private function backfillWellnessTasks(): void { + // 1. Backfill from patient_wellness_records (ABDM M2 Comprehensive Wellness & Vitals) + if ($this->db->tableExists('patient_wellness_records') && $this->db->tableExists('patient_master')) { + $patientFields = $this->db->getFieldNames('patient_master') ?? []; + $abhaCol = $this->resolveExistingColumn($patientFields, ['abha_address', 'abha_id', 'abha_no', 'abha']); + if ($abhaCol !== null) { + $wellnessRows = $this->db->table('patient_wellness_records w') + ->select('w.id, w.patient_id, w.recorded_at, w.care_context_reference, w.bridge_record_id, w.abdm_status, p.p_fname, p.p_lname, p.' . $abhaCol . ' as abha_id', false) + ->join('patient_master p', 'p.id = w.patient_id', 'inner') + ->where('p.' . $abhaCol . ' !=', '') + ->orderBy('w.id', 'DESC') + ->limit(200) + ->get() + ->getResultArray(); + + foreach ($wellnessRows as $wRow) { + $patientId = (int) ($wRow['patient_id'] ?? 0); + $wellnessId = (int) ($wRow['id'] ?? 0); + $abhaId = trim((string) ($wRow['abha_id'] ?? '')); + if ($patientId <= 0 || $wellnessId <= 0 || ! $this->isValidAbhaNumber($abhaId)) { + continue; + } + + $exists = $this->db->table('abdm_work_tasks') + ->select('id, status') + ->where('task_type', 'wellness_record_publish') + ->where('entity_type', 'wellness') + ->where('entity_id', (string) $wellnessId) + ->get(1) + ->getRowArray(); + if (! empty($exists)) { + continue; + } + + $patientName = trim(($wRow['p_fname'] ?? '') . ' ' . ($wRow['p_lname'] ?? '')); + $isPushed = (! empty($wRow['bridge_record_id']) || in_array($wRow['abdm_status'] ?? '', ['queued', 'pushed', 'linked'], true)); + $taskId = $this->taskService->createOrRefreshTask( + 'wellness_record_publish', + 'patient_wellness_records', + 'wellness', + (string) $wellnessId, + $patientId, + $patientName, + $abhaId, + 'submit', + [ + 'wellness_id' => $wellnessId, + 'care_context_reference' => (string) ($wRow['care_context_reference'] ?? ''), + 'bridge_record_id' => (int) ($wRow['bridge_record_id'] ?? 0), + 'clinical_timestamp' => (string) ($wRow['recorded_at'] ?? ''), + 'trigger' => 'wellness_records.backfilled', + ] + ); + + if ($isPushed && $taskId > 0) { + $this->db->table('abdm_work_tasks')->where('id', $taskId)->update([ + 'status' => 'completed', + 'result_summary' => 'Pushed to ABDM Bridge (ID #' . ($wRow['bridge_record_id'] ?? '') . ')', + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + } + } + } + + // 2. Also backfill from opd_prescription (for legacy OPD vitals) if (! $this->db->tableExists('opd_prescription') || ! $this->db->tableExists('patient_master')) { return; } diff --git a/app/Controllers/Api/v1/NursingApi.php b/app/Controllers/Api/v1/NursingApi.php index bc4d509..a42189d 100644 --- a/app/Controllers/Api/v1/NursingApi.php +++ b/app/Controllers/Api/v1/NursingApi.php @@ -1068,17 +1068,22 @@ class NursingApi extends BaseController $fhirBundle = $genResult['fhir_bundle'] ?? []; if (! empty($fhirBundle)) { $fhirBundleJson = json_encode($fhirBundle, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - $db->table('patient_wellness_records')->where('id', $wellnessId)->update(['fhir_bundle_json' => $fhirBundleJson]); + $db->table('patient_wellness_records')->where('id', $wellnessId)->update([ + 'fhir_bundle_json' => $fhirBundleJson, + 'care_context_reference' => $careContextRef, + ]); } } catch (\Throwable $e) { log_message('warning', '[savePatientWellness] FHIR generation error: ' . $e->getMessage()); } - // Also register in health_records for ABDM discovery & push + // Register in health_records for ABDM discovery & push + $healthRecordId = 0; if ($db->tableExists('health_records')) { + $effectiveAbha = trim((string) ($patient['abha_address'] ?? $patient['abha_id'] ?? '')); $db->table('health_records')->insert([ 'patient_id' => $patientId, - 'abha_id' => $patient['abha_address'] ?? $patient['abha_id'] ?? null, + 'abha_id' => $effectiveAbha !== '' ? $effectiveAbha : null, 'hi_type' => 'WellnessRecord', 'entity_type' => 'wellness', 'entity_id' => (string) $wellnessId, @@ -1089,13 +1094,113 @@ class NursingApi extends BaseController 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); + $healthRecordId = (int) $db->insertID(); + } + + // Proactive push to ABDM Bridge when patient has ABHA ID or Address + $pushStatus = 'local_discovery_ready'; + $bridgeRecordId = null; + $queueId = null; + $pushMsg = ''; + $effectiveAbha = trim((string) ($patient['abha_address'] ?? $patient['abha_id'] ?? '')); + + if ($effectiveAbha !== '') { + try { + $connector = \App\Libraries\Abdm\AbdmConnectorFactory::make(); + $pushPayload = [ + 'patient_id' => (string) $patientId, + 'patient_name' => trim(($patient['p_fname'] ?? '') . ' ' . ($patient['p_lname'] ?? '')), + 'abha_id' => (string) ($patient['abha_id'] ?? ''), + 'abha_address' => (string) ($patient['abha_address'] ?? ''), + 'year_of_birth' => ! empty($patient['dob']) ? date('Y', strtotime((string) $patient['dob'])) : null, + 'hi_type' => 'WellnessRecord', + 'record_type' => 'WellnessRecord', + 'visit_date' => date('Y-m-d', strtotime($recordedAt)), + 'care_context_reference' => $careContextRef, + 'care_context_display' => 'Wellness Record - ' . date('d/m/Y', strtotime($recordedAt)), + 'notes' => 'Wellness & Vitals Record', + 'queue_id' => $careContextRef, + 'record_data' => ! empty($fhirBundle) ? $fhirBundle : json_decode((string) ($fhirBundleJson ?? '{}'), true), + ]; + $pushRes = $connector->pushRecord($pushPayload); + $pushOk = (int) ($pushRes['ok'] ?? 0); + $httpCode = (int) ($pushRes['http_code'] ?? 0); + $statusVal = strtolower((string) ($pushRes['status'] ?? '')); + + if ($pushOk === 1 || in_array($httpCode, [200, 201, 202, 409], true) || in_array($statusVal, ['queued', 'pushed', 'linked'], true)) { + $pushStatus = 'queued'; + $bridgeRecordId = (int) ($pushRes['record_id'] ?? 0); + $queueId = (string) ($pushRes['queue_id'] ?? ''); + $pushMsg = ' & pushed to ABDM Bridge' . ($bridgeRecordId > 0 ? " (Record #{$bridgeRecordId})" : ''); + } else { + $pushStatus = 'failed'; + $pushMsg = ' (ABDM Bridge push pending: ' . ($pushRes['error_text'] ?? 'Connection error') . ')'; + } + } catch (\Throwable $pe) { + log_message('warning', '[savePatientWellness] Bridge push error: ' . $pe->getMessage()); + $pushStatus = 'failed'; + } + } + + // Update patient_wellness_records with push status & references + $db->table('patient_wellness_records')->where('id', $wellnessId)->update([ + 'care_context_reference' => $careContextRef, + 'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null, + 'queue_id' => $queueId !== '' ? $queueId : null, + 'abdm_status' => $pushStatus, + ]); + + // Update health_records + if ($healthRecordId > 0) { + $db->table('health_records')->where('id', $healthRecordId)->update([ + 'push_status' => $pushStatus, + 'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null, + 'abdm_txn_id' => $queueId !== '' ? $queueId : null, + 'push_at' => $pushStatus === 'queued' ? date('Y-m-d H:i:s') : null, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + + // Create or refresh task in abdm_work_tasks + if ($db->tableExists('abdm_work_tasks')) { + try { + $taskService = new \App\Libraries\AbdmWorkTaskService(); + $tId = $taskService->createOrRefreshTask( + 'wellness_record_publish', + 'patient_wellness_records', + 'wellness', + (string) $wellnessId, + $patientId, + trim(($patient['p_fname'] ?? '') . ' ' . ($patient['p_lname'] ?? '')), + $effectiveAbha, + 'submit', + [ + 'wellness_id' => $wellnessId, + 'care_context_reference' => $careContextRef, + 'bridge_record_id' => $bridgeRecordId, + 'clinical_timestamp' => $recordedAt, + 'trigger' => 'nursing.wellness_saved', + ] + ); + if ($pushStatus === 'queued' && $tId > 0) { + $db->table('abdm_work_tasks')->where('id', $tId)->update([ + 'status' => 'completed', + 'result_summary' => 'Pushed to ABDM Bridge (ID #' . $bridgeRecordId . ', Queue: ' . $queueId . ')', + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + } catch (\Throwable $te) { + log_message('warning', '[savePatientWellness] Task create error: ' . $te->getMessage()); + } } return $this->response->setJSON([ 'status' => 1, - 'message' => 'ABDM M2 Wellness & Vitals recorded successfully', + 'message' => 'ABDM M2 Wellness & Vitals recorded' . $pushMsg, 'wellness_id' => $wellnessId, 'care_context_reference' => $careContextRef, + 'abdm_push_status' => $pushStatus, + 'bridge_record_id' => $bridgeRecordId, 'bmi' => $bmi, ]); } diff --git a/app/Controllers/DoctorDocument.php b/app/Controllers/DoctorDocument.php index 3f73544..ad9727c 100644 --- a/app/Controllers/DoctorDocument.php +++ b/app/Controllers/DoctorDocument.php @@ -659,6 +659,36 @@ class DoctorDocument extends BaseController return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']); } + // Check if there is an existing patient_wellness_records row with fhir_bundle_json + if ($this->db->tableExists('patient_wellness_records')) { + $wBuilder = $this->db->table('patient_wellness_records')->where('patient_id', $patientId); + if ($opdSessionId > 0) { + $wBuilder->groupStart() + ->where('id', $opdSessionId) + ->orWhere('patient_id', $patientId) + ->groupEnd(); + } + $wRow = $wBuilder->orderBy('id', 'DESC')->get(1)->getRowArray(); + if (! empty($wRow['fhir_bundle_json'])) { + $decoded = json_decode((string) $wRow['fhir_bundle_json'], true); + if (is_array($decoded) && ! empty($decoded)) { + $cleanDate = date('Ymd', strtotime((string) ($wRow['recorded_at'] ?? 'now'))); + $ccRef = ! empty($wRow['care_context_reference']) + ? (string) $wRow['care_context_reference'] + : ('WELLNESS-' . $patientId . '-W' . $wRow['id'] . '-' . $cleanDate); + return $this->response->setJSON([ + 'ok' => 1, + 'source' => $wRow, + 'bundle' => $decoded, + 'fhir_bundle' => $decoded, + 'validation' => ['valid' => true, 'errors' => []], + 'care_context_reference' => $ccRef, + 'care_context_display' => 'Wellness Record - ' . date('d/m/Y', strtotime((string) ($wRow['recorded_at'] ?? 'now'))), + ]); + } + } + } + $source = $this->buildWellnessRecordSource($patientId, $opdSessionId); if (empty($source)) { return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Wellness source data (vitals/prescription) not found for patient #' . $patientId]); @@ -744,6 +774,40 @@ class DoctorDocument extends BaseController $addVital('59408-5', 'Oxygen saturation in Arterial blood by Pulse oximetry', $rxRow['spo2'] ?? null, '%', '%'); $addVital('2339-0', 'Blood Glucose', $rxRow['glucose'] ?? null, 'mg/dL', 'mg/dL'); + // If no vitals found in opd_prescription, check patient_wellness_records + if (empty($vitals) && $this->db->tableExists('patient_wellness_records')) { + $wBuilder = $this->db->table('patient_wellness_records')->where('patient_id', $patientId); + if ($opdSessionId > 0) { + $wBuilder->groupStart() + ->where('id', $opdSessionId) + ->orWhere('patient_id', $patientId) + ->groupEnd(); + } + $wRow = $wBuilder->orderBy('id', 'DESC')->get(1)->getRowArray(); + if (! empty($wRow)) { + $addVital('8480-6', 'Systolic blood pressure', $wRow['bp_systolic'] ?? null, 'mmHg', 'mm[Hg]'); + $addVital('8462-4', 'Diastolic blood pressure', $wRow['bp_diastolic'] ?? null, 'mmHg', 'mm[Hg]'); + $addVital('8867-4', 'Heart rate', $wRow['pulse_rate'] ?? null, '/min', '/min'); + $addVital('8302-2', 'Body height', $wRow['height_cm'] ?? null, 'cm', 'cm'); + $addVital('29463-7', 'Body weight', $wRow['weight_kg'] ?? null, 'kg', 'kg'); + $addVital('39156-5', 'Body Mass Index', $wRow['bmi'] ?? null, 'kg/m2', 'kg/m2'); + $tC = $wRow['temperature_c'] ?? null; + if ($tC === null && ! empty($wRow['temperature_f'])) { + $tC = round((((float) $wRow['temperature_f'] - 32) * 5) / 9, 1); + } + $addVital('8310-5', 'Body temperature', $tC, 'Cel', 'Cel'); + $addVital('9279-1', 'Respiratory rate', $wRow['resp_rate'] ?? null, '/min', '/min'); + $addVital('59408-5', 'Oxygen saturation in Arterial blood by Pulse oximetry', $wRow['spo2'] ?? null, '%', '%'); + $addVital('2339-0', 'Random Blood Glucose', $wRow['sugar_random'] ?? null, 'mg/dL', 'mg/dL'); + $addVital('1558-6', 'Fasting Blood Glucose', $wRow['sugar_fasting'] ?? null, 'mg/dL', 'mg/dL'); + $addVital('4548-4', 'Hemoglobin A1c', $wRow['hba1c'] ?? null, '%', '%'); + $addVital('718-7', 'Hemoglobin', $wRow['hemoglobin'] ?? null, 'g/dL', 'g/dL'); + $addVital('8280-0', 'Waist Circumference', $wRow['waist_circumference_cm'] ?? null, 'cm', 'cm'); + $addVital('98337-9', 'Hip Circumference', $wRow['hip_circumference_cm'] ?? null, 'cm', 'cm'); + $addVital('72287-6', 'Waist-hip ratio', $wRow['waist_hip_ratio'] ?? null, '{ratio}', '{ratio}'); + } + } + $physicalExam = []; foreach (['complaints', 'diagnosis', 'investigation'] as $f) { $val = trim((string) ($rxRow[$f] ?? '')); diff --git a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php index df19268..f543ef2 100644 --- a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php +++ b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php @@ -835,6 +835,16 @@ class AbdmTaskBoardSyncService $careContextRef = trim((string) $immRec['abdm_care_context_reference']); } } + if ($taskType === 'wellness_record_publish' && $this->db->tableExists('patient_wellness_records')) { + $wRec = $this->db->table('patient_wellness_records')->where('id', (int) $entityId)->get(1)->getRowArray(); + if (! empty($wRec)) { + if (! empty($wRec['care_context_reference'])) { + $careContextRef = trim((string) $wRec['care_context_reference']); + } else { + $careContextRef = 'WELLNESS-' . $patientId . '-W' . $entityId . '-' . $cleanVisitDate; + } + } + } $careContextDisplay = $hiType . ' ' . $visitDate; $pushData = [ @@ -854,21 +864,36 @@ class AbdmTaskBoardSyncService ]; $bundleJson = null; - if ($taskType === 'wellness_record_publish' && class_exists('\App\Controllers\DoctorDocument')) { - try { - $docCtrl = new \App\Controllers\DoctorDocument(); - $wSource = $docCtrl->buildWellnessRecordSource($patientId, $entityId); - if (! empty($wSource)) { - $wFactory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory(); - $wGen = $wFactory->wellness()->generate($wSource); - if (! empty($wGen['bundle'])) { - $pushData['bundle'] = $wGen['bundle']; - $pushData['fhir_bundle'] = $wGen['bundle']; - $bundleJson = (string) json_encode($wGen['bundle'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($taskType === 'wellness_record_publish') { + if ($this->db->tableExists('patient_wellness_records')) { + $wRec = $this->db->table('patient_wellness_records')->where('id', (int) $entityId)->get(1)->getRowArray(); + if (! empty($wRec['fhir_bundle_json'])) { + $decodedBundle = json_decode((string) $wRec['fhir_bundle_json'], true); + if (is_array($decodedBundle) && ! empty($decodedBundle)) { + $pushData['bundle'] = $decodedBundle; + $pushData['fhir_bundle'] = $decodedBundle; + $pushData['record_data'] = $decodedBundle; + $bundleJson = (string) $wRec['fhir_bundle_json']; } } - } catch (\Throwable $we) { - log_message('warning', '[processIndividualWorkTask] Wellness bundle generation error: ' . $we->getMessage()); + } + if (empty($pushData['bundle']) && class_exists('\App\Controllers\DoctorDocument')) { + try { + $docCtrl = new \App\Controllers\DoctorDocument(); + $wSource = $docCtrl->buildWellnessRecordSource($patientId, $entityId); + if (! empty($wSource)) { + $wFactory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory(); + $wGen = $wFactory->wellness()->generate($wSource); + if (! empty($wGen['bundle'])) { + $pushData['bundle'] = $wGen['bundle']; + $pushData['fhir_bundle'] = $wGen['bundle']; + $pushData['record_data'] = $wGen['bundle']; + $bundleJson = (string) json_encode($wGen['bundle'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + } + } catch (\Throwable $we) { + log_message('warning', '[processIndividualWorkTask] Wellness bundle generation error: ' . $we->getMessage()); + } } } @@ -894,6 +919,15 @@ class AbdmTaskBoardSyncService $bundleJson ); + if ($taskType === 'wellness_record_publish' && $this->db->tableExists('patient_wellness_records')) { + $this->db->table('patient_wellness_records')->where('id', (int) $entityId)->update([ + 'care_context_reference' => $careContextRef, + 'bridge_record_id' => (int) ($result['record_id'] ?? 0) > 0 ? (int) $result['record_id'] : null, + 'queue_id' => $queueId, + 'abdm_status' => 'queued', + ]); + } + return ['ok' => 1, 'queue_id' => $queueId]; }