diff --git a/app/Controllers/Abha.php b/app/Controllers/Abha.php index 0ae165a..aa7293d 100644 --- a/app/Controllers/Abha.php +++ b/app/Controllers/Abha.php @@ -83,20 +83,35 @@ class Abha extends BaseController $payload = $result['data'] ?? $result; $newTxnId = $payload['txnId'] ?? $payload['txn_id'] ?? $txnId; - // aadhaar/verify-otp returns ABHAProfile when ABHA is created/found - $profile = $payload['ABHAProfile'] ?? []; - if (empty($profile) && ! empty($payload['accounts'][0])) { - $profile = $payload['accounts'][0]; // legacy fallback - } + $profile = $this->pickGatewayAbhaProfile($payload); - $abhaNum = (string) ($profile['ABHANumber'] ?? $payload['ABHANumber'] ?? ''); - $name = (string) ($profile['name'] ?? $profile['fullName'] ?? ''); - $photo = (string) ($profile['profilePhoto'] ?? ''); - $mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? ''); - $profileGender = (string) ($profile['gender'] ?? ''); - $profileDob = (string) ($profile['dob'] ?? ''); + $abhaNum = (string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? ''); + $abhaAddress = (string) ($profile['preferredAbhaAddress'] ?? $profile['abha_address'] ?? $payload['preferredAbhaAddress'] ?? $payload['abha_address'] ?? ''); + $name = (string) ($profile['name'] ?? $profile['fullName'] ?? $profile['full_name'] ?? $payload['name'] ?? $payload['full_name'] ?? ''); + $photo = (string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? ''); + $mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? ''); + $profileGender = (string) ($profile['gender'] ?? $payload['gender'] ?? ''); + $profileDob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? ''); + $verifiedStatus = (string) ($profile['verifiedStatus'] ?? $payload['verifiedStatus'] ?? ''); + $verificationType = (string) ($profile['verificationType'] ?? $payload['verificationType'] ?? ''); + $kycVerified = $profile['kycVerified'] ?? $payload['kycVerified'] ?? null; + $mobileVerified = $profile['mobileVerified'] ?? $payload['mobileVerified'] ?? null; - $patientInfo = $this->autoCreateOrFindPatient($abhaNum, $name, $mobile, $profileGender, $profileDob); + $patientInfo = $this->autoCreateOrFindPatient( + $abhaNum, + $name, + $mobile, + $profileGender, + $profileDob, + [ + 'abha_address' => $abhaAddress, + 'profile_photo' => $photo, + 'verified_status' => $verifiedStatus, + 'verification_type' => $verificationType, + 'kyc_verified' => $kycVerified, + 'mobile_verified' => $mobileVerified, + ] + ); return $this->response->setJSON([ 'ok' => 1, @@ -180,20 +195,35 @@ class Abha extends BaseController $payload = $result['data'] ?? $result; - // mobile/verify-otp returns data.profile (new format) or data.ABHAProfile / accounts[0] (legacy) - $profile = $payload['profile'] ?? $payload['ABHAProfile'] ?? []; - if (empty($profile) && ! empty($payload['accounts'][0])) { - $profile = $payload['accounts'][0]; - } + $profile = $this->pickGatewayAbhaProfile($payload); - $abhaNum = (string) ($profile['ABHANumber'] ?? $payload['ABHANumber'] ?? ''); - $name = (string) ($profile['name'] ?? $profile['fullName'] ?? $payload['name'] ?? ''); - $photo = (string) ($profile['profilePhoto'] ?? $payload['profilePhoto'] ?? ''); - $gender = (string) ($profile['gender'] ?? $payload['gender'] ?? ''); - $dob = (string) ($profile['dob'] ?? $payload['dob'] ?? ''); - $mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? ''); + $abhaNum = (string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? ''); + $abhaAddress = (string) ($profile['preferredAbhaAddress'] ?? $profile['abha_address'] ?? $payload['preferredAbhaAddress'] ?? $payload['abha_address'] ?? ''); + $name = (string) ($profile['name'] ?? $profile['fullName'] ?? $profile['full_name'] ?? $payload['name'] ?? $payload['full_name'] ?? ''); + $photo = (string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? ''); + $gender = (string) ($profile['gender'] ?? $payload['gender'] ?? ''); + $dob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? ''); + $mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? ''); + $verifiedStatus = (string) ($profile['verifiedStatus'] ?? $payload['verifiedStatus'] ?? ''); + $verificationType = (string) ($profile['verificationType'] ?? $payload['verificationType'] ?? ''); + $kycVerified = $profile['kycVerified'] ?? $payload['kycVerified'] ?? null; + $mobileVerified = $profile['mobileVerified'] ?? $payload['mobileVerified'] ?? null; - $patientInfo = $this->autoCreateOrFindPatient($abhaNum, $name, $mobile, $gender, $dob); + $patientInfo = $this->autoCreateOrFindPatient( + $abhaNum, + $name, + $mobile, + $gender, + $dob, + [ + 'abha_address' => $abhaAddress, + 'profile_photo' => $photo, + 'verified_status' => $verifiedStatus, + 'verification_type' => $verificationType, + 'kyc_verified' => $kycVerified, + 'mobile_verified' => $mobileVerified, + ] + ); return $this->response->setJSON([ 'ok' => 1, @@ -287,7 +317,8 @@ class Abha extends BaseController string $name, string $mobile, string $gender, - string $dob + string $dob, + array $abhaMeta = [] ): array { $db = \Config\Database::connect(); $fields = $db->getFieldNames('patient_master') ?? []; @@ -301,7 +332,8 @@ class Abha extends BaseController $abhaField = 'abha_address'; } - $abhaNumClean = preg_replace('/\D/', '', $abhaNum); // strip dashes → 14 digits + $abhaNumClean = preg_replace('/\D/', '', $abhaNum); // strip dashes -> 14 digits + $abhaAddress = trim((string) ($abhaMeta['abha_address'] ?? '')); // 1. Search by ABHA number $existing = null; @@ -329,21 +361,58 @@ class Abha extends BaseController ->where('id', $patientId) ->update([$abhaField => $abhaNumClean]); } + + $this->syncAbhaMetaToPatient($patientId, $abhaMeta, $abhaNumClean, $abhaAddress, $fields); return ['patient_id' => $patientId, 'p_code' => $pCode, 'is_new' => false]; } + // 2b. Safe repair path for orphan placeholder rows created during prior failed mapping. + // Reuse only when there is exactly one unambiguous candidate with no clinical linkage. + $placeholder = $this->findRepairablePlaceholderPatient($db, $fields); + if ($placeholder !== null) { + $patientId = (int) ($placeholder['id'] ?? 0); + if ($patientId > 0) { + $repairData = [ + 'p_fname' => strtoupper($name !== '' ? $name : 'ABHA PATIENT'), + 'mphone1' => $mobile, + 'gender' => $this->toPatientGenderValue($gender), + 'estimate_dob' => 1, + ]; + + $dobDb = $this->normalizeDobToDb($dob); + if ($dobDb !== '') { + $repairData['dob'] = $dobDb; + $repairData['estimate_dob'] = 0; + } + + if ($abhaField && $abhaNumClean !== '') { + $repairData[$abhaField] = $abhaNumClean; + } + if ($abhaAddress !== '' && in_array('abha_address', $fields, true)) { + $repairData['abha_address'] = $abhaAddress; + } + + $this->applyAbhaMetaColumns($repairData, $abhaMeta, $fields); + if (($abhaNumClean !== '' || $abhaAddress !== '') && in_array('abdm_linked_at', $fields, true)) { + $repairData['abdm_linked_at'] = date('Y-m-d H:i:s'); + } + + $db->table('patient_master')->where('id', $patientId)->update($repairData); + + $row = $db->table('patient_master')->select('p_code')->where('id', $patientId)->get()->getRowArray(); + $pCode = (string) ($row['p_code'] ?? ''); + + $this->syncAbhaMetaToPatient($patientId, $abhaMeta, $abhaNumClean, $abhaAddress, $fields); + + return ['patient_id' => $patientId, 'p_code' => $pCode, 'is_new' => false]; + } + } + // 3. Create new patient row from ABHA profile data - $genderDb = 1; // default Male - $genderAbs = strtoupper($gender); - if ($genderAbs === 'F' || $genderAbs === '2') { $genderDb = 2; } + $genderDb = $this->toPatientGenderValue($gender); // Convert DOB to MySQL YYYY-MM-DD - $dobDb = ''; - if (preg_match('/^(\d{2})-(\d{2})-(\d{4})$/', $dob, $m)) { - $dobDb = $m[3] . '-' . $m[2] . '-' . $m[1]; // DD-MM-YYYY → YYYY-MM-DD - } elseif (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dob)) { - $dobDb = $dob; - } + $dobDb = $this->normalizeDobToDb($dob); $insertData = [ 'p_fname' => strtoupper($name !== '' ? $name : 'ABHA PATIENT'), @@ -361,6 +430,14 @@ class Abha extends BaseController if ($abhaField && $abhaNumClean !== '') { $insertData[$abhaField] = $abhaNumClean; } + if ($abhaAddress !== '' && in_array('abha_address', $fields, true)) { + $insertData['abha_address'] = $abhaAddress; + } + + $this->applyAbhaMetaColumns($insertData, $abhaMeta, $fields); + if (($abhaNumClean !== '' || $abhaAddress !== '') && in_array('abdm_linked_at', $fields, true)) { + $insertData['abdm_linked_at'] = date('Y-m-d H:i:s'); + } $patientModel = new \App\Models\PatientModel(); $patientId = $patientModel->insertPatient($insertData); @@ -370,9 +447,198 @@ class Abha extends BaseController $pCode = (string) ($row['p_code'] ?? ''); } + if ($patientId > 0) { + $this->syncAbhaMetaToPatient($patientId, $abhaMeta, $abhaNumClean, $abhaAddress, $fields); + } + return ['patient_id' => $patientId, 'p_code' => $pCode, 'is_new' => true]; } + private function syncAbhaMetaToPatient(int $patientId, array $abhaMeta, string $abhaNumClean, string $abhaAddress, array $fields): void + { + if ($patientId <= 0) { + return; + } + + $db = \Config\Database::connect(); + + $updates = []; + if ($abhaAddress !== '' && in_array('abha_address', $fields, true)) { + $updates['abha_address'] = $abhaAddress; + } + + $this->applyAbhaMetaColumns($updates, $abhaMeta, $fields); + + if (($abhaNumClean !== '' || $abhaAddress !== '') && in_array('abdm_linked_at', $fields, true)) { + $updates['abdm_linked_at'] = date('Y-m-d H:i:s'); + } + + if ($updates !== []) { + $db->table('patient_master')->where('id', $patientId)->update($updates); + } + } + + private function applyAbhaMetaColumns(array &$target, array $abhaMeta, array $fields): void + { + $verifiedStatus = trim((string) ($abhaMeta['verified_status'] ?? '')); + if ($verifiedStatus !== '' && in_array('abha_verified_status', $fields, true)) { + $target['abha_verified_status'] = strtoupper($verifiedStatus); + } + + $verificationType = trim((string) ($abhaMeta['verification_type'] ?? '')); + if ($verificationType !== '' && in_array('abha_verification_type', $fields, true)) { + $target['abha_verification_type'] = strtoupper($verificationType); + } + + if (array_key_exists('kyc_verified', $abhaMeta) && in_array('abha_kyc_verified', $fields, true)) { + $target['abha_kyc_verified'] = $this->toDbBool($abhaMeta['kyc_verified']); + } + + if (array_key_exists('mobile_verified', $abhaMeta) && in_array('abha_mobile_verified', $fields, true)) { + $target['abha_mobile_verified'] = $this->toDbBool($abhaMeta['mobile_verified']); + } + + $profilePhoto = trim((string) ($abhaMeta['profile_photo'] ?? '')); + if ($profilePhoto !== '' && in_array('abha_profile_photo_base64', $fields, true)) { + $target['abha_profile_photo_base64'] = $profilePhoto; + } + } + + private function toDbBool($value): int + { + if (is_bool($value)) { + return $value ? 1 : 0; + } + + $v = strtolower(trim((string) $value)); + if ($v === '1' || $v === 'true' || $v === 'yes' || $v === 'y') { + return 1; + } + + return 0; + } + + private function toPatientGenderValue(string $gender): int + { + $genderAbs = strtoupper(trim($gender)); + if ($genderAbs === 'F' || $genderAbs === '2' || $genderAbs === 'FEMALE') { + return 2; + } + if ($genderAbs === 'O' || $genderAbs === '3' || $genderAbs === 'OTHER') { + return 3; + } + + return 1; + } + + private function normalizeDobToDb(string $dob): string + { + $dob = trim($dob); + if ($dob === '') { + return ''; + } + + if (preg_match('/^(\d{2})-(\d{2})-(\d{4})$/', $dob, $m)) { + return $m[3] . '-' . $m[2] . '-' . $m[1]; + } + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dob)) { + return $dob; + } + + return ''; + } + + /** + * Find one safe placeholder candidate to repair. + * Conditions: + * - name is ABHA PATIENT + * - ABHA id and address are empty + * - no OPD/invoice linkage + * - inserted recently (last 24h) + * - exactly one candidate (to avoid accidental overwrite) + * + * @param array $fields + * @return array|null + */ + private function findRepairablePlaceholderPatient($db, array $fields): ?array + { + if (! $db->tableExists('patient_master')) { + return null; + } + + $abhaField = null; + foreach (['abha_id', 'abha_no', 'abha'] as $f) { + if (in_array($f, $fields, true)) { + $abhaField = $f; + break; + } + } + + $builder = $db->table('patient_master p') + ->select('p.id,p.p_code,p.insert_date') + ->where('UPPER(TRIM(COALESCE(p.p_fname, "")))', 'ABHA PATIENT') + ->where('(p.mphone1 IS NULL OR TRIM(p.mphone1) = "")', null, false) + ->where('p.insert_date >= DATE_SUB(NOW(), INTERVAL 24 HOUR)', null, false); + + if ($abhaField !== null) { + $builder->where('(p.' . $abhaField . ' IS NULL OR TRIM(p.' . $abhaField . ') = "")', null, false); + } + if (in_array('abha_address', $fields, true)) { + $builder->where('(p.abha_address IS NULL OR TRIM(p.abha_address) = "")', null, false); + } + + if ($db->tableExists('opd_master')) { + $builder->where('NOT EXISTS (SELECT 1 FROM opd_master o WHERE o.p_id = p.id)', null, false); + } + if ($db->tableExists('invoice_master')) { + $builder->where('NOT EXISTS (SELECT 1 FROM invoice_master i WHERE i.attach_type = 0 AND i.attach_id = p.id)', null, false); + } + + $rows = $builder->orderBy('p.id', 'DESC')->limit(2)->get()->getResultArray(); + if (count($rows) !== 1) { + return null; + } + + return $rows[0]; + } + + /** + * @param array $payload + * @return array + */ + private function pickGatewayAbhaProfile(array $payload): array + { + $candidates = [ + $payload['gateway_patient'] ?? null, + $payload['ABHAProfile'] ?? null, + $payload['accounts'][0] ?? null, + $payload['profile'] ?? null, + $payload, + ]; + + foreach ($candidates as $candidate) { + if (is_array($candidate) && $this->looksLikeGatewayAbhaProfile($candidate)) { + return $candidate; + } + } + + return []; + } + + /** + * @param array $profile + */ + private function looksLikeGatewayAbhaProfile(array $profile): bool + { + foreach (['ABHANumber', 'abha_id', 'preferredAbhaAddress', 'abha_address', 'name', 'fullName', 'full_name', 'gender', 'dob', 'date_of_birth', 'profilePhoto', 'profile_photo'] as $key) { + if (array_key_exists($key, $profile) && trim((string) $profile[$key]) !== '') { + return true; + } + } + + return false; + } + // ------------------------------------------------------------------------- // Validate ABHA number or address // POST abha/register/validate diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php index 624dc39..027e806 100644 --- a/app/Controllers/Ipd_discharge.php +++ b/app/Controllers/Ipd_discharge.php @@ -529,6 +529,7 @@ class Ipd_discharge extends BaseController // Main clinical discharge summary: use explicit DB field first (instruction_remark). // Fallback to section extraction for older cached content. $instructionRow = $ipdId > 0 ? $this->firstRowByIpd('ipd_discharge_instructions', $ipdId) : []; + $instructionMeta = $this->parseInstructionMetaPayload((string) ($instructionRow['comp_report'] ?? '')); $clinicalSummaryRaw = $this->normalizeRichText((string) ($instructionRow['comp_remark'] ?? '')); $clinicalSummary = $clinicalSummaryRaw !== '' ? '
' . $this->renderRichText($clinicalSummaryRaw) . '
' @@ -599,6 +600,28 @@ class Ipd_discharge extends BaseController $otherAdvice = $section(['Other Advice:']); $followUpInstructions = $section(['Discharge Summary:']); + // Heading-only fragments are treated as empty and should fallback to DB-backed meta values. + $otherAdviceHeadingOnly = trim(strip_tags((string) $otherAdvice)); + if (preg_match('/^Other\s+Advice\s*:?$/i', $otherAdviceHeadingOnly) === 1) { + $otherAdvice = ''; + } + + if ($otherAdvice === '') { + $otherAdviceRaw = trim((string) ($instructionMeta['other_text'] ?? '')); + if ($otherAdviceRaw === '' + && $this->tableHasColumns('ipd_discharge_drug_food_interaction', ['ipd_id']) + && $this->db->fieldExists('food_text', 'ipd_discharge_drug_food_interaction')) { + $legacyFoodRow = $this->firstRowByIpd('ipd_discharge_drug_food_interaction', $ipdId); + $otherAdviceRaw = trim((string) ($legacyFoodRow['food_text'] ?? '')); + } + + if ($otherAdviceRaw !== '') { + $otherAdvice = '
Other Advice: ' + . $this->renderStoredHtmlFragment($otherAdviceRaw) + . '
'; + } + } + // Ignore heading-only extraction (no real follow-up narrative text). $followUpHeadingOnly = trim(strip_tags((string) $followUpInstructions)); if (preg_match('/^Discharge\s+Summary\s*:?$/i', $followUpHeadingOnly) === 1) { diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index ac931da..6b48a4e 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -117,11 +117,13 @@ class Patient extends BaseController ]; $this->applyPatientAbhaFieldValues($data, $abhaId, $abhaAddress); - if ($chk_age === 'on') { - $data['age'] = $age_year; - $data['age_in_month'] = $age_month; - } else { - $data['dob'] = $this->parseDate($this->request->getPost('datepicker_dob')); + if (!$isAbhaVerifiedLocked) { + if ($chk_age === 'on') { + $data['age'] = $age_year; + $data['age_in_month'] = $age_month; + } else { + $data['dob'] = $this->parseDate($this->request->getPost('datepicker_dob')); + } } $patientModel = new PatientModel(); @@ -610,10 +612,14 @@ class Patient extends BaseController $profile_file_path = '/assets/images/no_image.svg'; $profile_picture_path = ''; + $abha_profile_photo_base64 = ''; if ($this->db->fieldExists('profile_picture', 'patient_master')) { $profile_picture_path = (string) ($data['data'][0]->profile_picture ?? ''); } + if ($this->db->fieldExists('abha_profile_photo_base64', 'patient_master')) { + $abha_profile_photo_base64 = trim((string) ($data['data'][0]->abha_profile_photo_base64 ?? '')); + } $sql = "SELECT * from blood_group order by id"; $query = $this->db->query($sql); @@ -643,6 +649,10 @@ class Patient extends BaseController } else { $profile_file_path = $profile_picture_path; } + } elseif ($abha_profile_photo_base64 !== '') { + $profile_file_path = str_starts_with($abha_profile_photo_base64, 'data:image') + ? $abha_profile_photo_base64 + : 'data:image/jpeg;base64,' . $abha_profile_photo_base64; } $data['profile_file_path'] = $profile_file_path; @@ -663,11 +673,17 @@ class Patient extends BaseController } $chk_age = $this->request->getPost('chk_age'); + $pid = (int) $this->request->getPost('p_id'); $abhaIdInput = trim((string) $this->request->getPost('input_abha_id')); $abhaAddressInput = trim((string) $this->request->getPost('input_abha_address')); [$abhaId, $abhaAddress, $abhaError] = $this->normalizeAbhaInputs($abhaIdInput, $abhaAddressInput); $age_month = (string) $this->request->getPost('input_age_month'); $age_year = (string) $this->request->getPost('input_age_year'); + $patientModel = new PatientModel(); + $existingPatient = $pid > 0 ? $patientModel->find($pid) : null; + $isAbhaVerifiedLocked = is_array($existingPatient) + && trim((string) ($existingPatient['abha_id'] ?? $existingPatient['abha_no'] ?? $existingPatient['abha'] ?? $existingPatient['abha_address'] ?? '')) !== '' + && strtoupper(trim((string) ($existingPatient['abha_verified_status'] ?? ''))) === 'VERIFIED'; $rules = [ 'input_name' => 'required|min_length[1]|max_length[30]', @@ -717,6 +733,23 @@ class Patient extends BaseController 'estimate_dob' => $estimate_dob, 'blood_group' => $this->request->getPost('input_blood_group'), ]; + + if ($isAbhaVerifiedLocked) { + $data['p_fname'] = (string) ($existingPatient['p_fname'] ?? $data['p_fname']); + $data['gender'] = (string) ($existingPatient['gender'] ?? $data['gender']); + $data['dob'] = $existingPatient['dob'] ?? ($data['dob'] ?? null); + if (array_key_exists('age', $existingPatient)) { + $data['age'] = $existingPatient['age']; + } + if (array_key_exists('age_in_month', $existingPatient)) { + $data['age_in_month'] = $existingPatient['age_in_month']; + } + if (array_key_exists('estimate_dob', $existingPatient)) { + $data['estimate_dob'] = $existingPatient['estimate_dob']; + } + $abhaId = trim((string) ($existingPatient['abha_id'] ?? $existingPatient['abha_no'] ?? $existingPatient['abha'] ?? $existingPatient['abha_address'] ?? $abhaId)); + } + $this->applyPatientAbhaFieldValues($data, $abhaId, $abhaAddress); if ($chk_age === 'on') { @@ -726,8 +759,6 @@ class Patient extends BaseController $data['dob'] = $this->parseDate($this->request->getPost('datepicker_dob')); } - $pid = (int) $this->request->getPost('p_id'); - $patientModel = new PatientModel(); $patientModel->updatePatient($data, $pid); $this->saveNamesToNameList([ (string) $this->request->getPost('input_name'), @@ -1356,14 +1387,14 @@ class Patient extends BaseController } $profile = $this->pickGatewayAbhaProfile($payload); - $abhaNumberRaw = trim((string) ($profile['ABHANumber'] ?? $payload['ABHANumber'] ?? '')); + $abhaNumberRaw = trim((string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? '')); $abhaDigits = preg_replace('/\D/', '', $abhaNumberRaw); - $abhaAddress = trim((string) ($profile['preferredAbhaAddress'] ?? $payload['abha_address'] ?? '')); - $gatewayName = trim((string) ($profile['name'] ?? $payload['name'] ?? '')); + $abhaAddress = trim((string) ($profile['preferredAbhaAddress'] ?? $profile['abha_address'] ?? $payload['abha_address'] ?? $payload['preferredAbhaAddress'] ?? '')); + $gatewayName = trim((string) ($profile['name'] ?? $profile['fullName'] ?? $profile['full_name'] ?? $payload['name'] ?? $payload['full_name'] ?? '')); $gatewayMobile = trim((string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? '')); - $gatewayDob = trim((string) ($profile['dob'] ?? $payload['dob'] ?? '')); + $gatewayDob = trim((string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? '')); $gatewayGender = trim((string) ($profile['gender'] ?? $payload['gender'] ?? '')); - $gatewayPhoto = trim((string) ($profile['profilePhoto'] ?? $payload['profilePhoto'] ?? '')); + $gatewayPhoto = trim((string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? '')); $patient = $this->findPatientForAbhaProfile($requestedPid, $abhaDigits, $gatewayMobile); $patientId = (int) ($patient['id'] ?? 0); @@ -1467,22 +1498,37 @@ class Patient extends BaseController */ private function pickGatewayAbhaProfile(array $payload): array { - if (isset($payload['gateway_patient']) && is_array($payload['gateway_patient'])) { - return $payload['gateway_patient']; - } - if (isset($payload['ABHAProfile']) && is_array($payload['ABHAProfile'])) { - return $payload['ABHAProfile']; - } - if (isset($payload['profile']) && is_array($payload['profile'])) { - return $payload['profile']; - } - if (isset($payload['accounts'][0]) && is_array($payload['accounts'][0])) { - return $payload['accounts'][0]; + $candidates = [ + $payload['gateway_patient'] ?? null, + $payload['ABHAProfile'] ?? null, + $payload['accounts'][0] ?? null, + $payload['profile'] ?? null, + $payload, + ]; + + foreach ($candidates as $candidate) { + if (is_array($candidate) && $this->looksLikeGatewayAbhaProfile($candidate)) { + return $candidate; + } } return []; } + /** + * @param array $profile + */ + private function looksLikeGatewayAbhaProfile(array $profile): bool + { + foreach (['ABHANumber', 'abha_id', 'preferredAbhaAddress', 'abha_address', 'name', 'fullName', 'full_name', 'gender', 'dob', 'date_of_birth', 'profilePhoto', 'profile_photo'] as $key) { + if (array_key_exists($key, $profile) && trim((string) $profile[$key]) !== '') { + return true; + } + } + + return false; + } + /** * @return array|null */ diff --git a/app/Controllers/Setting/AbdmGatewaySettings.php b/app/Controllers/Setting/AbdmGatewaySettings.php index ea2c030..39ac5ee 100644 --- a/app/Controllers/Setting/AbdmGatewaySettings.php +++ b/app/Controllers/Setting/AbdmGatewaySettings.php @@ -14,6 +14,9 @@ class AbdmGatewaySettings extends BaseController $token = $this->readSettingValue('EATRIA_BRIDGE_TOKEN'); $hfrId = $this->readSettingValue('ABDM_HFR_ID'); + if ($hfrId === '') { + $hfrId = $this->readSettingValue('H_HFR_ID'); + } $hmsName = $this->readSettingValue('ABDM_HMS_NAME'); $gwUrl = $this->readSettingValue('EATRIA_BRIDGE_URL') ?: 'https://abdm-bridge.e-atria.in/api'; $bridgeHospitalId = $this->readSettingValue('ABDM_BRIDGE_HOSPITAL_ID'); @@ -129,6 +132,9 @@ class AbdmGatewaySettings extends BaseController } if ($hfrId === '') { $hfrId = $this->readSettingValue('ABDM_HFR_ID'); + if ($hfrId === '') { + $hfrId = $this->readSettingValue('H_HFR_ID'); + } } $gwUrl = rtrim($gwUrl, '/'); @@ -210,7 +216,7 @@ class AbdmGatewaySettings extends BaseController ]); } - $statusUrl = $gwUrl . '/v3/gateway/status'; + $statusUrl = $gwUrl . '/v3/gateway/status' . ($hfrId !== '' ? '?hfr_id=' . urlencode($hfrId) : ''); $ch2 = curl_init(); curl_setopt_array($ch2, [ CURLOPT_URL => $statusUrl, diff --git a/app/Controllers/Setting/Template.php b/app/Controllers/Setting/Template.php index e9180fb..c031e07 100644 --- a/app/Controllers/Setting/Template.php +++ b/app/Controllers/Setting/Template.php @@ -175,7 +175,7 @@ HTML; if ($this->db->tableExists('hospital_setting')) { $hsRows = $this->db->table('hospital_setting') ->select('s_name, s_value') - ->whereIn('s_name', ['EATRIA_BRIDGE_URL', 'EATRIA_BRIDGE_TOKEN', 'ABDM_HFR_ID']) + ->whereIn('s_name', ['EATRIA_BRIDGE_URL', 'EATRIA_BRIDGE_TOKEN', 'ABDM_HFR_ID', 'H_HFR_ID']) ->get() ->getResultArray(); @@ -192,6 +192,9 @@ HTML; if ($sName === 'ABDM_HFR_ID') { $hfrId = $sValue; } + if ($sName === 'H_HFR_ID' && $hfrId === '') { + $hfrId = $sValue; + } } } diff --git a/app/Database/Migrations/2026-06-18-000609_AddAbhaProfileMetaToPatientMaster.php b/app/Database/Migrations/2026-06-18-000609_AddAbhaProfileMetaToPatientMaster.php new file mode 100644 index 0000000..ab9af9e --- /dev/null +++ b/app/Database/Migrations/2026-06-18-000609_AddAbhaProfileMetaToPatientMaster.php @@ -0,0 +1,108 @@ +tableExists('patient_master')) { + return; + } + + $fields = $this->getColumns('patient_master'); + $add = []; + + if (! in_array('abha_profile_photo_base64', $fields, true)) { + $add['abha_profile_photo_base64'] = [ + 'type' => 'LONGTEXT', + 'null' => true, + ]; + } + + if (! in_array('abha_verified_status', $fields, true)) { + $add['abha_verified_status'] = [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'null' => true, + ]; + } + + if (! in_array('abha_verification_type', $fields, true)) { + $add['abha_verification_type'] = [ + 'type' => 'VARCHAR', + 'constraint' => 30, + 'null' => true, + ]; + } + + if (! in_array('abha_kyc_verified', $fields, true)) { + $add['abha_kyc_verified'] = [ + 'type' => 'TINYINT', + 'constraint' => 1, + 'null' => true, + 'default' => 0, + ]; + } + + if (! in_array('abha_mobile_verified', $fields, true)) { + $add['abha_mobile_verified'] = [ + 'type' => 'TINYINT', + 'constraint' => 1, + 'null' => true, + 'default' => 0, + ]; + } + + if ($add !== []) { + $this->forge->addColumn('patient_master', $add); + } + } + + public function down(): void + { + if (! $this->tableExists('patient_master')) { + return; + } + + $fields = $this->getColumns('patient_master'); + foreach ([ + 'abha_profile_photo_base64', + 'abha_verified_status', + 'abha_verification_type', + 'abha_kyc_verified', + 'abha_mobile_verified', + ] as $column) { + if (in_array($column, $fields, true)) { + $this->forge->dropColumn('patient_master', $column); + } + } + } + + /** + * @return string[] + */ + private function getColumns(string $table): array + { + if (! preg_match('/^[a-zA-Z0-9_]+$/', $table)) { + return []; + } + + $result = $this->db->query('SHOW COLUMNS FROM `' . $table . '`')->getResultArray(); + + return array_map(static fn (array $row): string => (string) ($row['Field'] ?? ''), $result); + } + + private function tableExists(string $table): bool + { + if (! preg_match('/^[a-zA-Z0-9_]+$/', $table)) { + return false; + } + + $row = $this->db->query("SHOW TABLES LIKE '" . $table . "'")->getRowArray(); + + return ! empty($row); + } +} diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index e55e0ce..2912ded 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -41,7 +41,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface if ($db->tableExists('hospital_setting')) { $rows = $db->table('hospital_setting') ->select('s_name, s_value') - ->whereIn('s_name', ['EATRIA_BRIDGE_TOKEN', 'ABDM_BRIDGE_TOKEN', 'EATRIA_BRIDGE_URL', 'ABDM_HFR_ID']) + ->whereIn('s_name', ['EATRIA_BRIDGE_TOKEN', 'ABDM_BRIDGE_TOKEN', 'EATRIA_BRIDGE_URL', 'ABDM_HFR_ID', 'H_HFR_ID']) ->get() ->getResultArray(); @@ -58,8 +58,12 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface if (! empty($dbSettings['EATRIA_BRIDGE_URL'])) { $this->baseUrl = rtrim(trim($dbSettings['EATRIA_BRIDGE_URL']), '/'); } - if (! empty($dbSettings['ABDM_HFR_ID'])) { - $this->hfrId = trim($dbSettings['ABDM_HFR_ID']); + $dbHfrId = trim((string) ($dbSettings['ABDM_HFR_ID'] ?? '')); + if ($dbHfrId === '') { + $dbHfrId = trim((string) ($dbSettings['H_HFR_ID'] ?? '')); + } + if ($dbHfrId !== '') { + $this->hfrId = $dbHfrId; } } } catch (\Throwable $e) { @@ -87,6 +91,22 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface return trim($token, " \t\n\r\0\x0B\"'"); } + private function normalizeBridgeGender($value): string + { + $v = strtoupper(trim((string) $value)); + if ($v === '1' || $v === 'M' || $v === 'MALE') { + return 'M'; + } + if ($v === '2' || $v === 'F' || $v === 'FEMALE') { + return 'F'; + } + if ($v === '3' || $v === 'O' || $v === 'OTHER' || $v === 'OTHERS') { + return 'O'; + } + + return ''; + } + // ------------------------------------------------------------------------- // Internal HTTP helper // ------------------------------------------------------------------------- @@ -111,6 +131,10 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface */ private function get(string $path, array $query = []): array { + // e-Atria bridge expects hfr_id alongside Bearer auth for GET endpoints too. + if ($this->hfrId !== '' && empty($query['hfr_id'])) { + $query['hfr_id'] = $this->hfrId; + } return $this->httpCall('GET', $path, [], $query); } @@ -476,9 +500,19 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface } foreach (['abha_id', 'doctor_name', 'department', 'notes', 'gender', 'year_of_birth'] as $optional) { - if (! empty($data[$optional])) { - $body[$optional] = (string) $data[$optional]; + if (empty($data[$optional])) { + continue; } + + if ($optional === 'gender') { + $gender = $this->normalizeBridgeGender($data[$optional]); + if ($gender !== '') { + $body['gender'] = $gender; + } + continue; + } + + $body[$optional] = (string) $data[$optional]; } return $this->post('/v3/records/push', $body); @@ -582,6 +616,15 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface public function opdTokenCreate(array $payload): array { + if (array_key_exists('gender', $payload)) { + $gender = $this->normalizeBridgeGender($payload['gender']); + if ($gender !== '') { + $payload['gender'] = $gender; + } else { + unset($payload['gender']); + } + } + return $this->post('/v3/opd/token', $payload); } @@ -619,6 +662,15 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface public function hipLinkToken(array $payload): array { + if (array_key_exists('gender', $payload)) { + $gender = $this->normalizeBridgeGender($payload['gender']); + if ($gender !== '') { + $payload['gender'] = $gender; + } else { + unset($payload['gender']); + } + } + if ($this->hfrId !== '' && empty($payload['hfr_id'])) { $payload['hfr_id'] = $this->hfrId; } diff --git a/app/Views/billing/Patient_V.php b/app/Views/billing/Patient_V.php index a01b149..4f744d7 100644 --- a/app/Views/billing/Patient_V.php +++ b/app/Views/billing/Patient_V.php @@ -684,9 +684,9 @@
- + Print ABHA Card @@ -1099,7 +1099,7 @@ $('#abhareg_result_photo_ph').removeClass('d-none'); } if (patId > 0) { - $('#abhareg_result_profile_btn').attr('href', '/' + patId).removeClass('d-none'); + $('#abhareg_result_profile_btn').attr('data-url', '/' + patId).removeClass('d-none'); } if (abhaRaw.length === 14) { $('#abhareg_result_card_btn').attr('href', '' + abhaRaw).removeClass('d-none'); @@ -1281,6 +1281,14 @@ $('#abhareg_mob_otp_input').on('keypress', function(e) { if (e.which === 13) { $('#abhareg_mob_verify_btn').trigger('click'); } }); + + $('#abhareg_result_profile_btn').on('click', function() { + var url = ($(this).attr('data-url') || '').toString().trim(); + if (url === '') { + return; + } + load_form(url, 'Patient Profile'); + }); })(); /* ===== END ABHA REGISTRATION WIZARD ===== */ diff --git a/app/Views/billing/Person_Edit_V.php b/app/Views/billing/Person_Edit_V.php index 080e29f..81604f9 100644 --- a/app/Views/billing/Person_Edit_V.php +++ b/app/Views/billing/Person_Edit_V.php @@ -11,8 +11,17 @@ user(); $patientAbhaId = (string) ($data[0]->abha_id ?? $data[0]->abha_no ?? $data[0]->abha ?? $data[0]->abha_address ?? ''); + $abhaAddress = trim((string) ($data[0]->abha_address ?? '')); + $abhaVerifiedStatus = trim((string) ($data[0]->abha_verified_status ?? '')); + $abhaVerificationType = trim((string) ($data[0]->abha_verification_type ?? '')); + $abhaKycVerified = (int) ($data[0]->abha_kyc_verified ?? 0) === 1; + $abhaMobileVerified = (int) ($data[0]->abha_mobile_verified ?? 0) === 1; + $abhaLinkedAt = trim((string) ($data[0]->abdm_linked_at ?? '')); + $abhaPhotoAvailable = trim((string) ($data[0]->abha_profile_photo_base64 ?? '')) !== ''; + $abhaVerifiedLocked = $patientAbhaId !== '' && strtoupper($abhaVerifiedStatus) === 'VERIFIED'; $canEditNameAnytime = is_object($user) && method_exists($user, 'can') ? $user->can('billing.patient.edit-name-anytime') : false; $isAdmin = is_object($user) && method_exists($user, 'inGroup') ? $user->inGroup('admin') : false; + $abhaLockNotice = $abhaVerifiedLocked ? 'ABHA verified: name, gender, ABHA ID, and date of birth are locked.' : ''; // Allow edit if within 24 hours OR user has edit-name-anytime permission OR is admin if ($data[0]->p_edit == 1 || $canEditNameAnytime || $isAdmin) { @@ -60,7 +69,7 @@
> + value="p_fname ?>" type="text" autocomplete="off" >
@@ -69,14 +78,17 @@
+ + +
@@ -93,7 +105,7 @@ + autocomplete="off" maxlength="14" >
@@ -104,6 +116,26 @@ data-inputmask='"mask": "9999999999"' data-mask>
+
+
+
+
ABHA Address:
+
Status:
+
Type:
+
Linked At:
+
+
+
KYC Verified:
+
Mobile Verified:
+
ABHA Photo:
+
+ +
+
+
+ +
+
-
> @@ -140,14 +175,14 @@ Year : + autocomplete="off" style=" width:100px;" value="age?>" > Month : + value="age_in_month?>" >
@@ -157,7 +192,7 @@
+ id="datepicker_dob" type="date" value="" />
diff --git a/app/Views/billing/Person_profile_V.php b/app/Views/billing/Person_profile_V.php index aab8143..f93a7bb 100644 --- a/app/Views/billing/Person_profile_V.php +++ b/app/Views/billing/Person_profile_V.php @@ -15,6 +15,14 @@ abha_id ?? $data[0]->abha_no ?? $data[0]->abha ?? $data[0]->abha_address ?? ''); + $abhaAddress = trim((string) ($data[0]->abha_address ?? '')); + $abhaVerifiedStatus = trim((string) ($data[0]->abha_verified_status ?? '')); + $abhaVerificationType = trim((string) ($data[0]->abha_verification_type ?? '')); + $abhaKycVerified = (int) ($data[0]->abha_kyc_verified ?? 0) === 1; + $abhaMobileVerified = (int) ($data[0]->abha_mobile_verified ?? 0) === 1; + $abhaLinkedAt = trim((string) ($data[0]->abdm_linked_at ?? '')); + $abhaPhotoAvailable = trim((string) ($data[0]->abha_profile_photo_base64 ?? '')) !== ''; + $isAbhaLinkedAndVerified = $patientAbhaId !== '' && strtoupper($abhaVerifiedStatus) === 'VERIFIED'; ?> user(); @@ -77,11 +85,13 @@ + + 0) { ?>