fix(abdm): complete ABHA creation workflow

Split official and hospital ABHA cards, preserve profile/photo updates, improve existing-patient matching, require and forward communication mobile data, and show accurate Bridge OTP status and errors.
This commit is contained in:
DevSoftTech Server 2026-08-17 06:43:37 +05:30
parent 2e8304636b
commit c8f44e4493
10 changed files with 338 additions and 61 deletions

View File

@ -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']);

View File

@ -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('<h3 style="font-family:sans-serif;color:red;">Patient with ABHA number ' . esc($abhaNumber) . ' not found.</h3>');
}
$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('<h3 style="font-family:sans-serif;color:red;">Official ABHA card is not available for this patient.</h3>');
}
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('<h3 style="font-family:sans-serif;color:red;">Invalid patient.</h3>');
}
$db = \Config\Database::connect();
$patient = $db->table('patient_master')->where('id', $patientId)->get()->getRowArray();
if (! $patient) {
return $this->response->setStatusCode(404)
->setBody('<h3 style="font-family:sans-serif;color:red;">Patient not found.</h3>');
}
$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('<h3 style="font-family:sans-serif;color:red;">ABHA is not linked with this patient.</h3>');
}
$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<string, string>
*/
@ -1768,6 +1842,40 @@ class Abha extends BaseController
return '';
}
/**
* @param array<string, mixed> $profile
* @param array<string, mixed> $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<string, mixed> $profile
*/

View File

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

View File

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

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ABHA Card - <?= esc($patient['p_fname'] ?? '') ?></title>
<title>Hospital Card - <?= esc($patient['p_fname'] ?? '') ?></title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
@ -168,8 +168,6 @@
background: #fff;
}
.qr-value { margin-top: 5px; font-size: 10px; color: #3b5474; word-break: break-word; }
.official-card-wrap { margin-top: 14px; text-align: center; }
.official-card { display: block; width: 100%; max-height: 480px; object-fit: contain; border: 1px solid #d8e6fb; border-radius: 8px; background: #fff; }
.abha-number-wrap {
margin-top: 12px;
border: 1px solid #d3e3f9;
@ -239,14 +237,10 @@ $appLogo = esc(base_url('assets/img/logo.png'));
$profilePhotoUrl = (string) ($profile_photo_url ?? '/assets/images/no_image.svg');
$profilePhotoUrlEsc = esc($profilePhotoUrl);
$hmsId = esc($hms_id ?? '');
$abhaQrUrl = esc($abha_qr_url ?? '');
$patientQrImage = esc($patient_qr_image ?? '');
$barcodeImage = esc($hms_barcode_image ?? '');
$patientMobile = esc($patient_mobile ?? '');
$mobileVerified = (bool) ($mobile_verified ?? false);
$storedAbhaCard = trim((string) ($stored_abha_card ?? ''));
$storedAbhaCardSrc = $storedAbhaCard !== ''
? (str_starts_with($storedAbhaCard, 'data:') ? $storedAbhaCard : 'data:image/png;base64,' . $storedAbhaCard)
: '';
?>
<div class="abha-shell">
@ -269,13 +263,6 @@ $storedAbhaCardSrc = $storedAbhaCard !== ''
</div>
</div>
<?php if ($storedAbhaCardSrc !== ''): ?>
<div class="official-card-wrap">
<div class="meta">Stored official ABHA card</div>
<img class="official-card" src="<?= esc($storedAbhaCardSrc) ?>" alt="Stored official ABHA card">
</div>
<?php endif; ?>
<?php if ($barcodeImage !== '' && $hmsId !== ''): ?>
<div class="hospital-strip">
<div class="patient-id-strip">
@ -312,9 +299,9 @@ $storedAbhaCardSrc = $storedAbhaCard !== ''
</div>
<div class="qr-card">
<div class="qr-title">ABHA ID QR</div>
<?php if ($abhaQrUrl !== ''): ?>
<img class="qr-image" src="<?= $abhaQrUrl ?>" alt="ABHA QR Code">
<div class="qr-title">Patient Registration QR</div>
<?php if ($patientQrImage !== ''): ?>
<img class="qr-image" src="<?= $patientQrImage ?>" alt="Patient registration QR code">
<?php else: ?>
<div class="meta">QR unavailable</div>
<?php endif; ?>

View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Official ABHA Card - <?= esc($patient['p_fname'] ?? '') ?></title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
padding: 20px;
background: #eef3f9;
font-family: "Segoe UI", Arial, sans-serif;
}
.card-page {
width: 560px;
max-width: 100%;
margin: 0 auto;
}
.official-card {
display: block;
width: 100%;
height: auto;
background: #fff;
border: 1px solid #cbd8e8;
}
.actions {
margin-top: 18px;
text-align: center;
}
.btn-print {
border: 0;
border-radius: 6px;
padding: 10px 18px;
background: #104998;
color: #fff;
font-size: 14px;
font-weight: 700;
cursor: pointer;
}
@media print {
@page { size: auto; margin: 8mm; }
body { padding: 0; background: #fff; }
.card-page { width: 100%; }
.official-card { border: 0; }
.actions { display: none; }
}
</style>
</head>
<body>
<?php
$storedCard = trim((string) ($stored_abha_card ?? ''));
$storedCardSrc = str_starts_with($storedCard, 'data:')
? $storedCard
: 'data:image/png;base64,' . $storedCard;
?>
<main class="card-page">
<img class="official-card" src="<?= esc($storedCardSrc) ?>" alt="Official NHA ABHA card">
<div class="actions">
<button type="button" class="btn-print" onclick="window.print()">Print / Save PDF</button>
</div>
</main>
</body>
</html>

View File

@ -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);
});

