From 7e3c57f48fea2e6a278f21c4e06d7ff22e4d9a50 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sat, 12 Sep 2026 04:30:10 +0530 Subject: [PATCH] fix(patient): enforce unique ABHA ID and Aadhaar number on patient registration and updates --- app/Config/Routes.php | 4 + app/Controllers/Abha.php | 51 ++- app/Controllers/Patient.php | 460 +++++++++++++++++++++++--- app/Views/billing/Patient_V.php | 175 +++++++++- app/Views/billing/search_adv_data.php | 111 +++++-- 5 files changed, 704 insertions(+), 97 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 9b8de4b..998eb18 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -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'); diff --git a/app/Controllers/Abha.php b/app/Controllers/Abha.php index 24a80ef..c092c83 100644 --- a/app/Controllers/Abha.php +++ b/app/Controllers/Abha.php @@ -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'] ?? ''), + ]); + } } } } diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index 3181ca8..7260f30 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -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 $patientData */ diff --git a/app/Views/billing/Patient_V.php b/app/Views/billing/Patient_V.php index 823e6e2..c9a13c4 100644 --- a/app/Views/billing/Patient_V.php +++ b/app/Views/billing/Patient_V.php @@ -96,6 +96,7 @@ placeholder="Aadhaar Number" type="text" autocomplete="off" data-inputmask='"mask": "999999999999"' data-mask onchange="onchange_aadhar()"> +
@@ -104,6 +105,7 @@ +
@@ -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('', { - abha_id: abhaId, - '': $('input[name=""]').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('', 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 = '
' + + 'ABHA ID Already Registered!
' + + 'Linked to: ' + (p.p_fname || 'Existing Patient') + ' (' + (p.p_code || '') + ')
' + + '' + + 'View Existing Patient Profile' + + '
'; + $('#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('', 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 = '
' + + 'Aadhaar Already Registered!
' + + 'Linked to: ' + (p.p_fname || 'Existing Patient') + ' (' + (p.p_code || '') + ')
' + + '' + + 'View Existing Patient Profile' + + '
'; + $('#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('', 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 = '
' + + 'ABHA ID Already Registered!
' + + 'Linked to: ' + (cp.p_fname || 'Existing Patient') + ' (' + (cp.p_code || '') + ')
' + + '' + + 'View Existing Patient Profile' + + '
'; + $('#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 }); diff --git a/app/Views/billing/search_adv_data.php b/app/Views/billing/search_adv_data.php index a378c3a..21f3cfc 100644 --- a/app/Views/billing/search_adv_data.php +++ b/app/Views/billing/search_adv_data.php @@ -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); ?> -
-
+
+ + + ALREADY REGISTERED + +
@@ -171,11 +222,27 @@ if (! empty($search_result) && is_array($search_result)) {
Refer By:
+ + +
+ ABHA Conflict: Already linked to this patient. Cannot register new person with same ABHA ID. +
+ + + +
+ Aadhaar Conflict: Already linked to this patient. Cannot register new person with same Aadhaar. +
+ +
Confidence: - Match: + + + ' : '' ?>Match: +