fix(patient): enforce unique ABHA ID and Aadhaar number on patient registration and updates

This commit is contained in:
DevSoftTech Server 2026-09-12 04:30:10 +05:30
parent 35e1018c6b
commit 7e3c57f48f
5 changed files with 704 additions and 97 deletions

View File

@ -549,6 +549,8 @@ $routes->group('billing', function($routes) {
$routes->get('patient/search_ajax', 'Patient::search_ajax');
$routes->post('patient/search_adv', 'Patient::search_adv');
$routes->post('patient/abha_fetch_profile', 'Patient::abha_fetch_profile');
$routes->post('patient/check_abha_unique', 'Patient::check_abha_unique');
$routes->post('patient/check_aadhaar_unique', 'Patient::check_aadhaar_unique');
$routes->post('patient/abha_aadhaar_generate_otp', 'Patient::abhaAadhaarGenerateOtp');
$routes->post('patient/abha_aadhaar_verify_otp', 'Patient::abhaAadhaarVerifyOtp');
$routes->post('patient/abha_mobile_generate_otp', 'Patient::abhaMobileGenerateOtp');
@ -1300,6 +1302,8 @@ $routes->get('Patient/show_cards/(:num)/(:num)', 'Patient::show_cards/$1/$2');
$routes->post('Patient/update_card', 'Patient::update_card');
$routes->post('Patient/update_aadhar', 'Patient::update_aadhar');
$routes->post('Patient/create', 'Patient::create');
$routes->post('Patient/check_abha_unique', 'Patient::check_abha_unique');
$routes->post('Patient/check_aadhaar_unique', 'Patient::check_aadhaar_unique');
$routes->post('OcasePathLap/list_pathtest_bytype', 'OcasePathLap::list_pathtest_bytype');
$routes->post('OcasePathLap/get_echs_id', 'OcasePathLap::get_echs_id');
$routes->post('OcasePathLap/update_refer_doc', 'OcasePathLap::update_refer_doc');

View File

@ -1797,22 +1797,41 @@ class Abha extends BaseController
// ABHA number must be unique across patient_master — never let two
// patient rows carry the same ABHA.
if ($abhaField && $abhaNumClean !== '') {
$conflict = $db->table('patient_master')
->select('id,p_code,p_fname')
->where($abhaField, $abhaNumClean)
->get()->getRowArray();
if ($conflict) {
$conflictId = (int) ($conflict['id'] ?? 0);
if ($action === 'new' || $conflictId !== $patientId) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => 'This ABHA number is already linked to patient '
. ($conflict['p_code'] ?? '') . ' (' . ($conflict['p_fname'] ?? '') . '). '
. 'Please select that patient instead.',
'conflict_patient_id' => $conflictId,
'conflict_p_code' => (string) ($conflict['p_code'] ?? ''),
]);
if ($abhaNumClean !== '') {
$formattedAbha = (strlen($abhaNumClean) === 14)
? substr($abhaNumClean, 0, 2) . '-' . substr($abhaNumClean, 2, 4) . '-' . substr($abhaNumClean, 6, 4) . '-' . substr($abhaNumClean, 10, 4)
: '';
$checkVals = array_values(array_filter([$abhaNumClean, $formattedAbha]));
$conflictBuilder = $db->table('patient_master')->select('id,p_code,p_fname');
$hasAbhaCheck = false;
$conflictBuilder->groupStart();
foreach (['abha_id', 'abha_no', 'abha'] as $cand) {
if (in_array($cand, $fields, true)) {
if (! $hasAbhaCheck) {
$conflictBuilder->whereIn($cand, $checkVals);
$hasAbhaCheck = true;
} else {
$conflictBuilder->orWhereIn($cand, $checkVals);
}
}
}
$conflictBuilder->groupEnd();
if ($hasAbhaCheck) {
$conflict = $conflictBuilder->get()->getRowArray();
if ($conflict) {
$conflictId = (int) ($conflict['id'] ?? 0);
if ($action === 'new' || $conflictId !== $patientId) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => 'This ABHA number is already linked to patient '
. ($conflict['p_code'] ?? '') . ' (' . ($conflict['p_fname'] ?? '') . '). '
. 'Please select that patient instead.',
'conflict_patient_id' => $conflictId,
'conflict_p_code' => (string) ($conflict['p_code'] ?? ''),
]);
}
}
}
}

View File

