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:
parent
2e8304636b
commit
c8f44e4493
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; ?>
|
||||
|
|
|
|||
64
app/Views/abha/official_card.php
Normal file
64
app/Views/abha/official_card.php
Normal 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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 & 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);
|
||||
|
|
|
|||
|
|
@ -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 & 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 & 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);
|
||||
|
|
|
|||
|
|
@ -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'); }
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user