diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 2f381f9..ef432b4 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -777,6 +777,7 @@ $routes->post('abha/create/confirm_patient', 'Abha::confirmPatient', ['filter' = $routes->post('abha/create/address', 'Abha::address', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/create/address_suggestions', 'Abha::addressSuggestions', ['filter' => 'permission:abdm.abha.create']); $routes->get('abha/card/(:segment)', 'Abha::card/$1', ['filter' => 'permission:abdm.abha.create']); +$routes->get('abha/hospital-card/(:num)', 'Abha::hospitalCard/$1', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/register/card', 'Abha::fetchOfficialCard', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/register/validate', 'Abha::validateAbha', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/register/qr_scan', 'Abha::qrScan', ['filter' => 'permission:abdm.abha.create']); diff --git a/app/Controllers/Abha.php b/app/Controllers/Abha.php index 94a503b..dda2439 100644 --- a/app/Controllers/Abha.php +++ b/app/Controllers/Abha.php @@ -43,7 +43,13 @@ class Abha extends BaseController if ($txnId) { session()->set('abha_aadhaar_txn_' . $txnId, $aadhaar); } - return $this->response->setJSON(['ok' => 1, 'txn_id' => $txnId]); + return $this->response->setJSON([ + 'ok' => 1, + 'txn_id' => $txnId, + 'message' => (string) ($result['message'] ?? $result['data']['message'] ?? 'OTP sent to Aadhaar-linked mobile.'), + 'masked_mobile' => (string) ($result['masked_mobile'] ?? $result['data']['masked_mobile'] ?? $result['data']['maskedMobile'] ?? ''), + 'request_id' => (string) ($result['request_id'] ?? ''), + ]); } return $this->response->setJSON([ @@ -79,10 +85,15 @@ class Abha extends BaseController } if (empty($result['ok']) || $result['ok'] != 1) { + $requestId = trim((string) ($result['request_id'] ?? '')); + $errorText = $this->extractBridgeErrorText($result, 'OTP verification failed'); + if ($requestId !== '') { + $errorText .= ' (Bridge Request ID: ' . $requestId . ')'; + } return $this->response->setJSON([ 'ok' => 0, - 'error_text' => $this->extractBridgeErrorText($result, 'OTP verification failed'), - 'request_id' => (string) ($result['request_id'] ?? ''), + 'error_text' => $errorText, + 'request_id' => $requestId, ]); } // Keep top-level keys (gateway_patient, official_card) that also appear under data. @@ -109,7 +120,7 @@ class Abha extends BaseController ?: '' ); $name = $this->extractAbhaProfileName($profile, $payload); - $photo = (string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? ''); + $photo = $this->extractAbhaProfilePhoto($profile, $payload); // A different communication number is not verified until Step 3 succeeds. // Never persist the operator-entered number merely because Aadhaar OTP passed. $mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? ''); @@ -289,7 +300,7 @@ class Abha extends BaseController ?: '' ); $name = $this->extractAbhaProfileName($profile, $payload); - $photo = (string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? ''); + $photo = $this->extractAbhaProfilePhoto($profile, $payload); $gender = (string) ($profile['gender'] ?? $payload['gender'] ?? ''); $dob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? ''); // Bridge/gateway verify-otp responses don't always echo the mobile back; @@ -547,8 +558,8 @@ class Abha extends BaseController } // ------------------------------------------------------------------------- - // ABHA Card view — GET abha/card/{abha_number} - // Renders a printable card page for a patient whose ABHA is stored in HMS. + // Official ABHA Card — GET abha/card/{abha_number} + // Renders only the official card image returned by ABDM/NHA. // ------------------------------------------------------------------------- public function card(string $abhaNumber = '') { @@ -585,7 +596,47 @@ class Abha extends BaseController ->setBody('

Patient with ABHA number ' . esc($abhaNumber) . ' not found.

'); } - $abhaDisp = preg_replace('/^(\d{2})(\d{4})(\d{4})(\d{4})$/', '$1-$2-$3-$4', $abhaNumClean) ?: $abhaNumber; + $storedCard = trim((string) ($patient['abha_card_base64'] ?? '')); + if ($storedCard === '') { + return $this->response->setStatusCode(404) + ->setBody('

Official ABHA card is not available for this patient.

'); + } + + return view('abha/official_card', [ + 'patient' => $patient, + 'stored_abha_card' => $storedCard, + ]); + } + + // Hospital Card — GET abha/hospital-card/{patient_id} + public function hospitalCard(int $patientId = 0) + { + if ($patientId <= 0) { + return $this->response->setStatusCode(400) + ->setBody('

Invalid patient.

'); + } + + $db = \Config\Database::connect(); + $patient = $db->table('patient_master')->where('id', $patientId)->get()->getRowArray(); + if (! $patient) { + return $this->response->setStatusCode(404) + ->setBody('

Patient not found.

'); + } + + $abhaNumClean = ''; + foreach (['abha_id', 'abha_no', 'abha'] as $field) { + $candidate = preg_replace('/\D/', '', (string) ($patient[$field] ?? '')); + if (strlen($candidate) === 14) { + $abhaNumClean = $candidate; + break; + } + } + if ($abhaNumClean === '') { + return $this->response->setStatusCode(404) + ->setBody('

ABHA is not linked with this patient.

'); + } + + $abhaDisp = preg_replace('/^(\d{2})(\d{4})(\d{4})(\d{4})$/', '$1-$2-$3-$4', $abhaNumClean) ?: $abhaNumClean; $genderRaw = (string) ($patient['gender'] ?? ''); $genderLabel = $genderRaw === '1' ? 'Male' : ($genderRaw === '2' ? 'Female' : $genderRaw); $dobRaw = (string) ($patient['dob'] ?? ''); @@ -602,7 +653,19 @@ class Abha extends BaseController $profilePhotoUrl = $this->resolvePatientPhotoUrl($patient); $mobileNo = trim((string) ($patient['mphone1'] ?? '')); $mobileVerified = (int) ($patient['abha_mobile_verified'] ?? 0) === 1; - $abhaQrUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' . urlencode($abhaNumClean); + $patientQrPayload = json_encode([ + 'hidn' => $abhaNumClean, + 'hid' => trim((string) ($patient['abha_address'] ?? '')), + 'name' => trim((string) ($patient['p_fname'] ?? '')), + 'gender' => $genderLabel, + 'dob' => $dobRaw !== '0000-00-00' ? $dobRaw : '', + 'mobile' => $mobileNo, + 'address' => trim((string) ($patient['add1'] ?? '')), + 'districtName' => trim((string) ($patient['district'] ?? '')), + 'stateName' => trim((string) ($patient['state'] ?? '')), + 'pincode' => trim((string) ($patient['zip'] ?? '')), + ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $patientQrImage = $this->buildQrCodeDataUri(is_string($patientQrPayload) ? $patientQrPayload : $abhaNumClean); $barcodeSvg = $this->buildCode39Svg($hmsId); $barcodeImage = $this->buildCode39ImageDataUri($hmsId, $barcodeSvg); @@ -621,13 +684,24 @@ class Abha extends BaseController 'hms_barcode_svg' => $barcodeSvg, 'hms_barcode_image' => $barcodeImage, 'profile_photo_url' => $profilePhotoUrl, - 'abha_qr_url' => $abhaQrUrl, + 'patient_qr_image' => $patientQrImage, 'patient_mobile' => $mobileNo, 'mobile_verified' => $mobileVerified, - 'stored_abha_card' => trim((string) ($patient['abha_card_base64'] ?? '')), ]); } + private function buildQrCodeDataUri(string $payload): string + { + try { + $qrCode = new \Mpdf\QrCode\QrCode($payload, 'M'); + $svg = (new \Mpdf\QrCode\Output\Svg())->output($qrCode, 220); + return 'data:image/svg+xml;base64,' . base64_encode($svg); + } catch (\Throwable $e) { + log_message('error', '[ABHA] Hospital card QR generation failed: ' . $e->getMessage()); + return ''; + } + } + /** * @return array */ @@ -1768,6 +1842,40 @@ class Abha extends BaseController return ''; } + /** + * @param array $profile + * @param array $payload + */ + private function extractAbhaProfilePhoto(array $profile, array $payload): string + { + $sources = [ + $profile, + $payload, + $payload['ABHAProfile'] ?? [], + $payload['data']['ABHAProfile'] ?? [], + $payload['gateway_abha_profile'] ?? [], + $payload['data']['gateway_abha_profile'] ?? [], + $payload['gateway_patient'] ?? [], + $payload['data']['gateway_patient'] ?? [], + $payload['account'] ?? [], + $payload['data']['account'] ?? [], + ]; + + foreach ($sources as $source) { + if (! is_array($source)) { + continue; + } + foreach (['profilePhoto', 'profile_photo', 'patient_photo', 'photo'] as $key) { + $photo = trim((string) ($source[$key] ?? '')); + if ($photo !== '') { + return $photo; + } + } + } + + return ''; + } + /** * @param array $profile */ diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index 4e51d4c..84d88ac 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -897,7 +897,11 @@ class Patient extends BaseController $query = $this->db->query($sql); $data['patient_tag_list'] = $query->getResult(); - if (count($file_data) > 0) { + if ($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; + } elseif (count($file_data) > 0) { $pos = strpos($file_data[0]->full_path, '/uploads/', 1); if ($pos !== false) { $profile_file_path = substr($file_data[0]->full_path, $pos); @@ -911,10 +915,6 @@ 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; diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index 404dee2..6b11309 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -933,11 +933,16 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface public function abhaMobileGenerateOtp(array $payload): array { - // Gateway API format: { "mobile": "<10-digit>" } + // txnId is optional for standalone mobile lookup, but required when + // updating the communication mobile during Aadhaar enrolment. // Gateway handles RSA encryption and M3 format conversion internally. $body = [ 'mobile' => (string) ($payload['mobile'] ?? $payload['loginId'] ?? ''), ]; + $txnId = trim((string) ($payload['txnId'] ?? $payload['txn_id'] ?? '')); + if ($txnId !== '') { + $body['txnId'] = $txnId; + } if ($this->hfrId !== '' && empty($body['hfr_id'])) { $body['hfr_id'] = $this->hfrId; } diff --git a/app/Views/abha/card.php b/app/Views/abha/card.php index 6e60560..66343d4 100644 --- a/app/Views/abha/card.php +++ b/app/Views/abha/card.php @@ -3,7 +3,7 @@ -ABHA Card - <?= esc($patient['p_fname'] ?? '') ?> +Hospital Card - <?= esc($patient['p_fname'] ?? '') ?> + + + +
+ Official NHA ABHA card +
+ +
+
+ + diff --git a/app/Views/billing/Patient_V.php b/app/Views/billing/Patient_V.php index 4d0fa8e..94f6218 100644 --- a/app/Views/billing/Patient_V.php +++ b/app/Views/billing/Patient_V.php @@ -1426,7 +1426,7 @@ window.AbhaPatientMatchModal.open(profile, profile.candidates || [], function(finalResponse) { if (typeof showRegResult === 'function') showRegResult(finalResponse); }); - }); + }, $('#input_mphone1').val()); }).on('click.abhaLauncher', '#abha_open_verify_modal_btn', function() { openAbhaAccountVerification('', null); }); diff --git a/app/Views/billing/Person_profile_V.php b/app/Views/billing/Person_profile_V.php index 9b6f97e..20f06b0 100644 --- a/app/Views/billing/Person_profile_V.php +++ b/app/Views/billing/Person_profile_V.php @@ -14,7 +14,14 @@
abha_id ?? $data[0]->abha_no ?? $data[0]->abha ?? $data[0]->abha_address ?? ''); + $patientAbhaId = ''; + foreach (['abha_id', 'abha_no', 'abha', 'abha_address'] as $abhaField) { + $candidateAbha = trim((string) ($data[0]->{$abhaField} ?? '')); + if ($candidateAbha !== '') { + $patientAbhaId = $candidateAbha; + break; + } + } $abhaAddress = trim((string) ($data[0]->abha_address ?? '')); if ($abhaAddress === '' && preg_match('/abha_address\s*:\s*([A-Za-z0-9._-]+@[A-Za-z0-9.-]+)/i', (string) ($data[0]->log ?? ''), $abhaLogMatch) === 1) { $abhaAddress = trim((string) ($abhaLogMatch[1] ?? '')); @@ -165,15 +172,34 @@ - {$abhaNumberField} ?? '')); + if (strlen($candidateAbhaNumber) === 14) { + $abhaCardNum = $candidateAbhaNumber; + break; + } + } + $hasLinkedAbha = strlen($abhaCardNum) === 14; + $hasOfficialAbhaCard = $hasLinkedAbha && trim((string) ($data[0]->abha_card_base64 ?? '')) !== ''; ?> + - ABHA Card + title="View and print the official NHA ABHA card"> + Official ABHA Card + + + + + Hospital Card @@ -397,7 +423,11 @@
- $data, 'patientAbhaId' => $patientAbhaId]) ?> +
ABHA Number Create and Verify
+

Create a new ABHA or link an existing ABHA profile to this patient.

+
@@ -409,9 +439,62 @@
+ + + + +