@ -133,6 +133,49 @@ class Patient extends BaseController
->with('error', $referByError);
}
// Enforce unique ABHA ID across patients
if ($abhaId !== '') {
$conflictAbha = $this->findPatientByAbha($abhaId);
if ($conflictAbha !== null) {
$msg = 'This ABHA ID is already linked to patient ' . ($conflictAbha['p_code'] ?? '') . ' (' . ($conflictAbha['p_fname'] ?? '') . '). ABHA ID must be unique to one person.';
if ($isAjax) {
return $this->response->setJSON([
'insertid' => 0,
'error_text' => $msg,
]);
}
return redirect()->to(base_url('billing/patient'))
->withInput()
->with('error', $msg);
}
}
// Enforce unique Aadhaar Number across patients
$rawAadhaar = trim((string) $this->request->getPost('input_udai'));
if ($rawAadhaar !== '') {
$vault = new \App\Libraries\AadhaarVaultService();
if (! $vault->isMasked($rawAadhaar)) {
$digits = $vault->normalize($rawAadhaar);
if ($digits !== '') {
$conflictAadhaar = $this->findPatientByAadhaar($digits);
if ($conflictAadhaar !== null) {
$msg = 'This Aadhaar Number is already linked to patient ' . ($conflictAadhaar['p_code'] ?? '') . ' (' . ($conflictAadhaar['p_fname'] ?? '') . '). Aadhaar Number must be unique to one person.';
if ($isAjax) {
return $this->response->setJSON([
'insertid' => 0,
'error_text' => $msg,
]);
}
return redirect()->to(base_url('billing/patient'))
->withInput()
->with('error', $msg);
}
}
}
}
$bloodGroup = trim((string) $this->request->getPost('input_blood_group'));
if ($bloodGroup === '') {
$bloodGroup = 'Not Define';
@ -259,13 +302,39 @@ class Patient extends BaseController
}
if ($inputAadhar !== '') {
// Aadhaar is stored encrypted, so match on its deterministic hash.
$builder->orWhere('udai_hash', (new \App\Libraries\AadhaarVaultService())->hash($inputAadhar));
$hasCondition = true;
$cleanAadhaar = preg_replace('/\D/', '', $inputAadhar) ?? '';
if (strlen($cleanAadhaar) === 12) {
$vault = new \App\Libraries\AadhaarVaultService();
$aadhaarHash = $vault->hash($cleanAadhaar);
$formattedAadhaar = substr($cleanAadhaar, 0, 4) . ' ' . substr($cleanAadhaar, 4, 4) . ' ' . substr($cleanAadhaar, 8, 4);
$pmCols = $this->db->getFieldNames('patient_master') ?? [];
$builder->orGroupStart();
$hasAadhaarOr = false;
if (in_array('udai_hash', $pmCols, true) && $aadhaarHash !== '') {
$builder->where('udai_hash', $aadhaarHash);
$hasAadhaarOr = true;
}
if (in_array('udai', $pmCols, true)) {
if ($hasAadhaarOr) {
$builder->orWhereIn('udai', [$cleanAadhaar, $formattedAadhaar]);
} else {
$builder->whereIn('udai', [$cleanAadhaar, $formattedAadhaar]);
}
}
$builder->groupEnd();
$hasCondition = true;
}
}
if ($inputAbhaId !== '' && $abhaField !== null) {
$builder->orWhere($abhaField, $inputAbhaId);
$cleanAbha = preg_replace('/\D/', '', $inputAbhaId) ?? '';
if (strlen($cleanAbha) === 14) {
$formattedAbha = substr($cleanAbha, 0, 2) . '-' . substr($cleanAbha, 2, 4) . '-' . substr($cleanAbha, 6, 4) . '-' . substr($cleanAbha, 10, 4);
$builder->orWhereIn($abhaField, [$cleanAbha, $formattedAbha]);
} else {
$builder->orWhere($abhaField, $inputAbhaId);
}
$hasCondition = true;
}
@ -308,27 +377,23 @@ class Patient extends BaseController
return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA ID must be a 14-digit number']);
}
$profile = [];
$abhaField = $this->resolvePatientAbhaIdField();
if ($abhaField !== null) {
$row = $this->db->table('patient_master')
->select('id,p_fname,gender,dob,mphone1,city,state')
->where($abhaField, $abhaId)
->get(1)
->getRowArray();
if (! empty($row)) {
$profile = [
'name' => (string) ($row['p_fname'] ?? ''),
'gender' => (string) ($row['gender'] ?? ''),
'dob' => (string) ($row['dob'] ?? ''),
'mobile' => (string) ($row['mphone1'] ?? ''),
'city' => (string) ($row['city'] ?? ''),
'state' => (string) ($row['state'] ?? ''),
];
}
// Check if this ABHA is already registered to a patient in HMS
$conflict = $this->findPatientByAbha($abhaId);
if ($conflict !== null) {
return $this->response->setJSON([
'ok' => 1,
'already_registered' => true,
'conflict_patient' => [
'id' => (int) $conflict['id'],
'p_code' => (string) ($conflict['p_code'] ?? ''),
'p_fname' => (string) ($conflict['p_fname'] ?? ''),
'profile_url' => base_url('billing/patient/person_record/' . (int) $conflict['id']),
],
'message' => 'This ABHA ID is already registered to patient ' . ($conflict['p_code'] ?? '') . ' (' . ($conflict['p_fname'] ?? '') . '). ABHA ID must be unique to one person.',
]);
}
$profile = [];
$queueId = null;
try {
$bridge = new BridgeSyncService();
@ -341,9 +406,94 @@ class Patient extends BaseController
return $this->response->setJSON([
'ok' => 1,
'already_registered' => false,
'queue_id' => $queueId,
'profile' => $profile,
'source' => ! empty($profile) ? 'local_cache' : 'abdm_queue',
'source' => 'abdm_queue',
]);
}
public function check_abha_unique()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$abha = trim((string) ($this->request->getPost('input_abha_id') ?? $this->request->getPost('abha_id') ?? ''));
$excludeId = (int) ($this->request->getPost('patient_id') ?? 0);
if ($abha === '') {
return $this->response->setJSON(['ok' => 1, 'is_unique' => true, 'exists' => false]);
}
$conflict = $this->findPatientByAbha($abha, $excludeId > 0 ? $excludeId : null);
if ($conflict !== null) {
return $this->response->setJSON([
'ok' => 1,
'is_unique' => false,
'exists' => true,
'conflict_patient' => [
'id' => (int) $conflict['id'],
'p_code' => (string) ($conflict['p_code'] ?? ''),
'p_fname' => (string) ($conflict['p_fname'] ?? ''),
'profile_url' => base_url('billing/patient/person_record/' . (int) $conflict['id']),
],
'message' => 'ABHA ID is already registered to patient ' . ($conflict['p_code'] ?? '') . ' (' . ($conflict['p_fname'] ?? '') . '). ABHA ID must be unique to one person.',
]);
}
return $this->response->setJSON([
'ok' => 1,
'is_unique' => true,
'exists' => false,
'message' => 'ABHA ID is available.',
]);
}
public function check_aadhaar_unique()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$aadhaar = trim((string) ($this->request->getPost('input_udai') ?? $this->request->getPost('udai') ?? ''));
$excludeId = (int) ($this->request->getPost('patient_id') ?? 0);
if ($aadhaar === '') {
return $this->response->setJSON(['ok' => 1, 'is_unique' => true, 'exists' => false]);
}
$vault = new \App\Libraries\AadhaarVaultService();
if ($vault->isMasked($aadhaar)) {
return $this->response->setJSON(['ok' => 1, 'is_unique' => true, 'exists' => false, 'masked' => true]);
}
$digits = $vault->normalize($aadhaar);
if ($digits === '') {
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Aadhaar must be a 12-digit number']);
}
$conflict = $this->findPatientByAadhaar($digits, $excludeId > 0 ? $excludeId : null);
if ($conflict !== null) {
return $this->response->setJSON([
'ok' => 1,
'is_unique' => false,
'exists' => true,
'conflict_patient' => [
'id' => (int) $conflict['id'],
'p_code' => (string) ($conflict['p_code'] ?? ''),
'p_fname' => (string) ($conflict['p_fname'] ?? ''),
'profile_url' => base_url('billing/patient/person_record/' . (int) $conflict['id']),
],
'message' => 'Aadhaar Number is already registered to patient ' . ($conflict['p_code'] ?? '') . ' (' . ($conflict['p_fname'] ?? '') . '). Aadhaar Number must be unique to one person.',
]);
}
return $this->response->setJSON([
'ok' => 1,
'is_unique' => true,
'exists' => false,
'message' => 'Aadhaar Number is available.',
]);
}
@ -995,6 +1145,33 @@ class Patient extends BaseController
]);
}
// Enforce unique ABHA ID across patients (excluding current patient)
if ($abhaId !== '') {
$conflictAbha = $this->findPatientByAbha($abhaId, $pid);
if ($conflictAbha !== null) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'This ABHA ID is already linked to patient ' . ($conflictAbha['p_code'] ?? '') . ' (' . ($conflictAbha['p_fname'] ?? '') . '). ABHA ID must be unique to one person.',
]);
}
}
// Enforce unique Aadhaar Number across patients (excluding current patient)
$aadhaarVault = new \App\Libraries\AadhaarVaultService();
$aadhaarInput = trim((string) $this->request->getPost('input_Aadhar'));
if ($aadhaarInput !== '' && ! $aadhaarVault->isMasked($aadhaarInput)) {
$digits = $aadhaarVault->normalize($aadhaarInput);
if ($digits !== '') {
$conflictAadhaar = $this->findPatientByAadhaar($digits, $pid);
if ($conflictAadhaar !== null) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'This Aadhaar Number is already linked to patient ' . ($conflictAadhaar['p_code'] ?? '') . ' (' . ($conflictAadhaar['p_fname'] ?? '') . '). Aadhaar Number must be unique to one person.',
]);
}
}
}
$data = [
'mphone1' => $this->request->getPost('input_mphone1'),
'p_fname' => strtoupper((string) $this->request->getPost('input_name')),
@ -1011,8 +1188,6 @@ class Patient extends BaseController
'estimate_dob' => $estimate_dob,
'blood_group' => $this->request->getPost('input_blood_group'),
];
$aadhaarVault = new \App\Libraries\AadhaarVaultService();
$aadhaarInput = (string) $this->request->getPost('input_Aadhar');
if (! $aadhaarVault->isMasked($aadhaarInput)) {
$data += $aadhaarVault->buildColumns($aadhaarInput);
}
@ -1122,8 +1297,19 @@ class Patient extends BaseController
if ($vault->isMasked($udai)) {
return $this->response->setJSON(['update' => 1, 'showcontent' => 'Aadhaar unchanged']);
}
if (trim($udai) !== '' && $vault->normalize($udai) === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Enter a valid 12-digit Aadhaar number']);
if (trim($udai) !== '') {
$digits = $vault->normalize($udai);
if ($digits === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Enter a valid 12-digit Aadhaar number']);
}
$conflictAadhaar = $this->findPatientByAadhaar($digits, $pid);
if ($conflictAadhaar !== null) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'This Aadhaar Number is already linked to patient ' . ($conflictAadhaar['p_code'] ?? '') . ' (' . ($conflictAadhaar['p_fname'] ?? '') . '). Aadhaar Number must be unique to one person.',
]);
}
}
$patientModel = new PatientModel();
@ -1168,21 +1354,14 @@ class Patient extends BaseController
// ABHA number must be unique across patient_master — block if another
// patient already carries this exact ABHA id.
if ($abhaId !== '') {
$abhaField = $this->resolvePatientAbhaIdField();
if ($abhaField !== null) {
$conflict = $this->db->table('patient_master')
->select('id,p_code,p_fname')
->where($abhaField, $abhaId)
->where('id !=', $pid)
->get()->getRowArray();
if ($conflict) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'This ABHA number is already linked to patient '
. ($conflict['p_code'] ?? '') . ' (' . ($conflict['p_fname'] ?? '') . '). '
. 'An ABHA number can only be linked to one patient.',
]);
}
$conflict = $this->findPatientByAbha($abhaId, $pid);
if ($conflict !== null) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'This ABHA ID is already linked to patient '
. ($conflict['p_code'] ?? '') . ' (' . ($conflict['p_fname'] ?? '') . '). '
. 'An ABHA ID can only be linked to one patient.',
]);
}
}
@ -4048,6 +4227,201 @@ class Patient extends BaseController
return preg_match('/^\d{14}$/', $value) === 1;
}
/**
* Find an existing patient in patient_master by ABHA ID (14 digits) or ABHA Address.
* Matches raw digits, formatted (XX-XXXX-XXXX-XXXX), or ABHA address across all existing ABHA columns.
*
* @return array{id:int,p_code:string,p_fname:string,mphone1:string,abha_id:string,udai:string}|null
*/
public function findPatientByAbha(string $abha, ?int $excludePatientId = null): ?array
{
$abha = trim($abha);
if ($abha === '') {
return null;
}
$db = $this->db ?? \Config\Database::connect();
if (! $db->tableExists('patient_master')) {
return null;
}
$fields = $db->getFieldNames('patient_master') ?? [];
$digits = preg_replace('/\D/', '', $abha) ?? '';
$is14Digits = strlen($digits) === 14;
$formatted14 = $is14Digits
? substr($digits, 0, 2) . '-' . substr($digits, 2, 4) . '-' . substr($digits, 6, 4) . '-' . substr($digits, 10, 4)
: '';
$builder = $db->table('patient_master');
$builder->select('id,p_code,p_fname,mphone1');
$selectAbhaField = null;
foreach (['abha_id', 'abha_no', 'abha'] as $f) {
if (in_array($f, $fields, true)) {
$selectAbhaField = $f;
break;
}
}
if ($selectAbhaField !== null) {
$builder->select($selectAbhaField . ' AS abha_id');
}
if (in_array('udai', $fields, true)) {
$builder->select('udai');
}
$matchedAnyCondition = false;
$builder->groupStart();
// Check all candidate numeric ABHA columns
foreach (['abha_id', 'abha_no', 'abha'] as $f) {
if (in_array($f, $fields, true)) {
if ($is14Digits) {
if (! $matchedAnyCondition) {
$builder->whereIn($f, [$digits, $formatted14]);
$matchedAnyCondition = true;
} else {
$builder->orWhereIn($f, [$digits, $formatted14]);
}
} else {
if (! $matchedAnyCondition) {
$builder->where($f, $abha);
$matchedAnyCondition = true;
} else {
$builder->orWhere($f, $abha);
}
}
}
}
// Check abha_address column if ABHA address provided (e.g. name@abdm)
if (in_array('abha_address', $fields, true) && strpos($abha, '@') !== false) {
if (! $matchedAnyCondition) {
$builder->where('abha_address', $abha);
$matchedAnyCondition = true;
} else {
$builder->orWhere('abha_address', $abha);
}
}
$builder->groupEnd();
if (! $matchedAnyCondition) {
return null;
}
if ($excludePatientId !== null && $excludePatientId > 0) {
$builder->where('id !=', $excludePatientId);
}
$row = $builder->orderBy('id', 'ASC')->get(1)->getRowArray();
if (! $row) {
return null;
}
return [
'id' => (int) ($row['id'] ?? 0),
'p_code' => (string) ($row['p_code'] ?? ''),
'p_fname' => (string) ($row['p_fname'] ?? ''),
'mphone1' => (string) ($row['mphone1'] ?? ''),
'abha_id' => (string) ($row['abha_id'] ?? ''),
'udai' => (string) ($row['udai'] ?? ''),
];
}
/**
* Find an existing patient in patient_master by Aadhaar number (12 digits).
* Matches deterministic hash in udai_hash and unmasked digits in udai.
*
* @return array{id:int,p_code:string,p_fname:string,mphone1:string,abha_id:string,udai:string}|null
*/
public function findPatientByAadhaar(string $aadhaar, ?int $excludePatientId = null): ?array
{
$aadhaar = trim($aadhaar);
if ($aadhaar === '') {
return null;
}
$vault = new \App\Libraries\AadhaarVaultService();
if ($vault->isMasked($aadhaar)) {
return null;
}
$digits = $vault->normalize($aadhaar);
if ($digits === '') {
return null;
}
$db = $this->db ?? \Config\Database::connect();
if (! $db->tableExists('patient_master')) {
return null;
}
$fields = $db->getFieldNames('patient_master') ?? [];
$hash = $vault->hash($digits);
$formattedWithSpaces = substr($digits, 0, 4) . ' ' . substr($digits, 4, 4) . ' ' . substr($digits, 8, 4);
$builder = $db->table('patient_master');
$builder->select('id,p_code,p_fname,mphone1');
$selectAbhaField = null;
foreach (['abha_id', 'abha_no', 'abha'] as $f) {
if (in_array($f, $fields, true)) {
$selectAbhaField = $f;
break;
}
}
if ($selectAbhaField !== null) {
$builder->select($selectAbhaField . ' AS abha_id');
}
if (in_array('udai', $fields, true)) {
$builder->select('udai');
}
$hasHashField = in_array('udai_hash', $fields, true);
$hasUdaiField = in_array('udai', $fields, true);
if (! $hasHashField && ! $hasUdaiField) {
return null;
}
$builder->groupStart();
$hasCond = false;
if ($hasHashField && $hash !== '') {
$builder->where('udai_hash', $hash);
$hasCond = true;
}
if ($hasUdaiField) {
if (! $hasCond) {
$builder->whereIn('udai', [$digits, $formattedWithSpaces]);
$hasCond = true;
} else {
$builder->orWhereIn('udai', [$digits, $formattedWithSpaces]);
}
}
$builder->groupEnd();
if ($excludePatientId !== null && $excludePatientId > 0) {
$builder->where('id !=', $excludePatientId);
}
$row = $builder->orderBy('id', 'ASC')->get(1)->getRowArray();
if (! $row) {
return null;
}
return [
'id' => (int) ($row['id'] ?? 0),
'p_code' => (string) ($row['p_code'] ?? ''),
'p_fname' => (string) ($row['p_fname'] ?? ''),
'mphone1' => (string) ($row['mphone1'] ?? ''),
'abha_id' => (string) ($row['abha_id'] ?? ''),
'udai' => (string) ($row['udai'] ?? ''),
];
}
/**
* @param array<string, mixed> $patientData
*/

