fix(abha): make verification match modal aware of unique ABHA ID and dynamic titles

This commit is contained in:
DevSoftTech Server 2026-09-12 05:23:33 +05:30
parent 7e3c57f48f
commit f2e27828bb
3 changed files with 256 additions and 33 deletions

View File

@ -2032,14 +2032,17 @@ class Abha extends BaseController
return [];
}
$candidateAbhaCols = array_values(array_intersect(['abha_id', 'abha_no', 'abha', 'abha_address'], $fields));
$selectFields = array_values(array_intersect(
['id', 'p_code', 'p_fname', 'p_lname', 'gender', 'dob', 'age', 'mphone1', 'udai', 'add1', 'district', 'state', 'zip', 'profile_file_id', 'profile_picture', 'abha_profile_photo_base64'],
$fields
));
$select = implode(',', $selectFields);
if ($abhaField !== null && ! in_array($abhaField, ['id', 'p_code', 'p_fname', 'p_lname', 'gender', 'dob', 'age', 'mphone1', 'udai'], true)) {
$select .= ',' . $abhaField;
foreach ($candidateAbhaCols as $cCol) {
if (! in_array($cCol, $selectFields, true)) {
$selectFields[] = $cCol;
}
}
$select = implode(',', $selectFields);
// Two targeted passes: exact identifiers can never be crowded out by a
// common name, and gender alone is a filter (see scoring) not a selector.
@ -2054,24 +2057,53 @@ class Abha extends BaseController
}
};
$exactMatches = [];
if ($abhaField !== null && $abhaNumClean !== '') {
$exactMatches[$abhaField] = $abhaNumClean;
}
if ($aadhaar !== '') {
$exactMatches['udai'] = $aadhaar;
}
if ($mobile !== '') {
$exactMatches['mphone1'] = $mobile;
}
$formattedAbha = (strlen($abhaNumClean) === 14)
? substr($abhaNumClean, 0, 2) . '-' . substr($abhaNumClean, 2, 4) . '-' . substr($abhaNumClean, 6, 4) . '-' . substr($abhaNumClean, 10, 4)
: '';
$abhaVals = array_values(array_filter([$abhaNumClean, $formattedAbha]));
if ($exactMatches !== []) {
$collect(static function ($builder) use ($exactMatches): void {
foreach ($exactMatches as $column => $value) {
$builder->orWhere($column, $value);
$collect(static function ($builder) use ($candidateAbhaCols, $abhaVals, $aadhaar, $mobile, $fields): void {
$hasAny = false;
if (! empty($abhaVals) && ! empty($candidateAbhaCols)) {
foreach ($candidateAbhaCols as $cCol) {
if (! $hasAny) {
$builder->whereIn($cCol, $abhaVals);
$hasAny = true;
} else {
$builder->orWhereIn($cCol, $abhaVals);
}
}
});
}
}
if ($aadhaar !== '' && in_array('udai', $fields, true)) {
$formattedAadhaar = substr($aadhaar, 0, 4) . ' ' . substr($aadhaar, 4, 4) . ' ' . substr($aadhaar, 8, 4);
if (! $hasAny) {
$builder->whereIn('udai', [$aadhaar, $formattedAadhaar]);
$hasAny = true;
} else {
$builder->orWhereIn('udai', [$aadhaar, $formattedAadhaar]);
}
}
if ($aadhaar !== '' && in_array('udai_hash', $fields, true)) {
$vault = new \App\Libraries\AadhaarVaultService();
$hash = $vault->hash($aadhaar);
if ($hash !== '') {
if (! $hasAny) {
$builder->where('udai_hash', $hash);
$hasAny = true;
} else {
$builder->orWhere('udai_hash', $hash);
}
}
}
if ($mobile !== '' && in_array('mphone1', $fields, true)) {
if (! $hasAny) {
$builder->where('mphone1', $mobile);
$hasAny = true;
} else {
$builder->orWhere('mphone1', $mobile);
}
}
});
if ($nameTokens !== []) {
$hasLastName = in_array('p_lname', $fields, true);
@ -2115,17 +2147,32 @@ class Abha extends BaseController
$mobileMatch = $mobile !== '' && preg_replace('/\D/', '', (string) ($row['mphone1'] ?? '')) === $mobile;
$aadhaarMatch = $aadhaar !== '' && preg_replace('/\D/', '', (string) ($row['udai'] ?? '')) === $aadhaar;
$rowAbha = $abhaField !== null ? trim((string) ($row[$abhaField] ?? '')) : '';
$rowAbha = '';
$abhaMatch = false;
foreach ($candidateAbhaCols as $cCol) {
$val = trim((string) ($row[$cCol] ?? ''));
if ($val !== '') {
if ($rowAbha === '') {
$rowAbha = $val;
}
$digitsVal = preg_replace('/\D/', '', $val);
if ($abhaNumClean !== '' && $digitsVal === $abhaNumClean) {
$abhaMatch = true;
$rowAbha = $val;
break;
}
}
}
$abhaConflict = $rowAbha !== '' && preg_replace('/\D/', '', $rowAbha) !== $abhaNumClean;
$abhaMatch = $abhaNumClean !== '' && $rowAbha !== ''
&& preg_replace('/\D/', '', $rowAbha) === $abhaNumClean;
$minimumDemographicMatch = $nameOverlap && $birthYearMatch && $genderMatch;
if (! $abhaMatch && ! $aadhaarMatch && ! $minimumDemographicMatch) {
continue;
}
$score = ($abhaMatch ? 12 : 0) + ($aadhaarMatch ? 10 : 0)
// Top score to exact ABHA (1000) and Aadhaar (500) matches
$score = ($abhaMatch ? 1000 : 0) + ($aadhaarMatch ? 500 : 0)
+ ($minimumDemographicMatch ? 5 : 0) + ($nameOverlap ? 2 : 0)
+ ($birthYearMatch ? 2 : 0) + ($genderMatch ? 1 : 0) + ($mobileMatch ? 4 : 0);
@ -3001,9 +3048,65 @@ class Abha extends BaseController
$abhaField = $this->resolveAbhaFieldName($fields);
$candidates = $this->findMatchingCandidates($db, $fields, $name, $mobile, $gender, $dob, '', $abhaField, $abhaNumber);
$hasExactAbhaMatch = false;
$conflictPatient = null;
foreach ($candidates as $c) {
if (! empty($c['match']['abha'])) {
$hasExactAbhaMatch = true;
$conflictPatient = [
'id' => (int) $c['id'],
'p_code' => (string) ($c['p_code'] ?? ''),
'p_fname' => (string) ($c['name'] ?? ''),
'name' => (string) ($c['name'] ?? ''),
];
break;
}
}
// Direct fallback safety check via findPatientByAbha
if (! $hasExactAbhaMatch && ($abhaNumber !== '' || $abhaAddress !== '')) {
$patientController = new Patient();
$direct = $patientController->findPatientByAbha($abhaNumber !== '' ? $abhaNumber : $abhaAddress);
if ($direct !== null) {
$hasExactAbhaMatch = true;
$conflictPatient = [
'id' => (int) $direct['id'],
'p_code' => (string) ($direct['p_code'] ?? ''),
'p_fname' => (string) ($direct['p_fname'] ?? ''),
'name' => (string) ($direct['p_fname'] ?? ''),
];
array_unshift($candidates, [
'id' => (int) $direct['id'],
'p_code' => (string) ($direct['p_code'] ?? ''),
'name' => (string) ($direct['p_fname'] ?? ''),
'gender' => 0,
'gender_label' => '',
'dob' => '',
'age' => null,
'mobile' => (string) ($direct['mphone1'] ?? ''),
'photo_url' => '',
'address' => '',
'aadhaar' => (string) ($direct['udai'] ?? ''),
'abha' => (string) ($direct['abha_id'] ?? ''),
'abha_conflict' => false,
'match' => [
'name' => false,
'age' => false,
'gender' => false,
'mobile' => false,
'aadhaar' => false,
'abha' => true,
],
'score' => 1000,
]);
}
}
return $this->response->setJSON([
'ok' => 1,
'need_confirmation' => true,
'already_registered' => $hasExactAbhaMatch,
'conflict_patient' => $conflictPatient,
'abha_number' => $abhaNumber,
'abha_address' => $abhaAddress,
'name' => $name,

View File

@ -23,7 +23,11 @@
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-people-fill me-2"></i>Patient Already Exists</h5>
<h5 class="modal-title d-flex align-items-center" id="abhaMatch_modal_title">
<i class="bi bi-people-fill me-2" id="abhaMatch_modal_icon"></i>
<span id="abhaMatch_modal_title_text">Matching Patients in HMS</span>
<span id="abhaMatch_modal_title_badge" class="badge bg-danger ms-2 d-none">Already Exists</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
@ -69,10 +73,18 @@
</div>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-outline-secondary" id="abhaMatch_create_new_btn">
<i class="bi bi-person-plus me-1"></i>Create New Patient
</button>
<div class="d-flex gap-2">
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-secondary" id="abhaMatch_create_new_btn">
<i class="bi bi-person-plus me-1"></i>Create New Patient
</button>
<span id="abhaMatch_duplicate_warning" class="small text-danger d-none">
<i class="bi bi-shield-lock-fill me-1"></i>ABHA already registered. Cannot create duplicate.
</span>
</div>
<div class="d-flex gap-2 align-items-center">
<a href="#" target="_blank" class="btn btn-outline-danger d-none" id="abhaMatch_view_profile_btn">
<i class="bi bi-box-arrow-up-right me-1"></i>Open Patient Profile
</a>
<button type="button" class="btn btn-outline-primary" id="abhaMatch_keep_existing_btn" disabled><i class="bi bi-person-check me-1"></i>Keep Existing</button>
<button type="button" class="btn btn-primary" id="abhaMatch_update_existing_btn" disabled><i class="bi bi-arrow-repeat me-1"></i>Update Details</button>
</div>
@ -174,23 +186,30 @@ window.AbhaPatientMatchModal = (function () {
var conflictNote = c.abha_conflict
? '<div class="small text-danger mt-1"><i class="bi bi-exclamation-triangle-fill me-1"></i>Already has a different ABHA linked (' + esc(c.abha) + ')</div>'
: '';
var exactAbhaNote = m.abha
? '<div class="small text-danger fw-semibold mt-1"><i class="bi bi-shield-lock-fill me-1"></i>Already registered with this exact ABHA ID in HMS (' + esc(c.p_code || '') + ')</div>'
: '';
var cardCls = m.abha ? 'border-danger bg-danger-subtle' : (m.aadhaar ? 'border-warning bg-warning-subtle' : '');
var html = ''
+ '<div class="card mb-2 abhaMatch-candidate" data-id="' + c.id + '" style="cursor:pointer">'
+ '<div class="card mb-2 abhaMatch-candidate ' + cardCls + '" data-id="' + c.id + '" style="cursor:pointer">'
+ ' <div class="card-body py-2">'
+ ' <div class="d-flex justify-content-between align-items-start">'
+ ' <div>'
+ (c.photo_url ? ' <img src="' + esc(c.photo_url) + '" alt="Patient photo" class="rounded-circle me-2" style="width:38px;height:38px;object-fit:cover;vertical-align:middle">' : '')
+ ' <div class="fw-semibold d-inline-block">' + esc(c.name || '—') + ' <small class="text-muted">(' + esc(c.p_code || '') + ')</small></div>'
+ ' <div class="mt-1">'
+ (m.abha ? '<span class="badge bg-danger me-1 mb-1"><i class="bi bi-shield-lock-fill me-1"></i>Exact ABHA Match</span>' : '')
+ (m.aadhaar ? '<span class="badge bg-danger me-1 mb-1"><i class="bi bi-fingerprint me-1"></i>Exact Aadhaar Match</span>' : '')
+ badge(m.name, 'Name')
+ badge(m.age, 'Age ' + (c.age != null ? c.age : '?'))
+ badge(m.gender, genderText(c.gender))
+ badge(m.mobile, 'Mobile ' + maskMobile(c.mobile))
+ badge(m.aadhaar, 'Aadhaar')
+ (!m.aadhaar ? badge(m.aadhaar, 'Aadhaar') : '')
+ ' </div>'
+ ' <div class="small text-muted mt-1">DOB: ' + esc(c.dob || '—') + ' | Mobile: ' + esc(maskMobile(c.mobile) || '—') + '</div>'
+ ' <div class="small text-muted mt-1">Address: ' + esc(c.address || '—') + '</div>'
+ conflictNote
+ exactAbhaNote
+ ' </div>'
+ ' <div class="form-check">'
+ ' <input class="form-check-input abhaMatch-radio" type="radio" name="abhaMatchPick" value="' + c.id + '"' + (c.abha_conflict ? ' disabled' : '') + '>'
@ -215,6 +234,17 @@ window.AbhaPatientMatchModal = (function () {
function submitConfirm(action, patientId, updateMode, $btn) {
$('#abhaMatch_alert').empty();
var origHtml = $btn.html();
if (action === 'new') {
var exactConflict = (_profile && _profile.already_registered) || (window._abhaMatchCandidates || []).some(function(c) {
return c.match && (c.match.abha || c.match.aadhaar);
});
if (exactConflict) {
$('#abhaMatch_alert').html(alertHtml('danger', 'Cannot create duplicate patient: An ABHA ID or Aadhaar Number can only belong to one patient in HMS. Please select and update the existing patient record.'));
return;
}
}
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Saving…');
$('#abhaMatch_create_new_btn,#abhaMatch_keep_existing_btn,#abhaMatch_update_existing_btn').prop('disabled', true);
@ -296,13 +326,79 @@ window.AbhaPatientMatchModal = (function () {
_onResolved = onResolved;
$('#abhaMatch_alert').empty();
$('#abhaMatch_keep_existing_btn,#abhaMatch_update_existing_btn').prop('disabled', true);
$('#abhaMatch_view_profile_btn').addClass('d-none');
$('#abhaMatch_duplicate_warning').addClass('d-none');
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));
// Detect exact ABHA or Aadhaar conflict
var exactAbhaCandidate = (candidates || []).find(function (c) {
return (c.match && c.match.abha) || (_profile.conflict_patient && Number(c.id) === Number(_profile.conflict_patient.id));
});
var exactAadhaarCandidate = (candidates || []).find(function (c) {
return c.match && c.match.aadhaar;
});
var isExactMatch = !!(exactAbhaCandidate || exactAadhaarCandidate || _profile.already_registered);
var conflictPatient = exactAbhaCandidate || exactAadhaarCandidate || _profile.conflict_patient || null;
if (isExactMatch && conflictPatient) {
// Case 1: Exact ABHA / Aadhaar Match already registered in HMS
$('#abhaMatch_modal_icon').attr('class', 'bi bi-shield-exclamation text-danger me-2');
$('#abhaMatch_modal_title_text').text('Patient Already Registered with this ABHA ID');
$('#abhaMatch_modal_title_badge').removeClass('d-none').text('Already Exists');
var profileLink = '<?= base_url('billing/patient/person_record/') ?>' + (conflictPatient.id || '');
$('#abhaMatch_alert').html(
'<div class="alert alert-danger py-2 mb-3">' +
' <div class="d-flex align-items-center justify-content-between flex-wrap gap-2">' +
' <div>' +
' <i class="bi bi-shield-lock-fill me-2 fs-5 align-middle"></i>' +
' <strong>Unique ABHA ID Policy:</strong> This ABHA ID is already linked to patient ' +
' <strong>' + esc(conflictPatient.p_code || '') + ' (' + esc(conflictPatient.name || conflictPatient.p_fname || '') + ')</strong>. ' +
' Each patient in HMS must have a unique ABHA ID. Creating a duplicate patient is not allowed.' +
' </div>' +
(conflictPatient.id ? ' <a href="' + profileLink + '" target="_blank" class="btn btn-sm btn-outline-danger text-nowrap"><i class="bi bi-box-arrow-up-right me-1"></i>View Profile</a>' : '') +
' </div>' +
'</div>'
);
// Disable "Create New Patient" because of duplicate conflict
$('#abhaMatch_create_new_btn').prop('disabled', true).addClass('disabled opacity-50 btn-outline-secondary').removeClass('btn-primary').attr('title', 'Disabled: ABHA ID is already linked to patient ' + (conflictPatient.p_code || ''));
$('#abhaMatch_duplicate_warning').removeClass('d-none');
if (conflictPatient.id) {
$('#abhaMatch_view_profile_btn').attr('href', profileLink).removeClass('d-none');
}
// Auto-select the matching existing patient
if (conflictPatient.id) {
selectCandidate(Number(conflictPatient.id));
}
} else if ((candidates || []).length > 0) {
// Case 2: No exact conflict, but potential demographic matches found
$('#abhaMatch_modal_icon').attr('class', 'bi bi-people-fill text-primary me-2');
$('#abhaMatch_modal_title_text').text('Matching Patients in HMS');
$('#abhaMatch_modal_title_badge').addClass('d-none');
$('#abhaMatch_create_new_btn').prop('disabled', false).removeClass('disabled opacity-50 btn-primary').addClass('btn-outline-secondary').removeAttr('title');
if (Number(preferredPatientId || 0) > 0
&& (candidates || []).some(function (candidate) { return Number(candidate.id) === Number(preferredPatientId); })) {
selectCandidate(Number(preferredPatientId));
}
} else {
// Case 3: 0 matches found in HMS (new person - e.g. Keshav Singh)
$('#abhaMatch_modal_icon').attr('class', 'bi bi-person-plus-fill text-success me-2');
$('#abhaMatch_modal_title_text').text('Register New Patient from ABHA');
$('#abhaMatch_modal_title_badge').addClass('d-none');
$('#abhaMatch_empty').text('No matching patient found in HMS by name, age or gender. You can register a new patient record with this verified ABHA profile.');
// Highlight "Create New Patient" as the primary action
$('#abhaMatch_create_new_btn').prop('disabled', false).removeClass('disabled opacity-50 btn-outline-secondary').addClass('btn-primary').removeAttr('title');
}
if (_bsModal) { _bsModal.show(); } else { $modalEl.modal('show'); }
}
};

View File

@ -70,6 +70,7 @@
</section>
<section id="abhaVerifyStep4" class="d-none">
<div id="abhaVerifyStatusAlert"></div>
<div class="row g-4">
<div class="col-lg-6">
<div class="d-flex align-items-center gap-3 mb-3">
@ -254,6 +255,28 @@ window.AbhaVerifyModal = (function () {
$('#abhaVerifyCardWrap').html('<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i>' + reason + '</div>');
$('#abhaVerifyDownloadCard').addClass('d-none');
}
var isRegistered = !!(profile.already_registered && profile.conflict_patient);
if (isRegistered) {
var cp = profile.conflict_patient;
$('#abhaVerifyStatusAlert').html(
'<div class="alert alert-warning py-2 mb-3">' +
' <i class="bi bi-shield-lock-fill me-2 fs-5 align-middle"></i>' +
' <strong>Already Registered:</strong> This ABHA ID is already linked to HMS patient ' +
' <strong>' + escapeHtml(cp.p_code || '') + ' (' + escapeHtml(cp.name || cp.p_fname || '') + ')</strong>.' +
'</div>'
);
$('#abhaVerifyCompareBtn').html('<i class="bi bi-person-check me-1"></i>View Linked Patient in HMS').removeClass('btn-success').addClass('btn-primary');
} else {
$('#abhaVerifyStatusAlert').empty();
var count = (profile.candidates || []).length;
if (count === 0) {
$('#abhaVerifyCompareBtn').html('<i class="bi bi-person-plus me-1"></i>Register as New Patient').removeClass('btn-primary').addClass('btn-success');
} else {
$('#abhaVerifyCompareBtn').html('<i class="bi bi-people me-1"></i>Compare with HMS Patients').removeClass('btn-primary').addClass('btn-success');
}
}
showStep(4);
}
function verifyOtp() {
@ -313,6 +336,7 @@ window.AbhaVerifyModal = (function () {
: 'Enter the patient\'s 14-digit ABHA Number or ID. A mobile number is not required for account lookup.');
$('#abhaVerifyIdentifier').val(identifier || '');
$('#abhaVerifyPhoto,#abhaVerifyDownloadCard').addClass('d-none');
$('#abhaVerifyStatusAlert').empty();
showStep(1);
modal.show();
if (initialLookup && initialLookup.ok == 1) {