View File

@ -14,7 +14,14 @@
<form role="form" class="form1">
<?= csrf_field() ?>
<?php
$patientAbhaId = (string) ($data[0]->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 @@
<i class="bi bi-person-check me-1"></i><?= $patientAbhaId !== '' ? 'Re-link ABHA' : 'Link ABHA via OTP' ?>
</button>
<?php endif; ?>
<?php if ($patientAbhaId !== '') :
$abhaCardNum = preg_replace('/\D/', '', $patientAbhaId);
<?php
$abhaCardNum = '';
foreach (['abha_id', 'abha_no', 'abha'] as $abhaNumberField) {
$candidateAbhaNumber = preg_replace('/\D/', '', (string) ($data[0]->{$abhaNumberField} ?? ''));
if (strlen($candidateAbhaNumber) === 14) {
$abhaCardNum = $candidateAbhaNumber;
break;
}
}
$hasLinkedAbha = strlen($abhaCardNum) === 14;
$hasOfficialAbhaCard = $hasLinkedAbha && trim((string) ($data[0]->abha_card_base64 ?? '')) !== '';
?>
<?php if ($hasOfficialAbhaCard) : ?>
<a href="<?= base_url('abha/card/' . esc($abhaCardNum, 'url')) ?>"
target="_blank"
class="btn btn-sm btn-outline-success py-0"
style="font-size:12px"
title="View &amp; Print ABHA Card">
<i class="bi bi-card-image me-1"></i>ABHA Card
title="View and print the official NHA ABHA card">
<i class="bi bi-card-image me-1"></i>Official ABHA Card
</a>
<?php endif; ?>
<?php if ($hasLinkedAbha) : ?>
<a href="<?= base_url('abha/hospital-card/' . (int) ($data[0]->id ?? 0)) ?>"
target="_blank"
class="btn btn-sm btn-outline-primary py-0"
style="font-size:12px"
title="View and print the hospital patient card">
<i class="bi bi-person-vcard me-1"></i>Hospital Card
</a>
<?php endif; ?>
</div>
@ -397,7 +423,11 @@
<?php if (!$isAbhaLinkedAndVerified) : ?>
<div class="tab-pane fade pt-3" id="profile-abha" role="tabpanel">
<?= view('partials/abha_create_panel', ['data' => $data, 'patientAbhaId' => $patientAbhaId]) ?>
<h5 class="card-title mb-1">ABHA Number Create and Verify</h5>
<p class="text-muted small mb-3">Create a new ABHA or link an existing ABHA profile to this patient.</p>
<button type="button" class="btn btn-primary" id="profile_open_abha_create_btn">
<i class="bi bi-person-plus-fill me-1"></i>Create ABHA
</button>
</div>
<?php endif; ?>
@ -409,9 +439,62 @@
</form>
</section>
<?php if (!$isAbhaLinkedAndVerified) : ?>
<?= view('partials/abha_patient_match_modal') ?>
<?= view('partials/abha_create_modal') ?>
<?php endif; ?>
<script>
$(document).ready(function() {
$(document).off('click.profileAbhaCreate', '#profile_open_abha_create_btn')
.on('click.profileAbhaCreate', '#profile_open_abha_create_btn', function() {
if (!window.AbhaCreateModal || !window.AbhaPatientMatchModal) {
alert('ABHA create workflow is unavailable. Reload the patient profile and try again.');
return;
}
window.AbhaCreateModal.open(function(profile) {
var refreshProfile = function(result) {
var patientId = Number((result && result.patient_id) || <?= (int) ($data[0]->id ?? 0) ?>);
load_form('<?= base_url('billing/patient/person_record') ?>/' + patientId + '/0');
};
if (profile && profile.need_confirmation === false && Number(profile.patient_id || 0) > 0) {
refreshProfile(profile);
return;
}
var currentPatientId = <?= (int) ($data[0]->id ?? 0) ?>;
var candidates = (profile.candidates || []).slice();
if (!candidates.some(function(candidate) { return Number(candidate.id) === currentPatientId; })) {
var existingAbhaDigits = String(<?= json_encode($patientAbhaId) ?>).replace(/\D/g, '');
var incomingAbhaDigits = String(profile.abha_number || '').replace(/\D/g, '');
candidates.unshift({
id: currentPatientId,
p_code: <?= json_encode((string) ($data[0]->p_code ?? '')) ?>,
name: <?= json_encode(trim((string) ($data[0]->p_fname ?? '') . ' ' . (string) ($data[0]->p_lname ?? ''))) ?>,
gender: <?= (int) ($data[0]->gender ?? 0) ?>,
gender_label: <?= json_encode((string) ($data[0]->xgender ?? '')) ?>,
dob: <?= json_encode((string) ($data[0]->dob ?? '')) ?>,
mobile: <?= json_encode((string) ($data[0]->mphone1 ?? '')) ?>,
address: <?= json_encode(trim(implode(', ', array_filter([
(string) ($data[0]->add1 ?? ''),
(string) ($data[0]->district ?? ''),
(string) ($data[0]->state ?? ''),
(string) ($data[0]->zip ?? ''),
])))) ?>,
abha: <?= json_encode($patientAbhaId) ?>,
abha_conflict: existingAbhaDigits.length === 14
&& incomingAbhaDigits.length === 14
&& existingAbhaDigits !== incomingAbhaDigits,
match: {}
});
}
window.AbhaPatientMatchModal.open(profile, candidates, refreshProfile, currentPatientId);
}, <?= json_encode((string) ($data[0]->mphone1 ?? '')) ?>);
});
function getPatientIdOrWarn() {
var raw = $('#p_id').val();
var patientId = parseInt(raw, 10);

View File

@ -74,12 +74,12 @@
<div class="form-text"><i class="bi bi-lock-fill me-1"></i>Aadhaar is encrypted before transmission and never stored in HMS.</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold" for="abhaCreateMobile">Mobile Number (For ABHA Communication)</label>
<label class="form-label fw-semibold" for="abhaCreateMobile">Mobile Number (For ABHA Communication) <span class="text-danger">*</span></label>
<div class="input-group input-group-lg">
<span class="input-group-text"><i class="bi bi-phone"></i></span>
<input type="text" class="form-control" id="abhaCreateMobile" maxlength="10" inputmode="numeric" autocomplete="tel" placeholder="10-digit mobile">
</div>
<div class="form-text">If this differs from the Aadhaar-linked mobile, it is verified separately.</div>
<div class="form-text">Required by ABDM enrolment. If this differs from the Aadhaar-linked mobile, it is verified separately.</div>
</div>
</div>
@ -124,6 +124,7 @@
<i class="bi bi-chat-dots-fill text-success" style="font-size:2.2rem"></i>
<h6 class="mt-2 mb-1">Enter OTP</h6>
<div class="text-muted small" id="abhaCreateOtpHint">OTP sent to the Aadhaar registered mobile number.</div>
<div class="text-muted small mt-1 d-none" id="abhaCreateOtpRequestId"></div>
</div>
<label class="form-label fw-semibold" for="abhaCreateOtp">6-digit OTP</label>
<div class="input-group input-group-lg">
@ -317,12 +318,21 @@ window.AbhaCreateModal = (function () {
if (!value) return '';
return String(value).indexOf('data:') === 0 ? value : 'data:image/jpeg;base64,' + value;
}
function mergeProfile(baseProfile, response) {
var merged = $.extend({}, baseProfile || {});
$.each(response || {}, function(key, value) {
if (value !== null && value !== undefined && value !== '') {
merged[key] = value;
}
});
return merged;
}
function sendAadhaarOtp() {
var aadhaar = digits($('#abhaCreateAadhaar').val());
var mobile = digits($('#abhaCreateMobile').val());
if (aadhaar.length !== 12) { alertBox('warning', 'Enter a valid 12-digit Aadhaar number.'); return; }
if (mobile && mobile.length !== 10) { alertBox('warning', 'Enter a valid 10-digit mobile number.'); return; }
if (mobile.length !== 10) { alertBox('warning', 'Enter a valid 10-digit mobile number for ABHA communication.'); return; }
if (!$('#abhaCreateConsent1').is(':checked')) { alertBox('warning', 'Aadhaar authentication consent is mandatory before sending the OTP.'); return; }
if (!$('#abhaCreateConsent4').is(':checked')) { alertBox('warning', 'Confirm that the consent was explained to the patient.'); return; }
@ -333,7 +343,15 @@ window.AbhaCreateModal = (function () {
if (!response || response.ok != 1) { alertBox('danger', apiMessage(response, 'Unable to send Aadhaar OTP.')); return; }
createTxnId = response.txn_id || '';
$('#abhaCreateOtp').val('');
$('#abhaCreateOtpHint').text('OTP sent to the mobile number linked with this Aadhaar.');
var destination = response.masked_mobile || ((response.message || '').match(/\*{2,}\d{4}/) || [''])[0];
var bridgeMessage = String(response.message || '').trim();
if (!bridgeMessage) {
bridgeMessage = 'OTP sent to the Aadhaar registered mobile number' + (destination ? ' ending with ' + destination : '') + '.';
}
$('#abhaCreateOtpHint').text(bridgeMessage);
$('#abhaCreateOtpRequestId')
.toggleClass('d-none', !response.request_id)
.text(response.request_id ? 'Bridge Request ID: ' + response.request_id : '');
showStep(2);
startResendTimer($('#abhaCreateResendBtn'), 60);
$('#abhaCreateOtp').trigger('focus');
@ -356,8 +374,8 @@ window.AbhaCreateModal = (function () {
stopTimers();
createdProfile = response;
createTxnId = response.txn_id || createTxnId;
var abhaMobile = digits(response.mobile);
if (communicationMobile && abhaMobile && communicationMobile !== abhaMobile) {
var abhaMobile = digits(response.mobile).slice(-10);
if (communicationMobile && communicationMobile !== abhaMobile) {
requestMobileOtp();
return;
}
@ -369,19 +387,22 @@ window.AbhaCreateModal = (function () {
}
function requestMobileOtp() {
$('#abhaCreateMobileHint').html('Enter the OTP sent to <strong>' + escapeHtml(maskMobile(communicationMobile)) + '</strong> to update the mobile registered on this ABHA account.');
$('#abhaCreateMobileOtp').val('');
showStep(3);
alertBox('info', 'ABHA verified. Requesting an OTP for the alternate communication mobile...');
$.post('<?= base_url('abha/create/communication') ?>', { mobile: communicationMobile, txn_id: createTxnId, '<?= csrf_token() ?>': csrf() }, function (response) {
if (!response || response.ok != 1) {
alertBox('warning', apiMessage(response, 'Unable to send the mobile OTP. You can skip this step.'));
renderProfile(createdProfile || {});
alertBox('warning', 'ABHA verification succeeded, but the communication mobile was not updated: ' + apiMessage(response, 'Unable to send the mobile OTP.'));
return;
}
mobileTxnId = response.txn_id || createTxnId;
$('#abhaCreateMobileHint').html('Enter the OTP sent to <strong>' + escapeHtml(maskMobile(communicationMobile)) + '</strong> to update the mobile registered on this ABHA account.');
$('#abhaCreateMobileOtp').val('');
showStep(3);
startMobileResendTimer($('#abhaCreateMobileResendBtn'));
$('#abhaCreateMobileOtp').trigger('focus');
}, 'json').fail(function (xhr) {
alertBox('warning', apiMessage(xhr.responseJSON, 'Unable to send the mobile OTP. You can skip this step.'));
renderProfile(createdProfile || {});
alertBox('warning', 'ABHA verification succeeded, but the communication mobile was not updated: ' + apiMessage(xhr.responseJSON, 'Unable to send the mobile OTP.'));
});
}
@ -393,7 +414,7 @@ window.AbhaCreateModal = (function () {
button.prop('disabled', false).html('<i class="bi bi-check2-circle me-1"></i>Verify &amp; Update Mobile');
if (!response || response.ok != 1) { alertBox('danger', apiMessage(response, 'Mobile OTP verification failed.')); return; }
stopTimers();
createdProfile = $.extend({}, createdProfile, response);
createdProfile = mergeProfile(createdProfile, response);
renderProfile(createdProfile);
}, 'json').fail(function (xhr) {
button.prop('disabled', false).html('<i class="bi bi-check2-circle me-1"></i>Verify &amp; Update Mobile');
@ -516,10 +537,11 @@ window.AbhaCreateModal = (function () {
$('#abhaCreateRegisterBtn').on('click', function () {
if (!createdProfile) return;
var profile = createdProfile;
// Already linked: nothing else opens, so keep this window up for the operator to close.
if (profile.need_confirmation === false && Number(profile.patient_id || 0) > 0) {
alertBox('success', 'This ABHA is already linked to HMS patient <strong>' + escapeHtml(profile.p_code || '') + '</strong>. The registration page behind has been updated. Close this window when you are done.');
if (typeof onCompleted === 'function') onCompleted(profile);
$('#abhaCreateModal').one('hidden.bs.modal', function () {
if (typeof onCompleted === 'function') onCompleted(profile);
});
modal.hide();
return;
}
$('#abhaCreateModal').one('hidden.bs.modal', function () {
@ -532,7 +554,7 @@ window.AbhaCreateModal = (function () {
});
return {
open: function (callback) {
open: function (callback, prefillMobile) {
onCompleted = callback;
createTxnId = '';
mobileTxnId = '';
@ -540,6 +562,8 @@ window.AbhaCreateModal = (function () {
createdProfile = null;
stopTimers();
$('#abhaCreateAadhaar,#abhaCreateMobile,#abhaCreateOtp,#abhaCreateMobileOtp,#abhaCreateCustomAddress').val('');
$('#abhaCreateMobile').val(digits(prefillMobile));
$('#abhaCreateOtpRequestId').addClass('d-none').text('');
$('#abhaCreateConsent1,#abhaCreateConsent2,#abhaCreateConsent3,#abhaCreateConsent4').prop('checked', false);
$('#abhaCreatePhoto,#abhaCreateDownloadCard').addClass('d-none');
showStep(1);

View File

@ -16,7 +16,7 @@
* this modal is shown.
*
* Include once per page: <?= view('partials/abha_patient_match_modal') ?>
* Then call: window.AbhaPatientMatchModal.open(profileResp, candidates, function(confirmResp) { ... });
* Then call: window.AbhaPatientMatchModal.open(profileResp, candidates, function(confirmResp) { ... }, preferredPatientId);
*/
?>
<div class="modal fade" id="abhaMatchModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
@ -269,7 +269,8 @@ window.AbhaPatientMatchModal = (function () {
$modalEl = $('#abhaMatchModal');
_bsModal = window.bootstrap ? new bootstrap.Modal($modalEl[0]) : null;
$(document).on('click', '.abhaMatch-candidate', function () {
$(document).off('click.abhaPatientMatch', '.abhaMatch-candidate')
.on('click.abhaPatientMatch', '.abhaMatch-candidate', function () {
if ($(this).find('.abhaMatch-radio').is(':disabled')) { return; }
selectCandidate(parseInt($(this).data('id'), 10));
});
@ -288,7 +289,7 @@ window.AbhaPatientMatchModal = (function () {
});
return {
open: function (profile, candidates, onResolved) {
open: function (profile, candidates, onResolved, preferredPatientId) {
_profile = profile || {};
window._abhaMatchCandidates = candidates || [];
_selectedId = 0;
@ -298,6 +299,10 @@ window.AbhaPatientMatchModal = (function () {
renderProfile(_profile);
renderCandidates(candidates || []);
$('#abhaMatch_update_preview').addClass('d-none').empty();
if (Number(preferredPatientId || 0) > 0
&& (candidates || []).some(function(candidate) { return Number(candidate.id) === Number(preferredPatientId); })) {
selectCandidate(Number(preferredPatientId));
}
if (_bsModal) { _bsModal.show(); } else { $modalEl.modal('show'); }
}
};