View File

@ -96,6 +96,7 @@
placeholder="Aadhaar Number" type="text" autocomplete="off"
data-inputmask='"mask": "999999999999"' data-mask
onchange="onchange_aadhar()">
<div id="udai_feedback" class="mt-1" style="display:none;"></div>
</div>
</div>
<div class="col-md-3">
@ -104,6 +105,7 @@
<input class="form-control" name="input_abha_id" id="input_abha_id"
placeholder="14-digit ABHA ID" type="text" autocomplete="off"
maxlength="14" data-inputmask='"mask": "99999999999999"' data-mask>
<div id="abha_id_feedback" class="mt-1" style="display:none;"></div>
</div>
</div>
<div class="col-md-3">
@ -982,6 +984,12 @@
syncAdvancedSearchFields();
$('form.form1').on('submit', function(form) {
if (isAbhaDuplicateConflict || isAadhaarDuplicateConflict) {
form.preventDefault();
notify('error', 'Duplicate Identifier', 'Cannot register patient: ABHA ID or Aadhaar Number is already registered in the system.');
return;
}
if (!validateReferByAgainstMaster('#refer_by_name', 'refer_by_name_list')) {
form.preventDefault();
return;
@ -1709,25 +1717,142 @@
})();
/* ===== END ABHA REGISTRATION WIZARD ===== */
$('#input_abha_id').on('change blur', function() {
var abhaId = ($(this).val() || '').toString().trim();
if (abhaId !== '') {
refreshAdvancedSearchResult({
input_abha_id: abhaId
});
} else {
$('#search_result_update').html('');
}
var isAbhaDuplicateConflict = false;
var isAadhaarDuplicateConflict = false;
if (!/^\d{14}$/.test(abhaId)) {
function updateRegisterButtonState() {
if (isAbhaDuplicateConflict || isAadhaarDuplicateConflict) {
$('#RegisterPatient').prop('disabled', true);
} else {
$('#RegisterPatient').prop('disabled', false);
}
}
function validateAbhaUniqueness() {
var rawVal = ($('#input_abha_id').val() || '').toString().trim();
var digits = rawVal.replace(/\D/g, '');
var isAddress = rawVal.indexOf('@') !== -1;
if (rawVal === '') {
isAbhaDuplicateConflict = false;
$('#input_abha_id').removeClass('is-invalid is-valid');
$('#abha_id_feedback').hide().empty();
updateRegisterButtonState();
return;
}
$.post('<?= base_url('patient/abha_fetch_profile') ?>', {
abha_id: abhaId,
'<?= csrf_token() ?>': $('input[name="<?= csrf_token() ?>"]').val()
}, function(resp) {
if (!resp || resp.ok != 1 || !resp.profile) {
if (digits.length !== 14 && !isAddress) {
isAbhaDuplicateConflict = false;
$('#input_abha_id').removeClass('is-invalid is-valid');
$('#abha_id_feedback').hide().empty();
updateRegisterButtonState();
return;
}
var csrf = getCsrfData();
var postData = {
input_abha_id: rawVal
};
postData[csrf.name] = csrf.value;
$.post('<?= base_url('billing/patient/check_abha_unique') ?>', postData, function(resp) {
if (resp && resp.is_unique === false) {
isAbhaDuplicateConflict = true;
$('#input_abha_id').addClass('is-invalid').removeClass('is-valid');
var p = resp.conflict_patient || {};
var conflictHtml = '<div class="alert alert-danger py-1 px-2 mb-0 small">' +
'<i class="bi bi-shield-fill-exclamation me-1"></i><strong>ABHA ID Already Registered!</strong><br/>' +
'Linked to: <strong>' + (p.p_fname || 'Existing Patient') + '</strong> (' + (p.p_code || '') + ')<br/>' +
'<a href="' + (p.profile_url || '#') + '" class="btn btn-xs btn-outline-danger mt-1" onclick="event.preventDefault(); load_form(\'' + p.profile_url + '\', \'Patient Record\');">' +
'<i class="bi bi-person-badge me-1"></i>View Existing Patient Profile</a>' +
'</div>';
$('#abha_id_feedback').html(conflictHtml).show();
updateRegisterButtonState();
} else {
isAbhaDuplicateConflict = false;
$('#input_abha_id').removeClass('is-invalid').addClass('is-valid');
$('#abha_id_feedback').hide().empty();
updateRegisterButtonState();
if (digits.length === 14) {
fetchAbdmProfile(digits);
}
}
}, 'json');
}
function validateAadhaarUniqueness() {
var rawVal = ($('#input_udai').val() || '').toString().trim();
var digits = rawVal.replace(/\D/g, '');
if (rawVal === '') {
isAadhaarDuplicateConflict = false;
$('#input_udai').removeClass('is-invalid is-valid');
$('#udai_feedback').hide().empty();
updateRegisterButtonState();
return;
}
if (digits.length !== 12) {
isAadhaarDuplicateConflict = false;
$('#input_udai').removeClass('is-invalid is-valid');
$('#udai_feedback').hide().empty();
updateRegisterButtonState();
return;
}
var csrf = getCsrfData();
var postData = {
input_udai: rawVal
};
postData[csrf.name] = csrf.value;
$.post('<?= base_url('billing/patient/check_aadhaar_unique') ?>', postData, function(resp) {
if (resp && resp.is_unique === false) {
isAadhaarDuplicateConflict = true;
$('#input_udai').addClass('is-invalid').removeClass('is-valid');
var p = resp.conflict_patient || {};
var conflictHtml = '<div class="alert alert-danger py-1 px-2 mb-0 small">' +
'<i class="bi bi-person-vcard-fill me-1"></i><strong>Aadhaar Already Registered!</strong><br/>' +
'Linked to: <strong>' + (p.p_fname || 'Existing Patient') + '</strong> (' + (p.p_code || '') + ')<br/>' +
'<a href="' + (p.profile_url || '#') + '" class="btn btn-xs btn-outline-danger mt-1" onclick="event.preventDefault(); load_form(\'' + p.profile_url + '\', \'Patient Record\');">' +
'<i class="bi bi-person-badge me-1"></i>View Existing Patient Profile</a>' +
'</div>';
$('#udai_feedback').html(conflictHtml).show();
updateRegisterButtonState();
} else {
isAadhaarDuplicateConflict = false;
$('#input_udai').removeClass('is-invalid').addClass('is-valid');
$('#udai_feedback').hide().empty();
updateRegisterButtonState();
}
}, 'json');
}
function fetchAbdmProfile(abhaId) {
var csrf = getCsrfData();
var postData = {
abha_id: abhaId
};
postData[csrf.name] = csrf.value;
$.post('<?= base_url('billing/patient/abha_fetch_profile') ?>', postData, function(resp) {
if (!resp || resp.ok != 1) {
return;
}
if (resp.already_registered && resp.conflict_patient) {
isAbhaDuplicateConflict = true;
$('#input_abha_id').addClass('is-invalid').removeClass('is-valid');
var cp = resp.conflict_patient;
var conflictHtml = '<div class="alert alert-danger py-1 px-2 mb-0 small">' +
'<i class="bi bi-shield-fill-exclamation me-1"></i><strong>ABHA ID Already Registered!</strong><br/>' +
'Linked to: <strong>' + (cp.p_fname || 'Existing Patient') + '</strong> (' + (cp.p_code || '') + ')<br/>' +
'<a href="' + (cp.profile_url || '#') + '" class="btn btn-xs btn-outline-danger mt-1" onclick="event.preventDefault(); load_form(\'' + cp.profile_url + '\', \'Patient Record\');">' +
'<i class="bi bi-person-badge me-1"></i>View Existing Patient Profile</a>' +
'</div>';
$('#abha_id_feedback').html(conflictHtml).show();
updateRegisterButtonState();
return;
}
@ -1742,7 +1867,6 @@
$('#input_city').val(p.city);
}
if (p.state && !$('#input_state').val()) {
// Try to match existing option (case-insensitive); fall back to first partial match
var stateTarget = (p.state || '').trim().toUpperCase();
var $sel = $('#input_state');
var bestVal = '';
@ -1769,6 +1893,22 @@
}
}
}, 'json');
}
$('#input_abha_id').on('change blur input', function() {
var abhaId = ($(this).val() || '').toString().trim();
if (abhaId !== '') {
refreshAdvancedSearchResult({
input_abha_id: abhaId
});
} else {
$('#search_result_update').html('');
}
validateAbhaUniqueness();
});
$('#input_udai').on('blur input', function() {
validateAadhaarUniqueness();
});
$("#input_city").autocomplete({
@ -1960,6 +2100,9 @@
function onchange_aadhar(control_button) {
var input_udai = $('#input_udai').val();
if (typeof validateAadhaarUniqueness === 'function') {
validateAadhaarUniqueness();
}
refreshAdvancedSearchResult({
"input_udai": input_udai
});

View File

@ -51,27 +51,65 @@ $normalize = static function ($value): string {
$filters = $filters ?? [];
$filterPhone = trim((string) ($filters['input_mphone1'] ?? ''));
$filterAadhaar = $normalize($filters['input_udai'] ?? '');
// Aadhaar is stored masked/encrypted, so rows can only be compared on the last 4 digits.
$filterAadhaarLast4 = substr(preg_replace('/\D/', '', (string) ($filters['input_udai'] ?? '')) ?? '', -4);
$filterAadhaarDigits = preg_replace('/\D/', '', (string) ($filters['input_udai'] ?? '')) ?? '';
$filterAadhaarLast4 = substr($filterAadhaarDigits, -4);
$filterAbha = trim((string) ($filters['input_abha_id'] ?? ''));
$filterAbhaDigits = preg_replace('/\D/', '', $filterAbha) ?? '';
$filterName = $normalize($filters['input_name'] ?? '');
$filterRelativeName = $normalize($filters['input_relative_name'] ?? '');
$getMatchScore = static function ($row) use ($normalize, $filterPhone, $filterAadhaarLast4, $filterAbha, $filterName, $filterRelativeName): int {
$isAbhaMatch = static function ($row) use ($filterAbha, $filterAbhaDigits): bool {
if ($filterAbha === '') {
return false;
}
$rowAbha = trim((string) ($row->abha_id ?? ''));
if ($rowAbha === '') {
return false;
}
if ($rowAbha === $filterAbha) {
return true;
}
$rowAbhaDigits = preg_replace('/\D/', '', $rowAbha) ?? '';
if ($filterAbhaDigits !== '' && strlen($filterAbhaDigits) === 14 && $rowAbhaDigits === $filterAbhaDigits) {
return true;
}
return false;
};
$isAadhaarMatch = static function ($row) use ($filterAadhaarDigits, $filterAadhaarLast4): bool {
if ($filterAadhaarDigits === '' || strlen($filterAadhaarDigits) !== 12) {
return false;
}
$rowUdai = trim((string) ($row->udai ?? ''));
$rowLast4 = trim((string) ($row->udai_last4 ?? ''));
if ($rowLast4 !== '' && $rowLast4 === $filterAadhaarLast4) {
return true;
}
$rowUdaiDigits = preg_replace('/\D/', '', $rowUdai) ?? '';
if ($rowUdaiDigits === $filterAadhaarDigits) {
return true;
}
return false;
};
$getMatchScore = static function ($row) use ($normalize, $filterPhone, $filterName, $filterRelativeName, $isAbhaMatch, $isAadhaarMatch): int {
$score = 0;
// Direct identifier matches carry massive weight
if ($isAbhaMatch($row)) {
$score += 10;
}
if ($isAadhaarMatch($row)) {
$score += 10;
}
if ($filterPhone !== '' && trim((string) ($row->mphone1 ?? '')) === $filterPhone) {
$score++;
}
if ($filterAadhaarLast4 !== '' && trim((string) ($row->udai_last4 ?? '')) === $filterAadhaarLast4) {
$score++;
}
if ($filterAbha !== '' && trim((string) ($row->abha_id ?? '')) === $filterAbha) {
$score++;
}
if (
$filterName !== ''
&& $filterRelativeName !== ''
@ -84,7 +122,11 @@ $getMatchScore = static function ($row) use ($normalize, $filterPhone, $filterAa
return $score;
};
$getConfidence = static function (int $score): array {
$getConfidence = static function (int $score, bool $isIdentifierConflict): array {
if ($isIdentifierConflict || $score >= 10) {
return ['High (Duplicate Conflict)', 'bg-danger text-white'];
}
if ($score >= 3) {
return ['High', 'bg-danger'];
}
@ -131,15 +173,19 @@ if (! empty($search_result) && is_array($search_result)) {
$ageDisplay = $getAgeDisplay($row);
$gender = $genderLabel($row->gender ?? '');
$hasAbhaConflict = $isAbhaMatch($row);
$hasAadhaarConflict = $isAadhaarMatch($row);
$isIdentifierConflict = $hasAbhaConflict || $hasAadhaarConflict;
$matchReasons = [];
if ($filterPhone !== '' && trim((string) ($row->mphone1 ?? '')) === $filterPhone) {
$matchReasons[] = 'Phone';
}
if ($filterAadhaarLast4 !== '' && trim((string) ($row->udai_last4 ?? '')) === $filterAadhaarLast4) {
if ($hasAadhaarConflict) {
$matchReasons[] = 'Aadhaar';
}
if ($filterAbha !== '' && trim((string) ($row->abha_id ?? '')) === $filterAbha) {
$matchReasons[] = 'ABHA';
if ($hasAbhaConflict) {
$matchReasons[] = 'ABHA ID';
}
if (
$filterName !== ''
@ -150,19 +196,24 @@ if (! empty($search_result) && is_array($search_result)) {
$matchReasons[] = 'Name + Relative';
}
$isStrongDuplicate = count($matchReasons) > 1;
$matchScore = $getMatchScore($row);
[$confidenceText, $confidenceClass] = $getConfidence($matchScore);
$isStrongDuplicate = count($matchReasons) > 1 || $isIdentifierConflict;
$matchScore = $getMatchScore($row);
[$confidenceText, $confidenceClass] = $getConfidence($matchScore, $isIdentifierConflict);
?>
<div class="list-group-item list-group-item-action <?= $isStrongDuplicate ? 'border border-warning' : '' ?>"
<div class="list-group-item list-group-item-action <?= $isIdentifierConflict ? 'border-2 border-danger shadow-sm' : ($isStrongDuplicate ? 'border border-warning' : '') ?>"
role="button"
tabindex="0"
onclick="load_form('<?= esc($profileUrl) ?>', 'Patient Record');"
onkeydown="if(event.key==='Enter' || event.key===' '){event.preventDefault();load_form('<?= esc($profileUrl) ?>', 'Patient Record');}">
<div class="d-flex justify-content-between align-items-start gap-2">
<div class="fw-semibold"><?= esc($fullName !== '' ? $fullName : 'Unknown') ?></div>
<div class="fw-semibold <?= $isIdentifierConflict ? 'text-danger' : '' ?>">
<?= esc($fullName !== '' ? $fullName : 'Unknown') ?>
<?php if ($isIdentifierConflict): ?>
<span class="badge bg-danger ms-1"><i class="bi bi-exclamation-octagon-fill me-1"></i>ALREADY REGISTERED</span>
<?php endif; ?>
</div>
<button type="button"
class="btn btn-sm btn-outline-primary"
class="btn btn-sm <?= $isIdentifierConflict ? 'btn-danger' : 'btn-outline-primary' ?>"
onclick="event.stopPropagation(); load_form('<?= esc($profileUrl) ?>', 'Patient Record');">
View Profile
</button>
@ -171,11 +222,27 @@ if (! empty($search_result) && is_array($search_result)) {
<div class="small fst-italic text-muted">Refer By: <?= esc(ucwords(strtolower($referBy))) ?></div>
<?php endif; ?>
<div class="small text-muted"><?= esc($relative !== '' ? $relative : 'Relative: NA') ?></div>
<?php if ($hasAbhaConflict): ?>
<div class="alert alert-danger py-1 px-2 mb-1 mt-2 small">
<i class="bi bi-shield-lock-fill me-1"></i><strong>ABHA Conflict:</strong> Already linked to this patient. Cannot register new person with same ABHA ID.
</div>
<?php endif; ?>
<?php if ($hasAadhaarConflict): ?>
<div class="alert alert-danger py-1 px-2 mb-1 mt-1 small">
<i class="bi bi-person-vcard-fill me-1"></i><strong>Aadhaar Conflict:</strong> Already linked to this patient. Cannot register new person with same Aadhaar.
</div>
<?php endif; ?>
<?php if (! empty($matchReasons)) : ?>
<div class="mt-1">
<span class="badge <?= esc($confidenceClass) ?> me-1">Confidence: <?= esc($confidenceText) ?></span>
<?php foreach ($matchReasons as $reason) : ?>
<span class="badge <?= $isStrongDuplicate ? 'bg-warning text-dark' : 'bg-info text-dark' ?> me-1">Match: <?= esc($reason) ?></span>
<?php $isCrit = ($reason === 'ABHA ID' || $reason === 'Aadhaar'); ?>
<span class="badge <?= $isCrit ? 'bg-danger text-white' : ($isStrongDuplicate ? 'bg-warning text-dark' : 'bg-info text-dark') ?> me-1">
<?= $isCrit ? '<i class="bi bi-exclamation-circle-fill me-1"></i>' : '' ?>Match: <?= esc($reason) ?>
</span>
<?php endforeach; ?>
</div>
<?php endif; ?>