Implement ABDM bridge health record flows
- add facility QR support and OPD queue bridge fixes - support care-context push warnings for lab, radiology, IPD and remaining HI types - improve ABHA patient matching and diagnosis autocomplete performance - clean up webcam streams on page navigation
This commit is contained in:
parent
2bd52532a0
commit
5d6acdd480
|
|
@ -19,7 +19,7 @@ $routes->get('Medical', 'Medical::index');
|
|||
$routes->get('Medical/search_customer', 'Medical::search_customer');
|
||||
$routes->get('Medical/CounterSaleForm', 'Medical::counter_sale_form');
|
||||
$routes->post('Medical/CounterSaleCreate', 'Medical::counter_sale_create');
|
||||
$routes->match(['get', 'post'], 'Medical/search', 'Medical::search');
|
||||
$routes->match(['GET', 'POST'], 'Medical/search', 'Medical::search');
|
||||
$routes->get('Medical/get_drug', 'Medical::get_drug');
|
||||
$routes->get('Medical/get_batch/(:num)', 'Medical::get_batch/$1');
|
||||
$routes->get('Medical/check_invoice/(:num)/(:num)/(:num)', 'Medical::check_invoice/$1/$2/$3');
|
||||
|
|
@ -28,7 +28,7 @@ $routes->get('Medical/invoice_new/(:num)', 'Medical::invoice_new/$1');
|
|||
$routes->get('Medical/invoice_edit/(:num)', 'Medical::invoice_edit/$1');
|
||||
$routes->get('Medical/final_invoice/(:num)', 'Medical::final_invoice/$1');
|
||||
$routes->get('Medical/open_invoice_edit/(:num)', 'Medical::open_invoice_edit/$1');
|
||||
$routes->match(['get', 'post'], 'Medical/edit_invoice_edit/(:num)', 'Medical::edit_invoice_edit/$1');
|
||||
$routes->match(['GET', 'POST'], 'Medical/edit_invoice_edit/(:num)', 'Medical::edit_invoice_edit/$1');
|
||||
$routes->post('Medical/patient_info', 'Medical::patient_info');
|
||||
$routes->post('Medical/ipd_info', 'Medical::ipd_info');
|
||||
$routes->post('Medical/org_info', 'Medical::org_info');
|
||||
|
|
@ -46,7 +46,7 @@ $routes->post('Medical/remove_item', 'Medical::remove_item');
|
|||
$routes->post('Medical/update_item_qty', 'Medical::update_item_qty');
|
||||
$routes->post('Medical/add_remove_item', 'Medical::add_remove_item');
|
||||
$routes->post('Medical/go_final', 'Medical::go_final');
|
||||
$routes->match(['get', 'post'], 'Medical/getInvoiceTable', 'Medical::getInvoiceTable');
|
||||
$routes->match(['GET', 'POST'], 'Medical/getInvoiceTable', 'Medical::getInvoiceTable');
|
||||
$routes->get('Medical/invoice_print/(:num)', 'Medical::invoice_print/$1');
|
||||
$routes->get('Medical/invoice_print/(:num)/(:num)', 'Medical::invoice_print/$1/$2');
|
||||
$routes->get('Medical/Invoice_Med_Draft', 'Medical::Invoice_Med_Draft');
|
||||
|
|
@ -148,7 +148,7 @@ $routes->post('Medical/CreatePurchaseReturn', 'Medical::create_purchase_return')
|
|||
$routes->get('Medical/PurchaseReturnInvoiceEdit/(:num)', 'Medical::purchase_return_invoice_edit/$1');
|
||||
$routes->get('Medical/PurchaseReturn_invoice_item_list/(:num)', 'Medical::purchase_return_invoice_item_list/$1');
|
||||
$routes->get('Medical/Purchase_Invoice_product/(:num)', 'Medical::purchase_invoice_product/$1');
|
||||
$routes->match(['get', 'post'], 'Medical/Purchase_Invoice_old/(:num)', 'Medical::purchase_invoice_old/$1');
|
||||
$routes->match(['GET', 'POST'], 'Medical/Purchase_Invoice_old/(:num)', 'Medical::purchase_invoice_old/$1');
|
||||
$routes->post('Medical/remove_item_invoice/(:num)', 'Medical::purchase_return_remove_item_invoice/$1');
|
||||
$routes->get('Medical/print_purchase_return/(:num)', 'Medical::print_purchase_return/$1');
|
||||
$routes->get('Medical/store_stock', 'Medical::store_stock');
|
||||
|
|
@ -168,7 +168,7 @@ $routes->post('Medical_backpanel/CreatePurchaseReturn', 'Medical::create_purchas
|
|||
$routes->get('Medical_backpanel/PurchaseReturnInvoiceEdit/(:num)', 'Medical::purchase_return_invoice_edit/$1');
|
||||
$routes->get('Medical_backpanel/PurchaseReturn_invoice_item_list/(:num)', 'Medical::purchase_return_invoice_item_list/$1');
|
||||
$routes->get('Medical_backpanel/Purchase_Invoice_product/(:num)', 'Medical::purchase_invoice_product/$1');
|
||||
$routes->match(['get', 'post'], 'Medical_backpanel/Purchase_Invoice_old/(:num)', 'Medical::purchase_invoice_old/$1');
|
||||
$routes->match(['GET', 'POST'], 'Medical_backpanel/Purchase_Invoice_old/(:num)', 'Medical::purchase_invoice_old/$1');
|
||||
$routes->post('Medical_backpanel/add_remove_item', 'Medical::purchase_return_add_remove_item');
|
||||
$routes->post('Medical_backpanel/remove_item_invoice/(:num)', 'Medical::purchase_return_remove_item_invoice/$1');
|
||||
$routes->get('Medical_backpanel/get_drug', 'Medical::get_drug');
|
||||
|
|
@ -180,7 +180,7 @@ $routes->get('Medical_backpanel/Print_bill_on_uhid', 'Medical::print_bill_on_uhi
|
|||
$routes->get('Medical_backpanel/uhid_report/(:segment)/(:segment)', 'Medical::uhid_report/$1/$2');
|
||||
$routes->get('Medical_backpanel/SupplierAccount', 'Medical::supplier_account');
|
||||
$routes->get('Medical_backpanel/invoice_item_log', 'Medical::invoice_item_log');
|
||||
$routes->match(['get', 'post'], 'Medical_backpanel/invoice_item_log_data', 'Medical::invoice_item_log_data');
|
||||
$routes->match(['GET', 'POST'], 'Medical_backpanel/invoice_item_log_data', 'Medical::invoice_item_log_data');
|
||||
$routes->get('Medical_backpanel/stock_details', 'Medical::stock_details');
|
||||
$routes->get('Medical_backpanel/Expire_stock', 'Medical::expire_stock');
|
||||
$routes->get('Medical_backpanel/Expire_stock_pdf', 'Medical::expire_stock_pdf');
|
||||
|
|
@ -273,7 +273,7 @@ $routes->post('Medical/supplier_account_add_entry/(:num)', 'Medical::supplier_ac
|
|||
$routes->get('Medical/supplier_account_edit_entry/(:num)', 'Medical::supplier_account_edit_entry/$1');
|
||||
$routes->post('Medical/supplier_account_edit_entry/(:num)', 'Medical::supplier_account_edit_entry/$1');
|
||||
$routes->get('Medical/invoice_item_log', 'Medical::invoice_item_log');
|
||||
$routes->match(['get', 'post'], 'Medical/invoice_item_log_data', 'Medical::invoice_item_log_data');
|
||||
$routes->match(['GET', 'POST'], 'Medical/invoice_item_log_data', 'Medical::invoice_item_log_data');
|
||||
$routes->get('Medical/master', 'Medical::master');
|
||||
$routes->get('Medical/master_report/med-payment-edit', 'Payment_Medical::index');
|
||||
$routes->get('Medical/master_report/(:segment)', 'Medical::master_report/$1');
|
||||
|
|
@ -281,7 +281,7 @@ $routes->get('Medical/master_link/(:segment)', 'Medical::master_link/$1');
|
|||
|
||||
$routes->get('Payment_Medical', 'Payment_Medical::index');
|
||||
$routes->get('Payment_Medical/payment_log', 'Payment_Medical::payment_log');
|
||||
$routes->match(['get', 'post'], 'Payment_Medical/payment_log_data', 'Payment_Medical::payment_log_data');
|
||||
$routes->match(['GET', 'POST'], 'Payment_Medical/payment_log_data', 'Payment_Medical::payment_log_data');
|
||||
$routes->post('Payment_Medical/payment_record', 'Payment_Medical::payment_record');
|
||||
$routes->post('Payment_Medical/change_to_bank', 'Payment_Medical::change_to_bank');
|
||||
$routes->post('Payment_Medical/change_to_cash', 'Payment_Medical::change_to_cash');
|
||||
|
|
@ -337,7 +337,7 @@ service('auth')->routes($routes);
|
|||
// Hospital Stock Management (Storestock)
|
||||
$routes->get('Storestock', 'Storestock::index');
|
||||
$routes->get('Storestock/Indent_List', 'Storestock::indent_list');
|
||||
$routes->match(['get', 'post'], 'Storestock/getIndentTable', 'Storestock::getIndentTable');
|
||||
$routes->match(['GET', 'POST'], 'Storestock/getIndentTable', 'Storestock::getIndentTable');
|
||||
$routes->get('Storestock/new_indent', 'Storestock::new_indent');
|
||||
$routes->post('Storestock/Indent_create/(:num)', 'Storestock::indent_create/$1');
|
||||
$routes->get('Storestock/Indent_show/(:num)', 'Storestock::indent_show/$1');
|
||||
|
|
@ -348,11 +348,11 @@ $routes->get('Storestock/final_invoice/(:num)', 'Storestock::final_invoice/$1');
|
|||
$routes->get('Storestock/print_single_indent/(:num)', 'Storestock::print_single_indent/$1');
|
||||
$routes->get('Storestock/store_stock', 'Storestock::store_stock');
|
||||
$routes->post('Storestock/store_Stock_result', 'Storestock::store_stock_result');
|
||||
$routes->match(['get', 'post'], 'Storestock/get_product_stock/(:num)', 'Storestock::get_product_stock/$1');
|
||||
$routes->match(['GET', 'POST'], 'Storestock/get_product_stock/(:num)', 'Storestock::get_product_stock/$1');
|
||||
$routes->get('Storestock/Report_2', 'Storestock::report_2');
|
||||
$routes->get('Storestock/main_store', 'Storestock::main_store');
|
||||
$routes->get('Storestock/Purchase', 'Storestock::Purchase');
|
||||
$routes->match(['get', 'post'], 'Storestock/PurchaseInvoice', 'Storestock::PurchaseInvoice');
|
||||
$routes->match(['GET', 'POST'], 'Storestock/PurchaseInvoice', 'Storestock::PurchaseInvoice');
|
||||
$routes->get('Storestock/PurchaseNew', 'Storestock::PurchaseNew');
|
||||
$routes->post('Storestock/CreatePurchase', 'Storestock::CreatePurchase');
|
||||
$routes->get('Storestock/PurchaseMasterEdit/(:num)', 'Storestock::PurchaseMasterEdit/$1');
|
||||
|
|
@ -363,7 +363,7 @@ $routes->post('Storestock/purchase_invoice_delete/(:num)', 'Storestock::purchase
|
|||
$routes->get('Storestock/print_purchase/(:num)', 'Storestock::print_purchase/$1');
|
||||
$routes->get('Storestock/Purchase_return', 'Storestock::Purchase_return');
|
||||
$routes->get('Storestock/PurchaseReturnNew', 'Storestock::PurchaseReturnNew');
|
||||
$routes->match(['get', 'post'], 'Storestock/PurchaseReturnInvoice', 'Storestock::PurchaseReturnInvoice');
|
||||
$routes->match(['GET', 'POST'], 'Storestock/PurchaseReturnInvoice', 'Storestock::PurchaseReturnInvoice');
|
||||
$routes->get('Storestock/PurchaseReturnInvoiceEdit/(:num)', 'Storestock::PurchaseReturnInvoiceEdit/$1');
|
||||
$routes->get('Storestock/PurchaseReturn_invoice_item_list/(:num)', 'Storestock::PurchaseReturn_invoice_item_list/$1');
|
||||
$routes->post('Storestock/create_purchase_return', 'Storestock::create_purchase_return');
|
||||
|
|
@ -391,8 +391,8 @@ $routes->get('Ipd_discharge/debug_ipd_fields/(:num)', 'Ipd_discharge::debug_ipd_
|
|||
$routes->get('Ipd_discharge/debug_discharge_html/(:num)', 'Ipd_discharge::debug_discharge_html/$1');
|
||||
$routes->get('Ipd_discharge/placeholder_preview', 'Ipd_discharge::placeholder_preview');
|
||||
$routes->get('Ipd_discharge/placeholder_preview/(:num)', 'Ipd_discharge::placeholder_preview/$1');
|
||||
$routes->match(['get', 'post'], 'Ipd_discharge/ipd_select/(:num)', 'Ipd_discharge::ipd_select/$1');
|
||||
$routes->match(['get', 'post'], 'Ipd_discharge/ipd_select/(:num)/(:num)', 'Ipd_discharge::ipd_select/$1/$2');
|
||||
$routes->match(['GET', 'POST'], 'Ipd_discharge/ipd_select/(:num)', 'Ipd_discharge::ipd_select/$1');
|
||||
$routes->match(['GET', 'POST'], 'Ipd_discharge/ipd_select/(:num)/(:num)', 'Ipd_discharge::ipd_select/$1/$2');
|
||||
$routes->get('Ipd_discharge/section_past_data', 'Ipd_discharge::section_past_data');
|
||||
$routes->get('Ipd_discharge/section_template_list', 'Ipd_discharge::section_template_list');
|
||||
$routes->post('Ipd_discharge/section_template_save', 'Ipd_discharge::section_template_save');
|
||||
|
|
@ -410,7 +410,7 @@ $routes->post('Ipd_discharge/update_complaint_field', 'Ipd_discharge::update_com
|
|||
$routes->get('Ipd_discharge/dietary_master_list', 'Ipd_discharge::dietary_master_list');
|
||||
$routes->post('Ipd_discharge/dietary_master_save', 'Ipd_discharge::dietary_master_save');
|
||||
$routes->post('Ipd_discharge/dietary_master_delete', 'Ipd_discharge::dietary_master_delete');
|
||||
$routes->match(['get', 'post'], 'Ipd_discharge/preview_discharge_report/(:num)', 'Ipd_discharge::preview_discharge_report/$1');
|
||||
$routes->match(['GET', 'POST'], 'Ipd_discharge/preview_discharge_report/(:num)', 'Ipd_discharge::preview_discharge_report/$1');
|
||||
$routes->get('Ipd_discharge/show_discharge/(:num)', 'Ipd_discharge::show_discharge/$1');
|
||||
$routes->get('Ipd_discharge/show_discharge/(:num)/(:num)', 'Ipd_discharge::show_discharge/$1/$2');
|
||||
$routes->get('Ipd_discharge/show_file3/(:num)', 'Ipd_discharge::show_file3/$1');
|
||||
|
|
@ -502,7 +502,7 @@ $routes->group('billing', function($routes) {
|
|||
$routes->get('patient/abdm_content_auto_flow/(:num)', 'Patient::abdm_content_auto_flow/$1');
|
||||
$routes->get('patient/abdm_timeline/(:num)', 'Patient::abdm_timeline/$1');
|
||||
$routes->post('patient/save_profile_image/(:num)', 'Patient::save_profile_image/$1');
|
||||
$routes->match(['get', 'post'], 'patient/patient_file_upload/(:num)', 'Patient::patient_file_upload/$1');
|
||||
$routes->match(['GET', 'POST'], 'patient/patient_file_upload/(:num)', 'Patient::patient_file_upload/$1');
|
||||
$routes->get('patient/city', 'Patient::city');
|
||||
$routes->get('patient/district_list', 'Patient::district_list');
|
||||
$routes->get('patient/get_name', 'Patient::get_name');
|
||||
|
|
@ -580,24 +580,24 @@ $routes->get('Opd/get_appointment_digest/(:num)', 'Opd::get_appointment_digest/$
|
|||
$routes->get('Opd/get_appointment_digest/(:num)/(:segment)', 'Opd::get_appointment_digest/$1/$2');
|
||||
$routes->get('Opd/get_appointment_live_rows/(:num)', 'Opd::get_appointment_live_rows/$1');
|
||||
$routes->get('Opd/get_appointment_live_rows/(:num)/(:segment)', 'Opd::get_appointment_live_rows/$1/$2');
|
||||
$routes->match(['get', 'post'], 'Opd/opd_status/(:num)/(:num)', 'Opd::opd_status/$1/$2');
|
||||
$routes->match(['GET', 'POST'], 'Opd/opd_status/(:num)/(:num)', 'Opd::opd_status/$1/$2');
|
||||
$routes->post('Opd/showfee', 'Opd::showfee');
|
||||
$routes->post('Opd/confirm_opd', 'Opd::confirm_opd');
|
||||
$routes->post('Opd/confirm_payment', 'Opd::confirm_payment');
|
||||
$routes->post('Opd/update_doc_date/(:num)', 'Opd::update_doc_date/$1');
|
||||
$routes->post('Opd/opd_discount_update/(:num)', 'Opd::opd_discount_update/$1');
|
||||
$routes->post('Opd/opd_cancel/(:num)', 'Opd::opd_cancel/$1');
|
||||
$routes->match(['get', 'post'], 'Opd/opd_load_doc/(:num)', 'Opd::opd_load_doc/$1');
|
||||
$routes->match(['get', 'post'], 'Opd/opd_file_list/(:num)', 'Opd::opd_file_list/$1');
|
||||
$routes->match(['get', 'post'], 'Opd/opd_file_last_list', 'Opd::opd_file_last_list');
|
||||
$routes->match(['get', 'post'], 'Opd/opd_file_last_list/(:num)', 'Opd::opd_file_last_list/$1');
|
||||
$routes->match(['get', 'post'], 'Opd/save_image/(:num)', 'Opd::save_image/$1');
|
||||
$routes->match(['GET', 'POST'], 'Opd/opd_load_doc/(:num)', 'Opd::opd_load_doc/$1');
|
||||
$routes->match(['GET', 'POST'], 'Opd/opd_file_list/(:num)', 'Opd::opd_file_list/$1');
|
||||
$routes->match(['GET', 'POST'], 'Opd/opd_file_last_list', 'Opd::opd_file_last_list');
|
||||
$routes->match(['GET', 'POST'], 'Opd/opd_file_last_list/(:num)', 'Opd::opd_file_last_list/$1');
|
||||
$routes->match(['GET', 'POST'], 'Opd/save_image/(:num)', 'Opd::save_image/$1');
|
||||
$routes->post('Opd/scan_ai_process_file', 'Opd::scan_ai_process_file');
|
||||
// Legacy CI3 uploads fallback: serves files from project-root uploads/ when missing from public/uploads/
|
||||
$routes->get('uploads/(:any)', 'Opd::serve_legacy_upload/$1');
|
||||
$routes->post('Opd/scan_ai_assist', 'Opd::scan_ai_assist');
|
||||
$routes->match(['get', 'post'], 'Opd/opd_file_hide/(:num)', 'Opd::opd_file_hide/$1');
|
||||
$routes->match(['get', 'post'], 'Opd/opd_file_delete/(:num)', 'Opd::opd_file_delete/$1');
|
||||
$routes->match(['GET', 'POST'], 'Opd/opd_file_hide/(:num)', 'Opd::opd_file_hide/$1');
|
||||
$routes->match(['GET', 'POST'], 'Opd/opd_file_delete/(:num)', 'Opd::opd_file_delete/$1');
|
||||
$routes->get('Opd/invoice/(:num)', 'Opd::invoice/$1');
|
||||
$routes->get('Opd/invoice_print/(:num)', 'Opd::invoice_print/$1');
|
||||
$routes->get('Opd/opd_lettre_print/(:num)', 'Opd::opd_lettre_print/$1');
|
||||
|
|
@ -608,7 +608,7 @@ $routes->get('Opd/print_template_builder', 'Opd::print_template_builder');
|
|||
$routes->post('Opd/print_template_save', 'Opd::print_template_save');
|
||||
$routes->post('Opd/print_template_rename', 'Opd::print_template_rename');
|
||||
$routes->post('Opd/print_template_delete', 'Opd::print_template_delete');
|
||||
$routes->match(['get', 'post'], 'Opd/paper_print_settings', 'Opd::paper_print_settings');
|
||||
$routes->match(['GET', 'POST'], 'Opd/paper_print_settings', 'Opd::paper_print_settings');
|
||||
|
||||
$routes->get('opd_print/invoice_print_pdf/(:num)', 'Opd::invoice_print_pdf/$1');
|
||||
$routes->get('opd_print/opd_PDF_print/(:num)', 'Opd::opd_pdf_print/$1');
|
||||
|
|
@ -616,7 +616,7 @@ $routes->get('opd_print/opd_Cont_print/(:num)', 'Opd::opd_cont_print/$1');
|
|||
$routes->get('opd_print/opd_blank_print/(:num)', 'Opd::opd_blank_print/$1');
|
||||
|
||||
$routes->get('Opd_prescription/Prescription/(:num)', 'Opd_prescription::Prescription/$1');
|
||||
$routes->match(['get', 'post'], 'Opd_prescription/create_opd_queue/(:num)', 'Opd_prescription::create_opd_queue/$1');
|
||||
$routes->match(['GET', 'POST'], 'Opd_prescription/create_opd_queue/(:num)', 'Opd_prescription::create_opd_queue/$1');
|
||||
$routes->get('Opd_prescription/opd_prescription_print/(:num)/(:num)', 'Opd_prescription::opd_prescription_print/$1/$2');
|
||||
$routes->get('Opd_prescription/opd_prescription_print/(:num)/(:num)/(:num)', 'Opd_prescription::opd_prescription_print/$1/$2/$3');
|
||||
$routes->post('Opd_prescription/opd_prescription_save', 'Opd_prescription::opd_prescription_save');
|
||||
|
|
@ -685,6 +685,9 @@ $routes->get('AbdmGateway/diagnosis_report_fhir_preview', 'AbdmGateway::diagnosi
|
|||
$routes->get('AbdmGateway/ipd_discharge_fhir_preview', 'AbdmGateway::ipdDischargeFhirPreview', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/immunization_fhir_preview', 'AbdmGateway::immunizationFhirPreview', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_immunization_bundle', 'AbdmGateway::shareImmunizationBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_wellness_bundle', 'AbdmGateway::shareWellnessBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_health_document_bundle', 'AbdmGateway::shareHealthDocumentBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_invoice_bundle', 'AbdmGateway::shareInvoiceBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('Immunization', 'Immunization::index', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->get('Immunization/schedule_master', 'Immunization::scheduleMaster', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->post('Immunization/sync_uip_master', 'Immunization::syncUipMaster', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
|
|
@ -723,6 +726,7 @@ $routes->post('abha/create/verify_comm_otp', 'Abha::verifyCommOtp');
|
|||
$routes->post('abha/create/address', 'Abha::address');
|
||||
$routes->get('abha/card/(:segment)', 'Abha::card/$1');
|
||||
$routes->post('abha/register/validate', 'Abha::validateAbha');
|
||||
$routes->get('abha/register/facility_qr', 'Abha::facilityQr');
|
||||
$routes->get('AbdmTaskBoard', 'AbdmTaskBoard::index', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmTaskBoard/list', 'AbdmTaskBoard::list', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmTaskBoard/perform_action', 'AbdmTaskBoard::performAction', ['filter' => $abdmPermFilter]);
|
||||
|
|
@ -959,15 +963,15 @@ $routes->post('Lab_Admin/pathology_master_apply_panel', 'Setting\Template::patho
|
|||
$routes->post('Lab_Admin/pathology_master_add_component', 'Setting\Template::pathologyMasterAddComponent');
|
||||
$routes->get('Lab_Admin/pathology_bridge_debug', 'Setting\Template::pathologyBridgeDebug');
|
||||
|
||||
$routes->match(['get', 'post'], 'setting/template/discharge_templates', 'Setting\Template::discharge_templates');
|
||||
$routes->match(['GET', 'POST'], 'setting/template/discharge_templates', 'Setting\Template::discharge_templates');
|
||||
$routes->post('setting/template/discharge_templates_seed', 'Setting\Template::discharge_templates_seed');
|
||||
$routes->get('setting/template/discharge_template_get/(:num)', 'Setting\Template::discharge_template_get/$1');
|
||||
$routes->match(['get', 'post'], 'setting/template/discharge_templates/delete/(:num)', 'Setting\Template::discharge_template_delete/$1');
|
||||
$routes->match(['get', 'post'], 'setting/template/ipd_document_templates', 'Setting\Template::ipd_document_templates');
|
||||
$routes->match(['GET', 'POST'], 'setting/template/discharge_templates/delete/(:num)', 'Setting\Template::discharge_template_delete/$1');
|
||||
$routes->match(['GET', 'POST'], 'setting/template/ipd_document_templates', 'Setting\Template::ipd_document_templates');
|
||||
$routes->get('setting/template/ipd_document_templates/delete/(:num)', 'Setting\Template::ipd_document_template_delete/$1');
|
||||
$routes->match(['get', 'post'], 'setting/template/diagnosis_print_settings', 'Setting\Template::diagnosis_print_settings');
|
||||
$routes->match(['get', 'post'], 'setting/template/diagnosis_print_settings/(:num)', 'Setting\Template::diagnosis_print_settings/$1');
|
||||
$routes->match(['get', 'post'], 'setting/template/document_print_settings', 'Setting\Template::document_print_settings');
|
||||
$routes->match(['GET', 'POST'], 'setting/template/diagnosis_print_settings', 'Setting\Template::diagnosis_print_settings');
|
||||
$routes->match(['GET', 'POST'], 'setting/template/diagnosis_print_settings/(:num)', 'Setting\Template::diagnosis_print_settings/$1');
|
||||
$routes->match(['GET', 'POST'], 'setting/template/document_print_settings', 'Setting\Template::document_print_settings');
|
||||
|
||||
$routes->get('Opdcase/addopd/(:num)', 'Opdcase::addopd/$1');
|
||||
$routes->post('Opdcase/showfee', 'Opdcase::showfee');
|
||||
|
|
@ -1087,7 +1091,7 @@ $routes->post('diagnosis/confirm-report-xray/(:num)', 'Diagnosis::confirmReportX
|
|||
// Editor JS compatibility aliases
|
||||
$routes->post('diagnosis/update-report/(:num)', 'Diagnosis::finalUpdateXray/$1');
|
||||
$routes->post('diagnosis/report-verify/(:num)', 'Diagnosis::confirmReportXray/$1');
|
||||
$routes->match(['get', 'post'], 'diagnosis/get-template-xray/(:num)', 'Diagnosis::getTemplateXray/$1');
|
||||
$routes->match(['GET', 'POST'], 'diagnosis/get-template-xray/(:num)', 'Diagnosis::getTemplateXray/$1');
|
||||
$routes->post('diagnosis/remove-test/(:num)', 'Diagnosis::removeTest/$1');
|
||||
$routes->post('diagnosis/update-combine-report', 'Diagnosis::updateCombineReport');
|
||||
$routes->post('diagnosis/report-compile/(:num)/(:num)', 'Diagnosis::reportCompile/$1/$2');
|
||||
|
|
@ -1124,11 +1128,11 @@ $routes->get('Lab_Report/report_file_list/(:num)/(:num)/(:num)', 'Diagnosis::rep
|
|||
$routes->get('Lab_Report/imaging_upload_gallery/(:num)/(:num)/(:num)', 'Diagnosis::imagingUploadGallery/$1/$2/$3');
|
||||
$routes->post('Lab_Report/imaging_ai_diagnosis/(:num)', 'Diagnosis::imagingAiDiagnosis/$1');
|
||||
$routes->get('Lab_Report/select_lab_radiology/(:num)/(:num)', 'Diagnosis::selectLabRadiology/$1/$2');
|
||||
$routes->match(['get', 'post'], 'Lab_Report/create_report_xray/(:num)', 'Diagnosis::createReportXray/$1');
|
||||
$routes->match(['GET', 'POST'], 'Lab_Report/create_report_xray/(:num)', 'Diagnosis::createReportXray/$1');
|
||||
$routes->get('Lab_Report/show_report_final/(:num)', 'Diagnosis::showReportFinalXray/$1');
|
||||
$routes->match(['get', 'post'], 'Lab_Report/get_template_xray/(:num)', 'Diagnosis::getTemplateXray/$1');
|
||||
$routes->match(['get', 'post'], 'Lab_Report/Final_Update_xray/(:num)', 'Diagnosis::finalUpdateXray/$1');
|
||||
$routes->match(['get', 'post'], 'Lab_Report/confirm_report_xray/(:num)', 'Diagnosis::confirmReportXray/$1');
|
||||
$routes->match(['GET', 'POST'], 'Lab_Report/get_template_xray/(:num)', 'Diagnosis::getTemplateXray/$1');
|
||||
$routes->match(['GET', 'POST'], 'Lab_Report/Final_Update_xray/(:num)', 'Diagnosis::finalUpdateXray/$1');
|
||||
$routes->match(['GET', 'POST'], 'Lab_Report/confirm_report_xray/(:num)', 'Diagnosis::confirmReportXray/$1');
|
||||
|
||||
// Legacy URL compatibility
|
||||
$routes->get('Ocasemaster/newcase/(:num)/(:num)', 'Billing\\CaseMaster::newcase/$1/$2');
|
||||
|
|
@ -1310,7 +1314,7 @@ $routes->group('item', static function($routes) {
|
|||
$routes->get('search-adv/(:num)', 'Item::searchAdv/$1');
|
||||
$routes->get('search-print/(:num)', 'Item::searchPrint/$1');
|
||||
$routes->get('export-excel/(:num)', 'Item::exportExcel/$1');
|
||||
$routes->match(['get', 'post'], 'item-record/(:num)', 'Item::itemRecord/$1');
|
||||
$routes->match(['GET', 'POST'], 'item-record/(:num)', 'Item::itemRecord/$1');
|
||||
$routes->get('itemtype-record/(:num)', 'Item::itemTypeRecord/$1');
|
||||
$routes->get('add', 'Item::addRecord');
|
||||
$routes->get('add-type', 'Item::addItemTypeRecord');
|
||||
|
|
@ -1328,7 +1332,7 @@ $routes->group('item-ipd', static function($routes) {
|
|||
$routes->get('search-adv/(:num)', 'ItemIpd::searchAdv/$1');
|
||||
$routes->get('search-print/(:num)', 'ItemIpd::searchPrint/$1');
|
||||
$routes->get('export-excel/(:num)', 'ItemIpd::exportExcel/$1');
|
||||
$routes->match(['get', 'post'], 'item-record/(:num)', 'ItemIpd::itemRecord/$1');
|
||||
$routes->match(['GET', 'POST'], 'item-record/(:num)', 'ItemIpd::itemRecord/$1');
|
||||
$routes->get('itemtype-record/(:num)', 'ItemIpd::itemTypeRecord/$1');
|
||||
$routes->get('add', 'ItemIpd::addRecord');
|
||||
$routes->get('add-type', 'ItemIpd::addItemTypeRecord');
|
||||
|
|
@ -1348,7 +1352,7 @@ $routes->group('package', static function($routes) {
|
|||
$routes->get('search-adv/(:num)', 'Package::searchAdv/$1');
|
||||
$routes->get('search-print/(:num)', 'Package::searchPrint/$1');
|
||||
$routes->get('export-excel/(:num)', 'Package::exportExcel/$1');
|
||||
$routes->match(['get', 'post'], 'item-record/(:num)', 'Package::itemRecord/$1');
|
||||
$routes->match(['GET', 'POST'], 'item-record/(:num)', 'Package::itemRecord/$1');
|
||||
$routes->get('itemtype-record/(:num)', 'Package::itemTypeRecord/$1');
|
||||
$routes->get('add', 'Package::addRecord');
|
||||
$routes->get('add-type', 'Package::addItemTypeRecord');
|
||||
|
|
@ -1366,7 +1370,7 @@ $routes->group('Package', static function($routes) {
|
|||
$routes->get('search_adv/(:num)', 'Package::searchAdv/$1');
|
||||
$routes->get('search_print/(:num)', 'Package::searchPrint/$1');
|
||||
$routes->get('export-excel/(:num)', 'Package::exportExcel/$1');
|
||||
$routes->match(['get', 'post'], 'item_record/(:num)', 'Package::itemRecord/$1');
|
||||
$routes->match(['GET', 'POST'], 'item_record/(:num)', 'Package::itemRecord/$1');
|
||||
$routes->get('itemtype_record/(:num)', 'Package::itemTypeRecord/$1');
|
||||
$routes->get('AddRecord', 'Package::addRecord');
|
||||
$routes->get('AddItemTypeRecord', 'Package::addItemTypeRecord');
|
||||
|
|
@ -1384,7 +1388,7 @@ $routes->group('IPD_Package', static function($routes) {
|
|||
$routes->get('search_adv/(:num)', 'Package::searchAdv/$1');
|
||||
$routes->get('search_print/(:num)', 'Package::searchPrint/$1');
|
||||
$routes->get('export-excel/(:num)', 'Package::exportExcel/$1');
|
||||
$routes->match(['get', 'post'], 'item_record/(:num)', 'Package::itemRecord/$1');
|
||||
$routes->match(['GET', 'POST'], 'item_record/(:num)', 'Package::itemRecord/$1');
|
||||
$routes->get('itemtype_record/(:num)', 'Package::itemTypeRecord/$1');
|
||||
$routes->get('AddRecord', 'Package::addRecord');
|
||||
$routes->get('AddItemTypeRecord', 'Package::addItemTypeRecord');
|
||||
|
|
@ -1402,7 +1406,7 @@ $routes->group('Item_IPD', static function($routes) {
|
|||
$routes->get('search_adv/(:num)', 'ItemIpd::searchAdv/$1');
|
||||
$routes->get('search_print/(:num)', 'ItemIpd::searchPrint/$1');
|
||||
$routes->get('export-excel/(:num)', 'ItemIpd::exportExcel/$1');
|
||||
$routes->match(['get', 'post'], 'item_record/(:num)', 'ItemIpd::itemRecord/$1');
|
||||
$routes->match(['GET', 'POST'], 'item_record/(:num)', 'ItemIpd::itemRecord/$1');
|
||||
$routes->get('itemtype_record/(:num)', 'ItemIpd::itemTypeRecord/$1');
|
||||
$routes->get('AddRecord', 'ItemIpd::addRecord');
|
||||
$routes->get('AddItemTypeRecord', 'ItemIpd::addItemTypeRecord');
|
||||
|
|
|
|||
|
|
@ -999,6 +999,8 @@ class AbdmGateway extends BaseController
|
|||
'phone' => trim((string) ($this->request->getPost('phone') ?? '')),
|
||||
'gender' => strtoupper(trim((string) ($this->request->getPost('gender') ?? ''))),
|
||||
'dob' => trim((string) ($this->request->getPost('dob') ?? '')),
|
||||
'age' => trim((string) ($this->request->getPost('age') ?? $this->request->getPost('patient_age') ?? '')),
|
||||
'birth_year' => trim((string) ($this->request->getPost('birth_year') ?? $this->request->getPost('birthYear') ?? $this->request->getPost('year_of_birth') ?? '')),
|
||||
];
|
||||
|
||||
$db = $this->db;
|
||||
|
|
@ -1542,15 +1544,15 @@ class AbdmGateway extends BaseController
|
|||
|
||||
$hasAbha = $abhaId !== '';
|
||||
$consent = null;
|
||||
$consentWarning = '';
|
||||
if ($hasAbha) {
|
||||
$consent = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
||||
if ($consent === null) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => 'No active consent found. Share blocked due to expiry/not-approved consent.',
|
||||
]);
|
||||
$consentWarning = 'No active consent found. Proceeding with care-context push only.';
|
||||
$this->logMissingConsentShareWarning('ipd', $ipdId, $patientId, $abhaId, $consentHandle, 'abdm.ipd.discharge.share.consent_missing', $consentWarning);
|
||||
}
|
||||
}
|
||||
$effectiveConsent = (string) ($consent['consent_handle'] ?? $consentHandle);
|
||||
|
||||
$fhirPayload = $this->buildIpdDischargeGatewayPayload($ipdId, $patientId, $abhaId);
|
||||
if ($fhirPayload === null) {
|
||||
|
|
@ -1575,7 +1577,7 @@ class AbdmGateway extends BaseController
|
|||
'entity_id' => (string) $ipdId,
|
||||
'fhir_bundle' => $bundleJson,
|
||||
'care_context_reference' => $ccRef,
|
||||
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
||||
'consent_handle' => $effectiveConsent,
|
||||
]);
|
||||
|
||||
if (! $hasAbha) {
|
||||
|
|
@ -1642,8 +1644,8 @@ class AbdmGateway extends BaseController
|
|||
'entity_id' => (string) $ipdId,
|
||||
'abha_id' => $abhaId,
|
||||
'patient_id' => $patientId,
|
||||
'request' => ['ipd_id' => $ipdId, 'hi_type' => 'DischargeSummaryRecord', 'consent_handle' => (string) ($consent['consent_handle'] ?? '')],
|
||||
'response' => ['queue_id' => $queueId],
|
||||
'request' => ['ipd_id' => $ipdId, 'hi_type' => 'DischargeSummaryRecord', 'consent_handle' => $effectiveConsent],
|
||||
'response' => ['queue_id' => $queueId, 'warning' => $consentWarning],
|
||||
'outcome' => $connectorError === null ? 'success' : 'failure',
|
||||
'error_message' => (string) ($connectorError ?? ''),
|
||||
]);
|
||||
|
|
@ -1651,8 +1653,10 @@ class AbdmGateway extends BaseController
|
|||
return $this->response->setJSON([
|
||||
'ok' => $connectorError === null ? 1 : 0,
|
||||
'queue_id' => $queueId,
|
||||
'consent_handle' => (string) ($consent['consent_handle'] ?? ''),
|
||||
'consent_handle' => $effectiveConsent,
|
||||
'status' => $connectorError === null ? 'queued' : 'failed',
|
||||
'message' => $connectorError === null && $consentWarning !== '' ? 'Record pushed to gateway without active consent. Recheck link status in M2 ABDM Gateway.' : null,
|
||||
'warning' => $consentWarning !== '' ? $consentWarning : null,
|
||||
'error' => $connectorError,
|
||||
]);
|
||||
}
|
||||
|
|
@ -1685,13 +1689,12 @@ class AbdmGateway extends BaseController
|
|||
|
||||
$hasAbha = $abhaId !== '';
|
||||
$consentRecord = null;
|
||||
$consentWarning = '';
|
||||
if ($hasAbha) {
|
||||
$consentRecord = $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle);
|
||||
if ($consentRecord === null) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'No active consent found. Share blocked due to expiry/not-approved consent.',
|
||||
]);
|
||||
$consentWarning = 'No active consent found. Proceeding with care-context push only.';
|
||||
$this->logMissingConsentShareWarning('lab', $labReqId, $patientId, $abhaId, $consentHandle, 'abdm.diagnosis.report.share.consent_missing', $consentWarning);
|
||||
}
|
||||
}
|
||||
$effectiveConsent = (string) ($consentRecord['consent_handle'] ?? $consentHandle);
|
||||
|
|
@ -1875,7 +1878,7 @@ class AbdmGateway extends BaseController
|
|||
'abha_id' => $abhaId,
|
||||
'patient_id' => $patientId,
|
||||
'request' => ['lab_req_id' => $labReqId, 'hi_type' => 'DiagnosticReportRecord', 'consent_handle' => $effectiveConsent],
|
||||
'response' => ['queue_id' => $queueId],
|
||||
'response' => ['queue_id' => $queueId, 'warning' => $consentWarning],
|
||||
'outcome' => $connectorError === null ? 'success' : 'failure',
|
||||
'error_message' => (string) ($connectorError ?? ''),
|
||||
]);
|
||||
|
|
@ -1885,6 +1888,8 @@ class AbdmGateway extends BaseController
|
|||
'queue_id' => $queueId,
|
||||
'consent_handle' => $effectiveConsent,
|
||||
'status' => $connectorError === null ? 'queued' : 'failed',
|
||||
'message' => $connectorError === null && $consentWarning !== '' ? 'Record pushed to gateway without active consent. Recheck link status in M2 ABDM Gateway.' : null,
|
||||
'warning' => $consentWarning !== '' ? $consentWarning : null,
|
||||
'error' => $connectorError,
|
||||
]);
|
||||
}
|
||||
|
|
@ -2218,6 +2223,74 @@ class AbdmGateway extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
public function shareWellnessBundle()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$opdId = (int) $this->request->getPost('opd_id');
|
||||
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? $this->request->getPost('abha_address') ?? ''));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']);
|
||||
}
|
||||
|
||||
$payload = $this->buildWellnessRecordPayload($patientId, $opdId, $abhaId);
|
||||
if ($payload === null) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'No wellness/vital data found for this patient']);
|
||||
}
|
||||
|
||||
return $this->pushAdditionalHiRecord($payload, $patientId, $abhaId, $consentHandle);
|
||||
}
|
||||
|
||||
public function shareHealthDocumentBundle()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? $this->request->getPost('abha_address') ?? ''));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
|
||||
if ($patientId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']);
|
||||
}
|
||||
|
||||
$payload = $this->buildHealthDocumentRecordPayload($patientId, $abhaId);
|
||||
if ($payload === null) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'document_title with document_text/document_base64/file_path is required']);
|
||||
}
|
||||
|
||||
return $this->pushAdditionalHiRecord($payload, $patientId, $abhaId, $consentHandle);
|
||||
}
|
||||
|
||||
public function shareInvoiceBundle()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
$invoiceId = (int) $this->request->getPost('invoice_id');
|
||||
$patientId = (int) $this->request->getPost('patient_id');
|
||||
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? $this->request->getPost('abha_address') ?? ''));
|
||||
$consentHandle = trim((string) $this->request->getPost('consent_handle'));
|
||||
|
||||
if ($invoiceId <= 0) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'invoice_id is required']);
|
||||
}
|
||||
|
||||
$payload = $this->buildInvoiceRecordPayload($invoiceId, $patientId, $abhaId);
|
||||
if ($payload === null) {
|
||||
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Invoice not found or patient could not be resolved']);
|
||||
}
|
||||
|
||||
return $this->pushAdditionalHiRecord($payload, (int) $payload['patient_id'], $abhaId, $consentHandle);
|
||||
}
|
||||
|
||||
public function ipdDischargeFhirPreview()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
|
|
@ -3911,6 +3984,16 @@ class AbdmGateway extends BaseController
|
|||
$identity['dob'] = $dob;
|
||||
}
|
||||
|
||||
$age = preg_replace('/\D/', '', (string) ($this->findFirstByKeys($payload, ['age', 'patient_age', 'patientAge']) ?? ''));
|
||||
if ($age !== '' && preg_match('/^\d{1,3}$/', $age)) {
|
||||
$identity['age'] = $age;
|
||||
}
|
||||
|
||||
$birthYear = preg_replace('/\D/', '', (string) ($this->findFirstByKeys($payload, ['birth_year', 'birthYear', 'year_of_birth', 'yearOfBirth']) ?? ''));
|
||||
if ($birthYear !== '' && preg_match('/^(19|20)\d{2}$/', $birthYear)) {
|
||||
$identity['birth_year'] = $birthYear;
|
||||
}
|
||||
|
||||
return $identity;
|
||||
}
|
||||
|
||||
|
|
@ -3982,7 +4065,10 @@ class AbdmGateway extends BaseController
|
|||
$genderDb = ($rawGender === 'F' || $rawGender === '2') ? 2 : ($rawGender !== '' ? 1 : null);
|
||||
$dob = trim((string) ($identity['dob'] ?? ''));
|
||||
$dobValid = preg_match('/^\d{4}-\d{2}-\d{2}$/', $dob) ? $dob : '';
|
||||
$yearOnly = $dobValid !== '' ? (int) substr($dobValid, 0, 4) : 0;
|
||||
$birthYear = trim((string) ($identity['birth_year'] ?? ''));
|
||||
$yearOnly = $dobValid !== '' ? (int) substr($dobValid, 0, 4) : (preg_match('/^(19|20)\d{2}$/', $birthYear) ? (int) $birthYear : 0);
|
||||
$age = trim((string) ($identity['age'] ?? ''));
|
||||
$ageYears = preg_match('/^\d{1,3}$/', $age) ? (int) $age : null;
|
||||
|
||||
if ($nameUpper !== '' && $dobValid !== '') {
|
||||
// Name + exact DOB [+ gender] — high confidence (score 3)
|
||||
|
|
@ -4014,6 +4100,13 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
foreach ($this->findFuzzyDemographicMatches($this->db, $select, $nameUpper, $genderDb, $dobValid, $yearOnly, $ageYears) as $row) {
|
||||
$reason = (string) ($row['_match_reason'] ?? 'Similar Name + Demographics');
|
||||
$score = (int) ($row['_match_score'] ?? 2);
|
||||
unset($row['_match_reason'], $row['_match_score']);
|
||||
$append([$row], $reason, $score);
|
||||
}
|
||||
|
||||
// Phone match — supplementary only (score 1), ambiguous for families
|
||||
if (($identity['phone'] ?? '') !== '') {
|
||||
$rows = $this->db->table('patient_master')->select($select)->where('mphone1', $identity['phone'])->limit(10)->get()->getResultArray();
|
||||
|
|
@ -4041,6 +4134,96 @@ class AbdmGateway extends BaseController
|
|||
return $result;
|
||||
}
|
||||
|
||||
private function findFuzzyDemographicMatches(
|
||||
\CodeIgniter\Database\BaseConnection $db,
|
||||
string $select,
|
||||
string $nameUpper,
|
||||
?int $genderDb,
|
||||
string $dobValid,
|
||||
int $yearOnly,
|
||||
?int $ageYears
|
||||
): array {
|
||||
if ($nameUpper === '' || strlen($this->normalizeMatchName($nameUpper)) < 4) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fields = $db->getFieldNames('patient_master') ?? [];
|
||||
$queries = [];
|
||||
|
||||
if ($dobValid !== '') {
|
||||
$queries[] = ['reason' => $genderDb !== null ? 'Similar Name + DOB + Gender' : 'Similar Name + DOB', 'score' => 3, 'dob' => $dobValid];
|
||||
}
|
||||
if ($yearOnly > 0) {
|
||||
$queries[] = ['reason' => $genderDb !== null ? 'Similar Name + Year of Birth + Gender' : 'Similar Name + Year of Birth', 'score' => 2, 'year' => $yearOnly];
|
||||
}
|
||||
if ($ageYears !== null && in_array('age', $fields, true)) {
|
||||
$queries[] = ['reason' => $genderDb !== null ? 'Similar Name + Age + Gender' : 'Similar Name + Age', 'score' => 2, 'age' => $ageYears];
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
foreach ($queries as $query) {
|
||||
$builder = $db->table('patient_master')->select($select);
|
||||
if (isset($query['dob'])) {
|
||||
$builder->where('dob', $query['dob']);
|
||||
}
|
||||
if (isset($query['year'])) {
|
||||
$builder->where('YEAR(dob)', (int) $query['year']);
|
||||
}
|
||||
if (isset($query['age'])) {
|
||||
$builder->where('age', (int) $query['age']);
|
||||
}
|
||||
if ($genderDb !== null) {
|
||||
$builder->where('gender', $genderDb);
|
||||
}
|
||||
|
||||
foreach ($builder->limit(100)->get()->getResultArray() as $row) {
|
||||
if (! $this->isSimilarPatientName($nameUpper, (string) ($row['p_fname'] ?? ''))) {
|
||||
continue;
|
||||
}
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
if ($id <= 0 || isset($matches[$id])) {
|
||||
continue;
|
||||
}
|
||||
$row['_match_reason'] = (string) $query['reason'];
|
||||
$row['_match_score'] = (int) $query['score'];
|
||||
$matches[$id] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($matches);
|
||||
}
|
||||
|
||||
private function normalizeMatchName(string $name): string
|
||||
{
|
||||
return preg_replace('/[^A-Z0-9]+/', '', strtoupper($name)) ?? '';
|
||||
}
|
||||
|
||||
private function isSimilarPatientName(string $incomingName, string $storedName): bool
|
||||
{
|
||||
$incoming = $this->normalizeMatchName($incomingName);
|
||||
$stored = $this->normalizeMatchName($storedName);
|
||||
if ($incoming === '' || $stored === '' || $incoming === $stored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$maxDistance = strlen($incoming) <= 8 ? 1 : (strlen($incoming) <= 14 ? 2 : 3);
|
||||
similar_text($incoming, $stored, $percent);
|
||||
if (levenshtein($incoming, $stored) <= $maxDistance || $percent >= 86.0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$incomingFirst = $this->normalizeMatchName(strtok($incomingName, ' ') ?: $incomingName);
|
||||
$storedFirst = $this->normalizeMatchName(strtok($storedName, ' ') ?: $storedName);
|
||||
if (strlen($incomingFirst) < 4 || strlen($storedFirst) < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$firstDistance = strlen($incomingFirst) <= 8 ? 1 : 2;
|
||||
similar_text($incomingFirst, $storedFirst, $firstPercent);
|
||||
|
||||
return levenshtein($incomingFirst, $storedFirst) <= $firstDistance || $firstPercent >= 84.0;
|
||||
}
|
||||
|
||||
private function resolveFirstExistingColumn(array $fields, array $candidates): ?string
|
||||
{
|
||||
foreach ($candidates as $candidate) {
|
||||
|
|
@ -5115,6 +5298,431 @@ class AbdmGateway extends BaseController
|
|||
};
|
||||
}
|
||||
|
||||
private function pushAdditionalHiRecord(array $payload, int $patientId, string $abhaId, string $consentHandle)
|
||||
{
|
||||
$hiType = (string) ($payload['hi_type'] ?? 'HealthDocumentRecord');
|
||||
$entityType = (string) ($payload['entity_type'] ?? strtolower($hiType));
|
||||
$entityId = (string) ($payload['entity_id'] ?? $patientId);
|
||||
$ccRef = (string) ($payload['care_context_reference'] ?? ($hiType . '-' . $patientId . '-' . date('Y-m-d')));
|
||||
$ccDisplay = (string) ($payload['care_context_display'] ?? $hiType);
|
||||
$bundle = (array) ($payload['bundle'] ?? []);
|
||||
$visitDate = (string) ($payload['visit_date'] ?? date('Y-m-d'));
|
||||
$patientName = (string) ($payload['patient_name'] ?? ('PATIENT-' . $patientId));
|
||||
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$consent = $abhaId !== '' ? $this->getActiveConsentRecord($patientId, $abhaId, $consentHandle) : null;
|
||||
$consentWarning = '';
|
||||
if ($abhaId !== '' && $consent === null) {
|
||||
$consentWarning = 'No active consent found. Proceeding with care-context push only.';
|
||||
$this->logMissingConsentShareWarning($entityType, (int) $entityId, $patientId, $abhaId, $consentHandle, 'abdm.' . $entityType . '.share.consent_missing', $consentWarning);
|
||||
}
|
||||
$effectiveConsent = (string) ($consent['consent_handle'] ?? $consentHandle);
|
||||
|
||||
$healthRecordId = $this->storeHealthRecord([
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'hi_type' => $hiType,
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'fhir_bundle' => $bundleJson,
|
||||
'care_context_reference' => $ccRef,
|
||||
'consent_handle' => $effectiveConsent,
|
||||
]);
|
||||
|
||||
if ($abhaId === '') {
|
||||
if ($healthRecordId > 0 && $this->db->tableExists('health_records')) {
|
||||
$this->db->table('health_records')->where('id', $healthRecordId)->update([
|
||||
'push_status' => 'local_only',
|
||||
'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'status' => 'local_stored',
|
||||
'health_record_id' => $healthRecordId,
|
||||
'care_context_reference' => $ccRef,
|
||||
'message' => 'ABHA not available. Record stored locally for later discovery.',
|
||||
]);
|
||||
}
|
||||
|
||||
$queueId = null;
|
||||
$bridgeRecordId = 0;
|
||||
$connectorError = null;
|
||||
try {
|
||||
$result = $this->connector->pushRecord([
|
||||
'patient_id' => (string) $patientId,
|
||||
'patient_name' => $patientName,
|
||||
'abha_id' => preg_match('/^\d{14}$/', $abhaId) === 1 ? $abhaId : '',
|
||||
'abha_address' => str_contains($abhaId, '@') ? $abhaId : '',
|
||||
'hi_type' => $hiType,
|
||||
'record_type' => $hiType,
|
||||
'visit_date' => $visitDate,
|
||||
'care_context_reference' => $ccRef,
|
||||
'care_context_display' => $ccDisplay,
|
||||
'notes' => $ccDisplay,
|
||||
'queue_id' => $ccRef,
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
$queueId = (string) ($result['queue_id'] ?? $result['existing_queue_id'] ?? '');
|
||||
$bridgeRecordId = (int) ($result['record_id'] ?? $result['id'] ?? $result['existing_record_id'] ?? 0);
|
||||
$httpCode = (int) ($result['http_code'] ?? 0);
|
||||
$errorCode = strtoupper(trim((string) ($result['error_code'] ?? '')));
|
||||
if ((int) ($result['ok'] ?? 0) !== 1 && $httpCode !== 409 && $errorCode !== 'DUPLICATE_RECORD') {
|
||||
$connectorError = trim((string) ($result['message'] ?? $result['error_text'] ?? $errorCode));
|
||||
if ($connectorError === '') {
|
||||
$connectorError = 'Bridge push failed';
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$connectorError = $e->getMessage();
|
||||
}
|
||||
|
||||
if ($healthRecordId > 0) {
|
||||
$this->updateHealthRecordTxn($healthRecordId, (string) ($queueId ?? ''), $connectorError, $bridgeRecordId);
|
||||
}
|
||||
|
||||
$this->getAuditService()->log([
|
||||
'action' => 'push_record',
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'abha_id' => $abhaId,
|
||||
'patient_id' => $patientId,
|
||||
'request' => ['hi_type' => $hiType, 'care_context_reference' => $ccRef, 'consent_handle' => $effectiveConsent],
|
||||
'response' => ['queue_id' => $queueId, 'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null, 'warning' => $consentWarning],
|
||||
'outcome' => $connectorError === null ? 'success' : 'failure',
|
||||
'error_message' => (string) ($connectorError ?? ''),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => $connectorError === null ? 1 : 0,
|
||||
'queue_id' => $queueId,
|
||||
'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null,
|
||||
'health_record_id' => $healthRecordId,
|
||||
'care_context_reference' => $ccRef,
|
||||
'consent_handle' => $effectiveConsent,
|
||||
'status' => $connectorError === null ? 'queued' : 'failed',
|
||||
'message' => $connectorError === null && $consentWarning !== '' ? 'Record pushed to gateway without active consent. Recheck link status in M2 ABDM Gateway.' : null,
|
||||
'warning' => $consentWarning !== '' ? $consentWarning : null,
|
||||
'error' => $connectorError,
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildWellnessRecordPayload(int $patientId, int $opdId, string $abhaId): ?array
|
||||
{
|
||||
$patientRow = $this->loadPatientRow($patientId);
|
||||
if (empty($patientRow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = [];
|
||||
if ($this->db->tableExists('opd_prescription')) {
|
||||
$builder = $this->db->table('opd_prescription')->where('p_id', $patientId);
|
||||
if ($opdId > 0) {
|
||||
$builder->where('opd_id', $opdId);
|
||||
}
|
||||
$builder->groupStart()
|
||||
->where("COALESCE(NULLIF(TRIM(bp), ''), NULLIF(TRIM(diastolic), ''), NULLIF(TRIM(pulse), ''), NULLIF(TRIM(height), ''), NULLIF(TRIM(weight), ''), NULLIF(TRIM(temp), ''), NULLIF(TRIM(rr_min), ''), NULLIF(TRIM(spo2), ''), NULLIF(TRIM(glucose), '')) IS NOT NULL", null, false)
|
||||
->groupEnd();
|
||||
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
|
||||
$vitals = $this->extractWellnessVitals($row);
|
||||
$lifestyle = $this->extractPatientLifestyleObservations($patientRow);
|
||||
if (empty($vitals) && empty($lifestyle)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$patient = $this->buildAbdmPatientResource($patientRow, $patientId, $abhaId);
|
||||
$bundle = $this->buildSimpleWellnessBundle($patient, $vitals, $lifestyle);
|
||||
$visitDate = (string) ($row['date_opd_visit'] ?? date('Y-m-d'));
|
||||
$entityId = (string) ($opdId > 0 ? $opdId : ($row['opd_id'] ?? $patientId));
|
||||
|
||||
return [
|
||||
'hi_type' => 'WellnessRecord',
|
||||
'entity_type' => 'wellness',
|
||||
'entity_id' => $entityId,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => $this->patientDisplayName($patientRow),
|
||||
'visit_date' => $visitDate,
|
||||
'care_context_reference' => 'WELLNESS-' . $entityId . '-' . date('Y-m-d', strtotime($visitDate)),
|
||||
'care_context_display' => 'Wellness Record - ' . date('d/m/Y', strtotime($visitDate)),
|
||||
'bundle' => $bundle,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildHealthDocumentRecordPayload(int $patientId, string $abhaId): ?array
|
||||
{
|
||||
$patientRow = $this->loadPatientRow($patientId);
|
||||
if (empty($patientRow)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$title = trim((string) ($this->request->getPost('document_title') ?? $this->request->getPost('title') ?? ''));
|
||||
$documentText = trim((string) ($this->request->getPost('document_text') ?? $this->request->getPost('notes') ?? ''));
|
||||
$contentType = trim((string) ($this->request->getPost('content_type') ?? ''));
|
||||
$base64 = trim((string) ($this->request->getPost('document_base64') ?? ''));
|
||||
$filePath = trim((string) ($this->request->getPost('file_path') ?? $this->request->getPost('attachment_path') ?? ''));
|
||||
if ($title === '') {
|
||||
$title = 'Health Document';
|
||||
}
|
||||
if ($base64 === '' && $documentText !== '') {
|
||||
$contentType = 'text/html';
|
||||
$base64 = base64_encode('<div xmlns="http://www.w3.org/1999/xhtml">' . esc($documentText) . '</div>');
|
||||
}
|
||||
if ($base64 === '' && $filePath !== '') {
|
||||
$file = $this->resolveReadableAttachmentPath($filePath);
|
||||
if ($file !== '') {
|
||||
$base64 = base64_encode((string) file_get_contents($file));
|
||||
$contentType = $contentType !== '' ? $contentType : ((string) (mime_content_type($file) ?: 'application/octet-stream'));
|
||||
}
|
||||
}
|
||||
if ($base64 === '') {
|
||||
return null;
|
||||
}
|
||||
$contentType = $contentType !== '' ? $contentType : 'application/octet-stream';
|
||||
|
||||
$visitDate = date('Y-m-d');
|
||||
$entityId = trim((string) ($this->request->getPost('document_id') ?? $this->request->getPost('entity_id') ?? '')) ?: (string) $patientId;
|
||||
$patient = $this->buildAbdmPatientResource($patientRow, $patientId, $abhaId);
|
||||
$bundle = $this->buildSimpleHealthDocumentBundle($patient, $title, $contentType, $base64);
|
||||
|
||||
return [
|
||||
'hi_type' => 'HealthDocumentRecord',
|
||||
'entity_type' => 'health_document',
|
||||
'entity_id' => $entityId,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => $this->patientDisplayName($patientRow),
|
||||
'visit_date' => $visitDate,
|
||||
'care_context_reference' => 'HDOC-' . $patientId . '-' . $entityId . '-' . $visitDate,
|
||||
'care_context_display' => $title,
|
||||
'bundle' => $bundle,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildInvoiceRecordPayload(int $invoiceId, int $patientId, string $abhaId): ?array
|
||||
{
|
||||
if (! $this->db->tableExists('invoice_master')) {
|
||||
return null;
|
||||
}
|
||||
$invoice = $this->db->table('invoice_master')->where('id', $invoiceId)->get(1)->getRowArray() ?? [];
|
||||
if (empty($invoice)) {
|
||||
return null;
|
||||
}
|
||||
if ($patientId <= 0) {
|
||||
$patientId = (int) ($invoice['attach_id'] ?? 0);
|
||||
}
|
||||
$patientRow = $this->loadPatientRow($patientId);
|
||||
if ($patientId <= 0 || empty($patientRow)) {
|
||||
return null;
|
||||
}
|
||||
$items = $this->db->tableExists('invoice_item')
|
||||
? $this->db->table('invoice_item')->where('inv_master_id', $invoiceId)->orderBy('id', 'ASC')->get()->getResultArray()
|
||||
: [];
|
||||
|
||||
$visitDate = (string) ($invoice['inv_date'] ?? date('Y-m-d'));
|
||||
$patient = $this->buildAbdmPatientResource($patientRow, $patientId, $abhaId);
|
||||
$bundle = $this->buildSimpleInvoiceBundle($patient, $invoice, $items);
|
||||
|
||||
return [
|
||||
'hi_type' => 'InvoiceRecord',
|
||||
'entity_type' => 'invoice',
|
||||
'entity_id' => (string) $invoiceId,
|
||||
'patient_id' => $patientId,
|
||||
'patient_name' => $this->patientDisplayName($patientRow),
|
||||
'visit_date' => date('Y-m-d', strtotime($visitDate)),
|
||||
'care_context_reference' => 'INV-' . $invoiceId . '-' . date('Y-m-d', strtotime($visitDate)),
|
||||
'care_context_display' => 'Invoice ' . (string) ($invoice['invoice_code'] ?? $invoiceId),
|
||||
'bundle' => $bundle,
|
||||
];
|
||||
}
|
||||
|
||||
private function loadPatientRow(int $patientId): array
|
||||
{
|
||||
if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) {
|
||||
return [];
|
||||
}
|
||||
return $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
|
||||
private function patientDisplayName(array $patientRow): string
|
||||
{
|
||||
return trim(trim((string) ($patientRow['p_fname'] ?? '')) . ' ' . trim((string) ($patientRow['p_lname'] ?? ''))) ?: 'Patient';
|
||||
}
|
||||
|
||||
private function buildAbdmPatientResource(array $patientRow, int $patientId, string $abhaId): array
|
||||
{
|
||||
$genderRaw = trim((string) ($patientRow['gender'] ?? ''));
|
||||
$gender = match ($genderRaw) {
|
||||
'1', 'M', 'm', 'male', 'Male' => 'male',
|
||||
'2', 'F', 'f', 'female', 'Female' => 'female',
|
||||
default => 'unknown',
|
||||
};
|
||||
$patient = [
|
||||
'resourceType' => 'Patient',
|
||||
'id' => 'patient-' . $patientId,
|
||||
'name' => [['text' => $this->patientDisplayName($patientRow)]],
|
||||
'gender' => $gender,
|
||||
];
|
||||
if (! empty($patientRow['dob'])) {
|
||||
$patient['birthDate'] = date('Y-m-d', strtotime((string) $patientRow['dob']));
|
||||
}
|
||||
if ($abhaId !== '') {
|
||||
$patient['identifier'] = [[
|
||||
'system' => str_contains($abhaId, '@') ? 'https://healthid.ndhm.gov.in/abha-address' : 'https://healthid.ndhm.gov.in',
|
||||
'value' => $abhaId,
|
||||
]];
|
||||
}
|
||||
return $patient;
|
||||
}
|
||||
|
||||
private function extractWellnessVitals(array $row): array
|
||||
{
|
||||
if (empty($row)) {
|
||||
return [];
|
||||
}
|
||||
$vitals = [];
|
||||
$add = static function (string $code, string $display, mixed $value, string $unit, string $ucum) use (&$vitals): void {
|
||||
if ($value === null || trim((string) $value) === '' || ! is_numeric($value)) {
|
||||
return;
|
||||
}
|
||||
$vitals[] = ['loinc_code' => $code, 'display' => $display, 'value' => (float) $value, 'unit' => $unit, 'ucum_code' => $ucum];
|
||||
};
|
||||
$add('8480-6', 'Systolic blood pressure', $row['bp'] ?? null, 'mmHg', 'mm[Hg]');
|
||||
$add('8462-4', 'Diastolic blood pressure', $row['diastolic'] ?? null, 'mmHg', 'mm[Hg]');
|
||||
$add('8867-4', 'Heart rate', $row['pulse'] ?? null, '/min', '/min');
|
||||
$add('8302-2', 'Body height', $row['height'] ?? null, 'cm', 'cm');
|
||||
$add('29463-7', 'Body weight', $row['weight'] ?? null, 'kg', 'kg');
|
||||
$temp = $row['temp'] ?? null;
|
||||
if ($temp !== null && is_numeric($temp) && (float) $temp > 45) {
|
||||
$temp = (((float) $temp - 32) * 5) / 9;
|
||||
}
|
||||
$add('8310-5', 'Body temperature', $temp, 'Cel', 'Cel');
|
||||
$add('9279-1', 'Respiratory rate', $row['rr_min'] ?? null, '/min', '/min');
|
||||
$add('59408-5', 'Oxygen saturation in Arterial blood by Pulse oximetry', $row['spo2'] ?? null, '%', '%');
|
||||
$add('2339-0', 'Glucose', $row['glucose'] ?? null, 'mg/dL', 'mg/dL');
|
||||
return $vitals;
|
||||
}
|
||||
|
||||
private function extractPatientLifestyleObservations(array $patientRow): array
|
||||
{
|
||||
$map = [
|
||||
'is_smoking' => 'Smoking status',
|
||||
'is_alcohol' => 'Alcohol use',
|
||||
'is_drug_abuse' => 'Drug abuse status',
|
||||
'is_tobacoo' => 'Tobacco use',
|
||||
'is_hypertesion' => 'Hypertension history',
|
||||
'is_niddm' => 'Diabetes history',
|
||||
];
|
||||
$items = [];
|
||||
foreach ($map as $field => $label) {
|
||||
$value = trim((string) ($patientRow[$field] ?? ''));
|
||||
if ($value !== '' && $value !== '0') {
|
||||
$items[] = ['display' => $label, 'value' => 'Yes'];
|
||||
}
|
||||
}
|
||||
if (trim((string) ($patientRow['Others'] ?? '')) !== '') {
|
||||
$items[] = ['display' => 'Other wellness notes', 'value' => trim((string) $patientRow['Others'])];
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function buildSimpleWellnessBundle(array $patient, array $vitals, array $lifestyle): array
|
||||
{
|
||||
$issuedAt = Time::now('Asia/Kolkata')->format(DATE_ATOM);
|
||||
$patientRef = 'Patient/' . (string) ($patient['id'] ?? 'patient-unknown');
|
||||
$entries = [];
|
||||
$sectionEntries = [];
|
||||
$index = 1;
|
||||
foreach ($vitals as $vital) {
|
||||
$id = 'wellness-obs-' . $index++;
|
||||
$sectionEntries[] = ['reference' => 'Observation/' . $id];
|
||||
$entries[] = ['resource' => [
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $id,
|
||||
'status' => 'final',
|
||||
'category' => [['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/observation-category', 'code' => 'vital-signs', 'display' => 'Vital Signs']]]],
|
||||
'code' => ['coding' => [['system' => 'http://loinc.org', 'code' => (string) $vital['loinc_code'], 'display' => (string) $vital['display']]], 'text' => (string) $vital['display']],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'effectiveDateTime' => $issuedAt,
|
||||
'valueQuantity' => ['value' => (float) $vital['value'], 'unit' => (string) $vital['unit'], 'system' => 'http://unitsofmeasure.org', 'code' => (string) $vital['ucum_code']],
|
||||
]];
|
||||
}
|
||||
foreach ($lifestyle as $item) {
|
||||
$id = 'wellness-social-' . $index++;
|
||||
$sectionEntries[] = ['reference' => 'Observation/' . $id];
|
||||
$entries[] = ['resource' => [
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $id,
|
||||
'status' => 'final',
|
||||
'category' => [['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/observation-category', 'code' => 'social-history', 'display' => 'Social History']]]],
|
||||
'code' => ['text' => (string) ($item['display'] ?? 'Lifestyle')],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'effectiveDateTime' => $issuedAt,
|
||||
'valueString' => (string) ($item['value'] ?? ''),
|
||||
]];
|
||||
}
|
||||
$composition = ['resource' => [
|
||||
'resourceType' => 'Composition',
|
||||
'id' => 'composition-wellness-' . date('YmdHis'),
|
||||
'status' => 'final',
|
||||
'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '371529009', 'display' => 'Health maintenance report']]],
|
||||
'subject' => ['reference' => $patientRef],
|
||||
'date' => $issuedAt,
|
||||
'title' => 'Wellness Record',
|
||||
'section' => [['title' => 'Vital Signs and Wellness', 'entry' => $sectionEntries]],
|
||||
]];
|
||||
return ['resourceType' => 'Bundle', 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => array_merge([$composition, ['resource' => $patient]], $entries)];
|
||||
}
|
||||
|
||||
private function buildSimpleHealthDocumentBundle(array $patient, string $title, string $contentType, string $base64): array
|
||||
{
|
||||
$issuedAt = Time::now('Asia/Kolkata')->format(DATE_ATOM);
|
||||
$patientRef = 'Patient/' . (string) ($patient['id'] ?? 'patient-unknown');
|
||||
$binaryId = 'binary-health-doc-' . date('YmdHis');
|
||||
$docId = 'doc-health-' . date('YmdHis');
|
||||
$composition = ['resource' => ['resourceType' => 'Composition', 'id' => 'composition-health-doc-' . date('YmdHis'), 'status' => 'final', 'type' => ['text' => 'Health Document Record'], 'subject' => ['reference' => $patientRef], 'date' => $issuedAt, 'title' => $title, 'section' => [['title' => $title, 'entry' => [['reference' => 'DocumentReference/' . $docId]]]]]];
|
||||
$binary = ['resource' => ['resourceType' => 'Binary', 'id' => $binaryId, 'contentType' => $contentType, 'data' => $base64]];
|
||||
$docRef = ['resource' => ['resourceType' => 'DocumentReference', 'id' => $docId, 'status' => 'current', 'type' => ['text' => $title], 'subject' => ['reference' => $patientRef], 'date' => $issuedAt, 'content' => [['attachment' => ['contentType' => $contentType, 'url' => 'Binary/' . $binaryId, 'title' => $title]]]]];
|
||||
return ['resourceType' => 'Bundle', 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => [$composition, ['resource' => $patient], $binary, $docRef]];
|
||||
}
|
||||
|
||||
private function buildSimpleInvoiceBundle(array $patient, array $invoice, array $items): array
|
||||
{
|
||||
$issuedAt = Time::now('Asia/Kolkata')->format(DATE_ATOM);
|
||||
$patientRef = 'Patient/' . (string) ($patient['id'] ?? 'patient-unknown');
|
||||
$invoiceId = 'invoice-' . (int) ($invoice['id'] ?? 0);
|
||||
$lineItems = [];
|
||||
foreach ($items as $idx => $item) {
|
||||
$lineItems[] = ['sequence' => $idx + 1, 'chargeItemCodeableConcept' => ['text' => (string) ($item['item_name'] ?? 'Item')], 'priceComponent' => [['type' => 'base', 'amount' => ['value' => (float) ($item['item_amount'] ?? 0), 'currency' => 'INR']]]];
|
||||
}
|
||||
$invoiceResource = ['resourceType' => 'Invoice', 'id' => $invoiceId, 'status' => 'issued', 'subject' => ['reference' => $patientRef], 'date' => ! empty($invoice['inv_date']) ? date('Y-m-d', strtotime((string) $invoice['inv_date'])) : date('Y-m-d'), 'totalNet' => ['value' => (float) ($invoice['net_amount'] ?? $invoice['total_amount'] ?? 0), 'currency' => 'INR'], 'totalGross' => ['value' => (float) ($invoice['total_amount'] ?? $invoice['net_amount'] ?? 0), 'currency' => 'INR']];
|
||||
if (! empty($lineItems)) {
|
||||
$invoiceResource['lineItem'] = $lineItems;
|
||||
}
|
||||
$composition = ['resource' => ['resourceType' => 'Composition', 'id' => 'composition-invoice-' . (int) ($invoice['id'] ?? 0), 'status' => 'final', 'type' => ['text' => 'Invoice Record'], 'subject' => ['reference' => $patientRef], 'date' => $issuedAt, 'title' => 'Invoice ' . (string) ($invoice['invoice_code'] ?? $invoice['id'] ?? ''), 'section' => [['title' => 'Invoice details', 'entry' => [['reference' => 'Invoice/' . $invoiceId]]]]]];
|
||||
return ['resourceType' => 'Bundle', 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => [$composition, ['resource' => $patient], ['resource' => $invoiceResource]]];
|
||||
}
|
||||
|
||||
private function resolveReadableAttachmentPath(string $path): string
|
||||
{
|
||||
$candidate = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
|
||||
if (! str_starts_with($candidate, DIRECTORY_SEPARATOR) && ! preg_match('/^[A-Za-z]:\\\\/', $candidate)) {
|
||||
$candidate = FCPATH . ltrim($candidate, DIRECTORY_SEPARATOR);
|
||||
}
|
||||
$real = realpath($candidate);
|
||||
if ($real === false || ! is_file($real) || ! is_readable($real)) {
|
||||
return '';
|
||||
}
|
||||
$allowed = array_filter([realpath(FCPATH), realpath(WRITEPATH)]);
|
||||
foreach ($allowed as $root) {
|
||||
if (str_starts_with($real, (string) $root)) {
|
||||
return $real;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function getActiveConsentRecord(int $patientId, string $abhaId, string $consentHandle = ''): ?array
|
||||
{
|
||||
$hasAbdmConsentTable = $this->db->tableExists('abdm_consent_records');
|
||||
|
|
@ -5185,6 +5793,68 @@ class AbdmGateway extends BaseController
|
|||
return null;
|
||||
}
|
||||
|
||||
private function logMissingConsentShareBlock(string $entityType, int $entityId, int $patientId, string $abhaId, string $consentHandle, string $eventType): void
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_api_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = 'No active consent found. Share blocked due to expiry/not-approved consent.';
|
||||
$this->db->table('abdm_api_logs')->insert([
|
||||
'channel' => 'bridge',
|
||||
'event_type' => $eventType,
|
||||
'endpoint' => '/AbdmGateway/share_' . $entityType,
|
||||
'http_method' => 'POST',
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => (string) $entityId,
|
||||
'request_json' => (string) json_encode([
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => $entityId,
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'consent_handle' => $consentHandle,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'response_code' => 200,
|
||||
'response_json' => (string) json_encode([
|
||||
'ok' => 0,
|
||||
'error_text' => $message,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'status' => 'error',
|
||||
'error_message' => $message,
|
||||
'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function logMissingConsentShareWarning(string $entityType, int $entityId, int $patientId, string $abhaId, string $consentHandle, string $eventType, string $message): void
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_api_logs')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->table('abdm_api_logs')->insert([
|
||||
'channel' => 'bridge',
|
||||
'event_type' => $eventType,
|
||||
'endpoint' => '/AbdmGateway/share_' . $entityType,
|
||||
'http_method' => 'POST',
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => (string) $entityId,
|
||||
'request_json' => json_encode([
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $abhaId,
|
||||
'consent_handle' => $consentHandle,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'response_json' => json_encode([
|
||||
'ok' => 1,
|
||||
'note' => $message,
|
||||
'care_context_push_only' => 1,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'response_code' => 200,
|
||||
'status' => 'warning',
|
||||
'error_message' => $message,
|
||||
'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the consent id expected by gateway: consent_id/gateway_consent_id fallback to consent_handle.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -220,6 +220,23 @@ class AbdmOpdQueue extends BaseController
|
|||
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
||||
$message = (string) (
|
||||
$result['error_text']
|
||||
?? $result['message']
|
||||
?? $result['error']
|
||||
?? 'Bridge rejected OPD token status update'
|
||||
);
|
||||
|
||||
return $this->response
|
||||
->setStatusCode((int) ($result['http_code'] ?? 502) ?: 502)
|
||||
->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => $message,
|
||||
'bridge_http_code' => (int) ($result['http_code'] ?? 0),
|
||||
]);
|
||||
}
|
||||
|
||||
// Mirror status to local DB so list() stays consistent
|
||||
try {
|
||||
$db->table('abdm_opd_tokens')
|
||||
|
|
@ -285,6 +302,8 @@ class AbdmOpdQueue extends BaseController
|
|||
$phone = trim((string) ($this->request->getPost('phone') ?? ''));
|
||||
$gender = strtoupper(trim((string) ($this->request->getPost('gender') ?? '')));
|
||||
$dob = trim((string) ($this->request->getPost('dob') ?? '')); // YYYY-MM-DD from gateway
|
||||
$age = trim((string) ($this->request->getPost('age') ?? $this->request->getPost('patient_age') ?? ''));
|
||||
$birthYear = trim((string) ($this->request->getPost('birth_year') ?? $this->request->getPost('birthYear') ?? $this->request->getPost('year_of_birth') ?? ''));
|
||||
$relationText = trim((string) ($this->request->getPost('relation_text') ?? ''));
|
||||
$relationType = trim((string) ($this->request->getPost('relation_type') ?? ''));
|
||||
$relationName = trim((string) ($this->request->getPost('relative_name') ?? ''));
|
||||
|
|
@ -307,7 +326,7 @@ class AbdmOpdQueue extends BaseController
|
|||
$abhaField = $this->resolveFirstExistingColumn($fields, ['abha_id', 'abha_no', 'abha', 'abha_address']);
|
||||
$aadhaarField = $this->resolveFirstExistingColumn($fields, ['udai', 'aadhar_no', 'aadhaar_no', 'aadhaar', 'adhar_no']);
|
||||
|
||||
$matches = $this->findPatientMatches($db, $abhaField, $aadhaarField, $abhaRaw, $abhaAddr, $aadhaarRaw, $phone, $name, $gender, $dob);
|
||||
$matches = $this->findPatientMatches($db, $abhaField, $aadhaarField, $abhaRaw, $abhaAddr, $aadhaarRaw, $phone, $name, $gender, $dob, $age, $birthYear);
|
||||
|
||||
if ($action === 'check') {
|
||||
return $this->response->setJSON([
|
||||
|
|
@ -323,6 +342,8 @@ class AbdmOpdQueue extends BaseController
|
|||
'phone' => $phone,
|
||||
'gender' => $gender,
|
||||
'dob' => $dob,
|
||||
'age' => $age,
|
||||
'birth_year' => $birthYear,
|
||||
'relation_text' => $relationText,
|
||||
'relation_type' => $relationType,
|
||||
'relative_name' => $relationName,
|
||||
|
|
@ -350,6 +371,8 @@ class AbdmOpdQueue extends BaseController
|
|||
'phone' => $phone,
|
||||
'gender' => $gender,
|
||||
'dob' => $dob,
|
||||
'age' => $age,
|
||||
'birth_year' => $birthYear,
|
||||
'relation_text' => $relationText,
|
||||
'relation_type' => $relationType,
|
||||
'relative_name' => $relationName,
|
||||
|
|
@ -396,7 +419,7 @@ class AbdmOpdQueue extends BaseController
|
|||
if (! empty($backfill)) {
|
||||
$db->table('patient_master')->where('id', $patientId)->update($backfill);
|
||||
}
|
||||
} elseif (($action === '' || $action === 'auto') && count($matches) === 1) {
|
||||
} elseif (($action === '' || $action === 'auto') && count($matches) === 1 && (int) ($matches[0]['match_score'] ?? 0) > 1) {
|
||||
$existing = $matches[0];
|
||||
$patientId = (int) ($existing['id'] ?? 0);
|
||||
$pCode = (string) ($existing['p_code'] ?? '');
|
||||
|
|
@ -441,21 +464,14 @@ class AbdmOpdQueue extends BaseController
|
|||
|
||||
$this->attachPatientToToken($db, $tokenId, $patientId);
|
||||
|
||||
// Mark token as CALLED on gateway (non-blocking)
|
||||
try {
|
||||
$this->gw->opdTokenUpdateStatus($tokenId, 'CALLED');
|
||||
} catch (\Throwable $e) {
|
||||
// non-critical
|
||||
}
|
||||
|
||||
$oldStatus = strtoupper(trim((string) ($tokenBefore['status'] ?? '')));
|
||||
if ($oldStatus !== 'CALLED') {
|
||||
if ($oldStatus !== '') {
|
||||
$this->logTokenAudit(
|
||||
'abdm_opd_queue',
|
||||
(string) $tokenId,
|
||||
'status',
|
||||
$oldStatus,
|
||||
'CALLED',
|
||||
'patient_id',
|
||||
(string) ($tokenBefore['patient_id'] ?? ''),
|
||||
(string) $patientId,
|
||||
[
|
||||
'source' => 'register_opd',
|
||||
'action' => $action,
|
||||
|
|
@ -497,7 +513,9 @@ class AbdmOpdQueue extends BaseController
|
|||
string $phone,
|
||||
string $name = '',
|
||||
string $gender = '',
|
||||
string $dob = ''
|
||||
string $dob = '',
|
||||
string $age = '',
|
||||
string $birthYear = ''
|
||||
): array {
|
||||
$select = 'id,p_code,p_fname,mphone1,dob,gender';
|
||||
if ($abhaField) {
|
||||
|
|
@ -548,7 +566,8 @@ class AbdmOpdQueue extends BaseController
|
|||
$nameUpper = strtoupper(trim($name));
|
||||
$genderDb = ($gender === 'F' || $gender === '2') ? 2 : ($gender !== '' ? 1 : null);
|
||||
$dobValid = preg_match('/^\d{4}-\d{2}-\d{2}$/', $dob) ? $dob : '';
|
||||
$yearOnly = $dobValid !== '' ? (int) substr($dobValid, 0, 4) : 0;
|
||||
$yearOnly = $dobValid !== '' ? (int) substr($dobValid, 0, 4) : (preg_match('/^(19|20)\d{2}$/', $birthYear) ? (int) $birthYear : 0);
|
||||
$ageYears = preg_match('/^\d{1,3}$/', $age) ? (int) $age : null;
|
||||
|
||||
if ($nameUpper !== '' && $dobValid !== '') {
|
||||
// Name + exact DOB [+ gender] — high confidence (score 3)
|
||||
|
|
@ -580,6 +599,13 @@ class AbdmOpdQueue extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
foreach ($this->findFuzzyDemographicMatches($db, $select, $nameUpper, $genderDb, $dobValid, $yearOnly, $ageYears) as $row) {
|
||||
$reason = (string) ($row['_match_reason'] ?? 'Similar Name + Demographics');
|
||||
$score = (int) ($row['_match_score'] ?? 2);
|
||||
unset($row['_match_reason'], $row['_match_score']);
|
||||
$append([$row], $reason, $score);
|
||||
}
|
||||
|
||||
// Phone match — supplementary only (score 1), ambiguous for families
|
||||
if ($phone !== '') {
|
||||
$rows = $db->table('patient_master')->select($select)->where('mphone1', $phone)->limit(10)->get()->getResultArray();
|
||||
|
|
@ -607,6 +633,96 @@ class AbdmOpdQueue extends BaseController
|
|||
return $result;
|
||||
}
|
||||
|
||||
private function findFuzzyDemographicMatches(
|
||||
\CodeIgniter\Database\BaseConnection $db,
|
||||
string $select,
|
||||
string $nameUpper,
|
||||
?int $genderDb,
|
||||
string $dobValid,
|
||||
int $yearOnly,
|
||||
?int $ageYears
|
||||
): array {
|
||||
if ($nameUpper === '' || strlen($this->normalizeMatchName($nameUpper)) < 4) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fields = $db->getFieldNames('patient_master') ?? [];
|
||||
$queries = [];
|
||||
|
||||
if ($dobValid !== '') {
|
||||
$queries[] = ['reason' => $genderDb !== null ? 'Similar Name + DOB + Gender' : 'Similar Name + DOB', 'score' => 3, 'dob' => $dobValid];
|
||||
}
|
||||
if ($yearOnly > 0) {
|
||||
$queries[] = ['reason' => $genderDb !== null ? 'Similar Name + Year of Birth + Gender' : 'Similar Name + Year of Birth', 'score' => 2, 'year' => $yearOnly];
|
||||
}
|
||||
if ($ageYears !== null && in_array('age', $fields, true)) {
|
||||
$queries[] = ['reason' => $genderDb !== null ? 'Similar Name + Age + Gender' : 'Similar Name + Age', 'score' => 2, 'age' => $ageYears];
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
foreach ($queries as $query) {
|
||||
$builder = $db->table('patient_master')->select($select);
|
||||
if (isset($query['dob'])) {
|
||||
$builder->where('dob', $query['dob']);
|
||||
}
|
||||
if (isset($query['year'])) {
|
||||
$builder->where('YEAR(dob)', (int) $query['year']);
|
||||
}
|
||||
if (isset($query['age'])) {
|
||||
$builder->where('age', (int) $query['age']);
|
||||
}
|
||||
if ($genderDb !== null) {
|
||||
$builder->where('gender', $genderDb);
|
||||
}
|
||||
|
||||
foreach ($builder->limit(100)->get()->getResultArray() as $row) {
|
||||
if (! $this->isSimilarPatientName($nameUpper, (string) ($row['p_fname'] ?? ''))) {
|
||||
continue;
|
||||
}
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
if ($id <= 0 || isset($matches[$id])) {
|
||||
continue;
|
||||
}
|
||||
$row['_match_reason'] = (string) $query['reason'];
|
||||
$row['_match_score'] = (int) $query['score'];
|
||||
$matches[$id] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($matches);
|
||||
}
|
||||
|
||||
private function normalizeMatchName(string $name): string
|
||||
{
|
||||
return preg_replace('/[^A-Z0-9]+/', '', strtoupper($name)) ?? '';
|
||||
}
|
||||
|
||||
private function isSimilarPatientName(string $incomingName, string $storedName): bool
|
||||
{
|
||||
$incoming = $this->normalizeMatchName($incomingName);
|
||||
$stored = $this->normalizeMatchName($storedName);
|
||||
if ($incoming === '' || $stored === '' || $incoming === $stored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$maxDistance = strlen($incoming) <= 8 ? 1 : (strlen($incoming) <= 14 ? 2 : 3);
|
||||
similar_text($incoming, $stored, $percent);
|
||||
if (levenshtein($incoming, $stored) <= $maxDistance || $percent >= 86.0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$incomingFirst = $this->normalizeMatchName(strtok($incomingName, ' ') ?: $incomingName);
|
||||
$storedFirst = $this->normalizeMatchName(strtok($storedName, ' ') ?: $storedName);
|
||||
if (strlen($incomingFirst) < 4 || strlen($storedFirst) < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$firstDistance = strlen($incomingFirst) <= 8 ? 1 : 2;
|
||||
similar_text($incomingFirst, $storedFirst, $firstPercent);
|
||||
|
||||
return levenshtein($incomingFirst, $storedFirst) <= $firstDistance || $firstPercent >= 84.0;
|
||||
}
|
||||
|
||||
private function createPatientFromToken(
|
||||
\CodeIgniter\Database\BaseConnection $db,
|
||||
?string $abhaField,
|
||||
|
|
@ -711,7 +827,6 @@ class AbdmOpdQueue extends BaseController
|
|||
->where('gateway_token_id', $tokenId)
|
||||
->update([
|
||||
'patient_id' => $patientId,
|
||||
'status' => 'CALLED',
|
||||
'processed_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class Abha extends BaseController
|
|||
?: ($payload['abha_address'] ?? '')
|
||||
?: ''
|
||||
);
|
||||
$name = (string) ($profile['name'] ?? $profile['fullName'] ?? $profile['full_name'] ?? $payload['name'] ?? $payload['full_name'] ?? '');
|
||||
$name = $this->extractAbhaProfileName($profile, $payload);
|
||||
$photo = (string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? '');
|
||||
$mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? '');
|
||||
$profileGender = (string) ($profile['gender'] ?? $payload['gender'] ?? '');
|
||||
|
|
@ -225,7 +225,7 @@ class Abha extends BaseController
|
|||
?: ($payload['abha_address'] ?? '')
|
||||
?: ''
|
||||
);
|
||||
$name = (string) ($profile['name'] ?? $profile['fullName'] ?? $profile['full_name'] ?? $payload['name'] ?? $payload['full_name'] ?? '');
|
||||
$name = $this->extractAbhaProfileName($profile, $payload);
|
||||
$photo = (string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? '');
|
||||
$gender = (string) ($profile['gender'] ?? $payload['gender'] ?? '');
|
||||
$dob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? '');
|
||||
|
|
@ -289,6 +289,41 @@ class Abha extends BaseController
|
|||
return $this->response->setJSON(['ok' => 1, 'message' => 'ABHA created successfully.']);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Facility Scan & Share QR — GET abha/register/facility_qr
|
||||
// -------------------------------------------------------------------------
|
||||
public function facilityQr()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var \App\Libraries\Abdm\AbdmConnectorInterface $connector */
|
||||
$connector = AbdmConnectorFactory::make();
|
||||
$result = $connector->facilityQrCode();
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
|
||||
return $this->response->setStatusCode((int) ($result['http_code'] ?? 502) ?: 502)->setJSON([
|
||||
'ok' => 0,
|
||||
'error_text' => $result['error_text'] ?? $result['message'] ?? $result['error'] ?? 'Unable to fetch facility QR code',
|
||||
'bridge_http_code' => (int) ($result['http_code'] ?? 0),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'hospital_id' => $result['hospital_id'] ?? $result['data']['hospital_id'] ?? '',
|
||||
'hospital_name' => $result['hospital_name'] ?? $result['data']['hospital_name'] ?? '',
|
||||
'hfr_id' => $result['hfr_id'] ?? $result['data']['hfr_id'] ?? '',
|
||||
'qr_data' => $result['qr_data'] ?? $result['data']['qr_data'] ?? '',
|
||||
'request_id' => $result['request_id'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ABHA Card view — GET abha/card/{abha_number}
|
||||
// Renders a printable card page for a patient whose ABHA is stored in HMS.
|
||||
|
|
@ -391,11 +426,20 @@ class Abha extends BaseController
|
|||
if ($existing) {
|
||||
$patientId = (int) ($existing['id'] ?? 0);
|
||||
$pCode = (string) ($existing['p_code'] ?? '');
|
||||
$existingUpdates = [];
|
||||
|
||||
$existingName = strtoupper(trim((string) ($existing['p_fname'] ?? '')));
|
||||
if ($name !== '' && ($existingName === '' || $existingName === 'ABHA PATIENT')) {
|
||||
$existingUpdates['p_fname'] = strtoupper($name);
|
||||
}
|
||||
|
||||
// Backfill ABHA field if it was empty
|
||||
if ($abhaField && empty($existing[$abhaField]) && $abhaNumClean !== '') {
|
||||
$db->table('patient_master')
|
||||
->where('id', $patientId)
|
||||
->update([$abhaField => $abhaNumClean]);
|
||||
$existingUpdates[$abhaField] = $abhaNumClean;
|
||||
}
|
||||
|
||||
if ($existingUpdates !== []) {
|
||||
$db->table('patient_master')->where('id', $patientId)->update($existingUpdates);
|
||||
}
|
||||
|
||||
$this->syncAbhaMetaToPatient($patientId, $abhaMeta, $abhaNumClean, $abhaAddress, $fields);
|
||||
|
|
@ -687,12 +731,46 @@ class Abha extends BaseController
|
|||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $profile
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function extractAbhaProfileName(array $profile, array $payload): string
|
||||
{
|
||||
foreach ([$profile, $payload, $payload['gateway_abha_profile'] ?? [], $payload['gateway_patient'] ?? []] as $source) {
|
||||
if (! is_array($source)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (['name', 'fullName', 'full_name', 'patient_name'] as $key) {
|
||||
$name = trim((string) ($source[$key] ?? ''));
|
||||
if ($name !== '') {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
foreach (['firstName', 'first_name', 'middleName', 'middle_name', 'lastName', 'last_name'] as $key) {
|
||||
$part = trim((string) ($source[$key] ?? ''));
|
||||
if ($part !== '') {
|
||||
$parts[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parts !== []) {
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $profile
|
||||
*/
|
||||
private function looksLikeGatewayAbhaProfile(array $profile): bool
|
||||
{
|
||||
foreach (['ABHANumber', 'abha_id', 'preferredAddress', 'preferredAbhaAddress', 'abha_address', 'name', 'fullName', 'full_name', 'gender', 'dob', 'date_of_birth', 'profilePhoto', 'profile_photo'] as $key) {
|
||||
foreach (['ABHANumber', 'abha_id', 'preferredAddress', 'preferredAbhaAddress', 'abha_address', 'name', 'fullName', 'full_name', 'patient_name', 'firstName', 'first_name', 'lastName', 'last_name', 'gender', 'dob', 'date_of_birth', 'profilePhoto', 'profile_photo'] as $key) {
|
||||
if (array_key_exists($key, $profile) && trim((string) $profile[$key]) !== '') {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6193,6 +6193,7 @@ class Opd_prescription extends BaseController
|
|||
$out = [];
|
||||
$seen = [];
|
||||
$needle = mb_strtolower($this->normalizeAutocompleteSuggestionText($q));
|
||||
$hasLocalSnomedRows = false;
|
||||
|
||||
$csnotkRows = (new CsnotkTerminologyService())->searchDiagnosis($q, 20);
|
||||
foreach ($csnotkRows as $row) {
|
||||
|
|
@ -6206,6 +6207,9 @@ class Opd_prescription extends BaseController
|
|||
|
||||
$conceptId = trim((string) ($row['concept_id'] ?? ''));
|
||||
$source = trim((string) ($row['source'] ?? 'csnotk'));
|
||||
if (in_array($source, ['local', 'snomed'], true)) {
|
||||
$hasLocalSnomedRows = true;
|
||||
}
|
||||
$k = mb_strtoupper($name) . '|' . $conceptId;
|
||||
if (isset($seen[$k])) {
|
||||
continue;
|
||||
|
|
@ -6224,7 +6228,7 @@ class Opd_prescription extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
$snomedRows = $this->fetchSnomedDiagnosisRows($q, 20);
|
||||
$snomedRows = ($hasLocalSnomedRows || mb_strlen($needle) < 3) ? [] : $this->fetchSnomedDiagnosisRows($q, 20);
|
||||
foreach ($snomedRows as $row) {
|
||||
$name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? ''));
|
||||
if ($name === '') {
|
||||
|
|
|
|||
|
|
@ -2669,7 +2669,7 @@ class Patient extends BaseController
|
|||
$kycVerified = (int) ($row['abha_kyc_verified'] ?? 0) === 1;
|
||||
$mobileVerified = (int) ($row['abha_mobile_verified'] ?? 0) === 1;
|
||||
$hasStatusColumn = array_key_exists('abha_verified_status', $row);
|
||||
$isVerified = $verifiedStatus === 'VERIFIED' || ($kycVerified && $mobileVerified);
|
||||
$isVerified = in_array($verifiedStatus, ['1', 'VERIFIED', 'YES', 'Y', 'TRUE'], true) || ($kycVerified && $mobileVerified);
|
||||
if (! $hasStatusColumn && $abhaAddress !== '') {
|
||||
$isVerified = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,12 @@ interface AbdmConnectorInterface
|
|||
*/
|
||||
public function abhaMobileVerifyOtp(array $payload): array;
|
||||
|
||||
/**
|
||||
* Fetch hospital facility details and official ABDM Scan & Share QR image.
|
||||
* Gateway response includes hospital_name, hfr_id, and qr_data data URI/base64.
|
||||
*/
|
||||
public function facilityQrCode(): array;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Health Record Sharing
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -142,6 +142,14 @@ class DirectAbdmConnector implements AbdmConnectorInterface
|
|||
);
|
||||
}
|
||||
|
||||
public function facilityQrCode(): array
|
||||
{
|
||||
return [
|
||||
'ok' => 0,
|
||||
'error_text' => 'Facility QR API is not supported by DirectAbdmConnector. Use eatria_bridge connector.',
|
||||
];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Scan & Share
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -322,6 +322,14 @@ class DreamsoftConnector implements AbdmConnectorInterface
|
|||
];
|
||||
}
|
||||
|
||||
public function facilityQrCode(): array
|
||||
{
|
||||
return [
|
||||
'ok' => 0,
|
||||
'error_text' => 'Facility QR API is not supported by DreamsoftConnector. Use eatria_bridge connector.',
|
||||
];
|
||||
}
|
||||
|
||||
public function opdTokenCreate(array $payload): array
|
||||
{
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -847,6 +847,11 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
|
|||
return $this->get('/v3/opd/queue', $params);
|
||||
}
|
||||
|
||||
public function facilityQrCode(): array
|
||||
{
|
||||
return $this->get('/v3/facility/qr-code');
|
||||
}
|
||||
|
||||
public function opdTokenCreate(array $payload): array
|
||||
{
|
||||
if (array_key_exists('gender', $payload)) {
|
||||
|
|
@ -858,12 +863,21 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
|
|||
}
|
||||
}
|
||||
|
||||
if ($this->hfrId !== '' && empty($payload['hfr_id'])) {
|
||||
$payload['hfr_id'] = $this->hfrId;
|
||||
}
|
||||
|
||||
return $this->post('/v3/opd/token', $payload);
|
||||
}
|
||||
|
||||
public function opdTokenUpdateStatus(int $tokenId, string $status): array
|
||||
{
|
||||
return $this->patch('/v3/opd/token/' . $tokenId, ['status' => $status]);
|
||||
$body = ['status' => $status];
|
||||
if ($this->hfrId !== '') {
|
||||
$body['hfr_id'] = $this->hfrId;
|
||||
}
|
||||
|
||||
return $this->patch('/v3/opd/token/' . $tokenId, $body);
|
||||
}
|
||||
|
||||
public function opdRunningTokenStatus(): array
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class CsnotkTerminologyService
|
|||
// Local DB is primary — ~17ms vs ~330ms for external API from server
|
||||
$rows = $this->localSearch($q, $limit, ['finding', 'disorder', 'morphologic abnormality', 'disease']);
|
||||
|
||||
if (count($rows) < $limit && $this->apiEnabled && $this->baseUrl !== '') {
|
||||
if (count($rows) < $limit && mb_strlen($q) >= 3 && $this->apiEnabled && $this->baseUrl !== '') {
|
||||
$rows = $this->mergeFallback($rows, $this->apiSearchDiagnosis($q, $limit), $limit);
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ class CsnotkTerminologyService
|
|||
// Local DB is primary — ~17ms vs ~330ms for external API from server
|
||||
$rows = $this->localSearch($q, $limit, ['finding', 'disorder']);
|
||||
|
||||
if (count($rows) < $limit && $this->apiEnabled && $this->baseUrl !== '') {
|
||||
if (count($rows) < $limit && mb_strlen($q) >= 3 && $this->apiEnabled && $this->baseUrl !== '') {
|
||||
$rows = $this->mergeFallback($rows, $this->apiSearchBySemTag($q, $limit, ['finding', 'disorder']), $limit);
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ class CsnotkTerminologyService
|
|||
|
||||
$rows = $this->localSearch($q, $limit, $substanceTags);
|
||||
|
||||
if (count($rows) < $limit && $this->apiEnabled && $this->baseUrl !== '') {
|
||||
if (count($rows) < $limit && mb_strlen($q) >= 3 && $this->apiEnabled && $this->baseUrl !== '') {
|
||||
// CSNOtk: search substance tag first, then product tag as fallback
|
||||
$apiRows = $this->apiSearchBySemTag($q, $limit, ['substance', 'product']);
|
||||
$rows = $this->mergeFallback($rows, $apiRows, $limit);
|
||||
|
|
@ -160,7 +160,7 @@ class CsnotkTerminologyService
|
|||
// Local DB is primary — ~17ms vs ~330ms for external API from server
|
||||
$rows = $this->localSearch($q, $limit, ['procedure', 'observable entity', 'regime/therapy']);
|
||||
|
||||
if (count($rows) < $limit && $this->apiEnabled && $this->baseUrl !== '') {
|
||||
if (count($rows) < $limit && mb_strlen($q) >= 3 && $this->apiEnabled && $this->baseUrl !== '') {
|
||||
$rows = $this->mergeFallback($rows, $this->apiSearchBySemTag($q, $limit, ['procedure', 'observable entity']), $limit);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -408,6 +408,7 @@
|
|||
aadhaar_number: t.aadhaar_number ?? t.aadhar_number ?? t.udai ?? '',
|
||||
patient_name: t.patient_name ?? '', phone: t.phone ?? '',
|
||||
gender: t.gender ?? '', dob: t.dob ?? '',
|
||||
age: t.age ?? t.patient_age ?? '', birth_year: t.birth_year ?? t.birthYear ?? t.year_of_birth ?? '',
|
||||
relation_text: t.relation_text ?? '', relation_type: t.relation_type ?? '', relative_name: t.relative_name ?? '',
|
||||
email: t.email ?? t.email1 ?? '',
|
||||
address: t.address ?? t.add1 ?? '',
|
||||
|
|
@ -488,6 +489,7 @@
|
|||
aadhaar_number: t.aadhaar_number ?? '',
|
||||
patient_name: t.patient_name ?? '', phone: t.phone ?? '',
|
||||
gender: t.gender ?? '', dob: t.dob ?? '',
|
||||
age: t.age ?? t.patient_age ?? '', birth_year: t.birth_year ?? t.birthYear ?? t.year_of_birth ?? '',
|
||||
}).then(r => {
|
||||
if (!r.ok) {
|
||||
body.innerHTML = `<div class="alert alert-danger small">${esc(r.error_text ?? 'Failed to process token')}</div>
|
||||
|
|
@ -554,6 +556,7 @@
|
|||
aadhaar_number: t.aadhaar_number ?? '',
|
||||
patient_name: t.patient_name ?? '', phone: t.phone ?? '',
|
||||
gender: t.gender ?? '', dob: t.dob ?? '',
|
||||
age: t.age ?? t.patient_age ?? '', birth_year: t.birth_year ?? t.birthYear ?? t.year_of_birth ?? '',
|
||||
}).then(handleProcessResult).catch(err => {
|
||||
body.innerHTML = `<div class="alert alert-danger small">Request error: ${esc(err.message)}</div>`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2315,13 +2315,60 @@
|
|||
}
|
||||
|
||||
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
||||
setStatus('Submitted to ABDM gateway.' + queueText);
|
||||
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
||||
setStatus('Submitted to ABDM gateway.' + queueText + warningText);
|
||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||
if (badge) {
|
||||
badge.textContent = 'IN_PROGRESS';
|
||||
badge.className = 'badge bg-info status-pill';
|
||||
}
|
||||
alert('Submitted to ABDM gateway.' + queueText);
|
||||
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
||||
if (res.queue_id) {
|
||||
var logUrl = '<?= base_url('AbdmBridgeLog') ?>' + '?search=' + encodeURIComponent(res.queue_id);
|
||||
window.open(logUrl, '_blank');
|
||||
}
|
||||
fhirPreviewModal.hide();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (taskType === 'ipd_discharge_publish') {
|
||||
var ipdId = parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) || 0;
|
||||
var ipdPatientId = parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) || 0;
|
||||
if (ipdId <= 0 || ipdPatientId <= 0) {
|
||||
alert('Task data missing (ipd_id/patient_id).');
|
||||
return;
|
||||
}
|
||||
|
||||
var origHtmlIpd = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting…';
|
||||
|
||||
post('<?= base_url('AbdmGateway/share_ipd_discharge_bundle') ?>', {
|
||||
ipd_id: ipdId,
|
||||
patient_id: ipdPatientId,
|
||||
abha_id: _fhirSubmitAbha,
|
||||
consent_handle: ''
|
||||
}, function (res) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtmlIpd;
|
||||
|
||||
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
||||
var err = (res && (res.error || res.error_text || res.message)) ? (res.error || res.error_text || res.message) : 'Gateway submit failed';
|
||||
setStatus(err, true);
|
||||
alert('Submission failed:\n' + err);
|
||||
return;
|
||||
}
|
||||
|
||||
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
||||
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
||||
setStatus('Submitted to ABDM gateway.' + queueText + warningText);
|
||||
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
||||
if (badge) {
|
||||
badge.textContent = 'IN_PROGRESS';
|
||||
badge.className = 'badge bg-info status-pill';
|
||||
}
|
||||
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
||||
if (res.queue_id) {
|
||||
var logUrl = '<?= base_url('AbdmBridgeLog') ?>' + '?search=' + encodeURIComponent(res.queue_id);
|
||||
window.open(logUrl, '_blank');
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ $(function() {
|
|||
var $clearHistory = $('#clear_history');
|
||||
var historyList = [];
|
||||
var currentIndex = -1;
|
||||
var cameraCleanupRequested = false;
|
||||
var modalEl = document.getElementById('capturePreviewModal');
|
||||
var modalInstance = modalEl ? new bootstrap.Modal(modalEl) : null;
|
||||
var editModalEl = document.getElementById('profileEditModal');
|
||||
|
|
@ -464,6 +465,7 @@ $(function() {
|
|||
return;
|
||||
}
|
||||
|
||||
cameraCleanupRequested = false;
|
||||
if (stream) {
|
||||
stopCamera();
|
||||
}
|
||||
|
|
@ -471,6 +473,10 @@ $(function() {
|
|||
var preferredDeviceId = typeof deviceId === 'string' ? deviceId : ($cameraSelect.val() || selectedDeviceId || '');
|
||||
|
||||
function attachStream(s, persistedDeviceId) {
|
||||
if (cameraCleanupRequested) {
|
||||
s.getTracks().forEach(function(track) { track.stop(); });
|
||||
return;
|
||||
}
|
||||
stream = s;
|
||||
video.srcObject = s;
|
||||
video.onloadedmetadata = function() {
|
||||
|
|
@ -522,8 +528,13 @@ $(function() {
|
|||
$cameraResolution.text('Camera: stopped');
|
||||
}
|
||||
|
||||
var previousPageCleanup = window.pageCleanup;
|
||||
window.pageCleanup = function() {
|
||||
cameraCleanupRequested = true;
|
||||
stopCamera();
|
||||
if (typeof previousPageCleanup === 'function') {
|
||||
previousPageCleanup();
|
||||
}
|
||||
};
|
||||
|
||||
$startBtn.on('click', startCamera);
|
||||
|
|
@ -771,7 +782,8 @@ $(function() {
|
|||
}
|
||||
});
|
||||
|
||||
$(window).on('beforeunload', function() {
|
||||
$(window).off('beforeunload.profilecamera pagehide.profilecamera').on('beforeunload.profilecamera pagehide.profilecamera', function() {
|
||||
cameraCleanupRequested = true;
|
||||
stopCamera();
|
||||
});
|
||||
|
||||
|
|
@ -786,6 +798,7 @@ $(function() {
|
|||
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
cameraCleanupRequested = true;
|
||||
stopCamera();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -534,6 +534,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12" style="margin: 10px;">
|
||||
<!-- Active method banner -->
|
||||
<div id="abhareg_active_banner" class="alert alert-primary d-flex align-items-center py-2 d-none mb-3">
|
||||
<span id="abhareg_banner_icon" class="me-2 fs-5"></span>
|
||||
|
|
@ -623,23 +627,41 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panel: Method 3 - Facility QR (informational) -->
|
||||
<!-- Panel: Method 3 - Facility QR -->
|
||||
<div id="abhareg_panel_facility" class="abhareg-panel d-none">
|
||||
<div class="alert alert-info">
|
||||
<h6 class="fw-bold"><i class="bi bi-qr-code me-2"></i>Scan & Share — Health Facility QR</h6>
|
||||
<p class="mb-2">In this method, the <strong>patient</strong> scans the hospital's facility QR code using their ABHA / PHR app.</p>
|
||||
<ol class="mb-2">
|
||||
<li>Patient opens their <strong>ABHA app</strong> (or PHR app)</li>
|
||||
<li>Selects "Scan Facility QR"</li>
|
||||
<li>Scans the hospital's QR code displayed at the OPD counter</li>
|
||||
<li>Grants consent — profile is sent to the HMS automatically</li>
|
||||
</ol>
|
||||
<p class="mb-0 text-muted small">The hospital's facility QR code is provided by NHA/ABDM based on your Health Facility Registry (HFR) ID.</p>
|
||||
</div>
|
||||
<div class="text-center mt-3">
|
||||
<a href="https://abha.abdm.gov.in/abha/v3/" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>Open ABHA Portal
|
||||
</a>
|
||||
<div id="abhareg_facility_alert"></div>
|
||||
<div class="row g-3 align-items-start">
|
||||
<div class="col-md-4 text-center">
|
||||
<div class="border rounded bg-white p-3 d-inline-block shadow-sm" style="min-width:260px;min-height:260px">
|
||||
<div id="abhareg_facility_loading" class="text-muted small py-5">
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>Loading facility QR...
|
||||
</div>
|
||||
<img id="abhareg_facility_qr_img" src="" alt="Health Facility QR" class="img-fluid d-none" style="max-width:230px">
|
||||
</div>
|
||||
<div class="mt-2 d-flex justify-content-center gap-2 flex-wrap">
|
||||
<button type="button" id="abhareg_facility_refresh_btn" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Refresh QR
|
||||
</button>
|
||||
<button type="button" id="abhareg_facility_print_btn" class="btn btn-primary btn-sm d-none">
|
||||
<i class="bi bi-printer me-1"></i>Print QR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="alert alert-info mb-2">
|
||||
<h6 class="fw-bold"><i class="bi bi-qr-code me-2"></i>Scan & Share — Health Facility QR</h6>
|
||||
<p class="mb-2">Display or print this QR at the OPD counter. The patient scans it using their ABHA / PHR app.</p>
|
||||
<ol class="mb-2">
|
||||
<li>Patient opens their <strong>ABHA app</strong> (or PHR app)</li>
|
||||
<li>Selects "Scan Facility QR"</li>
|
||||
<li>Scans this hospital QR code</li>
|
||||
<li>Grants consent — profile appears in the ABDM OPD Queue automatically</li>
|
||||
</ol>
|
||||
<div class="small mb-1"><strong>Facility:</strong> <span id="abhareg_facility_name">—</span></div>
|
||||
<div class="small"><strong>HFR ID:</strong> <span id="abhareg_facility_hfr">—</span></div>
|
||||
</div>
|
||||
<div class="small text-muted">Source: ABDM Bridge <code>GET /api/v3/facility/qr-code</code>.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1045,6 +1067,8 @@
|
|||
var qrScriptLoading = false;
|
||||
var qrCameraStorageKey = 'abha_qr_preferred_camera';
|
||||
var qrSelectedCamera = '';
|
||||
var facilityQrLoaded = false;
|
||||
var qrCleanupRequested = false;
|
||||
var csrf = function() { return $('input[name="<?= csrf_token() ?>"]').first().val(); };
|
||||
|
||||
try {
|
||||
|
|
@ -1074,6 +1098,7 @@
|
|||
$('#abhareg_result').addClass('d-none');
|
||||
if (method === 'qr') { initQrScanner(); }
|
||||
else { stopQrScanner(); }
|
||||
if (method === 'facility') { loadFacilityQr(false); }
|
||||
};
|
||||
|
||||
window.abhaRegReset = function() {
|
||||
|
|
@ -1094,6 +1119,76 @@
|
|||
$('#abhareg_mob_step1_alert,#abhareg_mob_step2_alert').html('');
|
||||
};
|
||||
|
||||
function loadFacilityQr(force) {
|
||||
if (facilityQrLoaded && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('#abhareg_facility_alert').html('');
|
||||
$('#abhareg_facility_loading').removeClass('d-none').html('<span class="spinner-border spinner-border-sm me-1"></span>Loading facility QR...');
|
||||
$('#abhareg_facility_qr_img,#abhareg_facility_print_btn').addClass('d-none');
|
||||
$('#abhareg_facility_refresh_btn').prop('disabled', true);
|
||||
|
||||
$.getJSON('<?= base_url('abha/register/facility_qr') ?>', function(resp) {
|
||||
if (!resp || resp.ok != 1 || !resp.qr_data) {
|
||||
var msg = (resp && resp.error_text) ? resp.error_text : 'Unable to load Health Facility QR.';
|
||||
$('#abhareg_facility_loading').html('');
|
||||
$('#abhareg_facility_alert').html('<div class="alert alert-danger py-2">' + msg + '</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
var qrData = String(resp.qr_data || '');
|
||||
if (qrData && qrData.indexOf('data:image') !== 0) {
|
||||
qrData = 'data:image/png;base64,' + qrData;
|
||||
}
|
||||
|
||||
facilityQrLoaded = true;
|
||||
$('#abhareg_facility_qr_img').attr('src', qrData).removeClass('d-none');
|
||||
$('#abhareg_facility_loading').addClass('d-none');
|
||||
$('#abhareg_facility_print_btn').removeClass('d-none');
|
||||
$('#abhareg_facility_name').text(resp.hospital_name || '—');
|
||||
$('#abhareg_facility_hfr').text(resp.hfr_id || '—');
|
||||
$('#abhareg_facility_qr_img').data('hospitalName', resp.hospital_name || 'Health Facility');
|
||||
$('#abhareg_facility_qr_img').data('hfrId', resp.hfr_id || '');
|
||||
}).fail(function(xhr) {
|
||||
var msg = 'Unable to load Health Facility QR.';
|
||||
if (xhr && xhr.responseJSON && xhr.responseJSON.error_text) {
|
||||
msg = xhr.responseJSON.error_text;
|
||||
}
|
||||
$('#abhareg_facility_loading').html('');
|
||||
$('#abhareg_facility_alert').html('<div class="alert alert-danger py-2">' + msg + '</div>');
|
||||
}).always(function() {
|
||||
$('#abhareg_facility_refresh_btn').prop('disabled', false);
|
||||
});
|
||||
}
|
||||
|
||||
function printFacilityQr() {
|
||||
var qrSrc = $('#abhareg_facility_qr_img').attr('src') || '';
|
||||
if (!qrSrc) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hospitalName = $('#abhareg_facility_qr_img').data('hospitalName') || 'Health Facility';
|
||||
var hfrId = $('#abhareg_facility_qr_img').data('hfrId') || '';
|
||||
var safeHospitalName = $('<div>').text(hospitalName).html();
|
||||
var safeHfrId = $('<div>').text(hfrId).html();
|
||||
var printWindow = window.open('', '_blank', 'width=520,height=680');
|
||||
if (!printWindow) {
|
||||
alert('Please allow pop-ups to print the facility QR.');
|
||||
return;
|
||||
}
|
||||
|
||||
printWindow.document.write('<!doctype html><html><head><title>Health Facility QR</title>'
|
||||
+ '<style>body{font-family:Arial,sans-serif;text-align:center;padding:28px;color:#111}h2{margin:0 0 8px;font-size:22px}.muted{color:#555;margin-bottom:18px}.qr{width:320px;max-width:90%;border:1px solid #ddd;padding:14px}.hint{margin-top:18px;font-size:14px;line-height:1.5}@media print{button{display:none}}</style>'
|
||||
+ '</head><body><h2>' + safeHospitalName + '</h2>'
|
||||
+ '<div class="muted">Health Facility QR' + (safeHfrId ? ' | HFR ID: ' + safeHfrId : '') + '</div>'
|
||||
+ '<img class="qr" src="' + qrSrc + '" alt="Health Facility QR">'
|
||||
+ '<div class="hint">Scan this QR using ABHA / PHR app for Scan & Share OPD registration.</div>'
|
||||
+ '<p><button onclick="window.print()">Print</button></p></body></html>');
|
||||
printWindow.document.close();
|
||||
printWindow.focus();
|
||||
}
|
||||
|
||||
function regAlert(id, type, msg) {
|
||||
$('#' + id).html(msg ? '<div class="alert alert-' + type + ' py-2">' + msg + '</div>' : '');
|
||||
}
|
||||
|
|
@ -1172,6 +1267,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
qrCleanupRequested = false;
|
||||
var selectedCameraId = typeof cameraId === 'string' ? cameraId : ($('#abhareg_qr_camera_select').val() || qrSelectedCamera || '');
|
||||
|
||||
if (!html5QrCode) {
|
||||
|
|
@ -1203,6 +1299,10 @@
|
|||
},
|
||||
function() {}
|
||||
).then(function() {
|
||||
if (qrCleanupRequested) {
|
||||
stopQrScanner();
|
||||
return;
|
||||
}
|
||||
saveQrSelectedCamera(selectedCameraId);
|
||||
}).catch(function(err) {
|
||||
if (selectedCameraId) {
|
||||
|
|
@ -1402,6 +1502,26 @@
|
|||
});
|
||||
}
|
||||
|
||||
function cleanupQrScanner() {
|
||||
qrCleanupRequested = true;
|
||||
stopQrScanner();
|
||||
}
|
||||
|
||||
var previousPageCleanup = window.pageCleanup;
|
||||
window.pageCleanup = function() {
|
||||
cleanupQrScanner();
|
||||
if (typeof previousPageCleanup === 'function') {
|
||||
previousPageCleanup();
|
||||
}
|
||||
};
|
||||
|
||||
$(window).off('beforeunload.abhaqr pagehide.abhaqr').on('beforeunload.abhaqr pagehide.abhaqr', cleanupQrScanner);
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
cleanupQrScanner();
|
||||
}
|
||||
});
|
||||
|
||||
$('#abhareg_qr_stop_btn').on('click', function() { stopQrScanner(); });
|
||||
$('#abhareg_qr_camera_select').on('change', function() {
|
||||
var cameraId = ($(this).val() || '').toString();
|
||||
|
|
@ -1416,6 +1536,8 @@
|
|||
initQrScanner(qrSelectedCamera || '');
|
||||
});
|
||||
});
|
||||
$('#abhareg_facility_refresh_btn').on('click', function() { loadFacilityQr(true); });
|
||||
$('#abhareg_facility_print_btn').on('click', function() { printFacilityQr(); });
|
||||
|
||||
// Enter key on OTP fields
|
||||
$('#abhareg_num_otp_input').on('keypress', function(e) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ $abhaNumber = trim((string) (($person->abha_id ?? '') ?: ($person->abha_no ?? ''
|
|||
$abhaAddress = trim((string) ($person->abha_address ?? ''));
|
||||
$abhaVerifiedRaw = strtolower(trim((string) (($person->abha_verified_status ?? '') ?: ($person->abha_status ?? '') ?: ($person->abha_verified ?? ''))));
|
||||
$abhaLooksVerified = in_array($abhaVerifiedRaw, ['1', 'verified', 'yes', 'y', 'true'], true);
|
||||
$canUseM3 = $patientId > 0 && $abhaAddress !== '';
|
||||
$canUseM3 = $patientId > 0 && $abhaAddress !== '' && $abhaLooksVerified;
|
||||
?>
|
||||
|
||||
<section class="content">
|
||||
|
|
@ -58,7 +58,13 @@ $canUseM3 = $patientId > 0 && $abhaAddress !== '';
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small text-muted mb-2" id="ayushmanAbdmStatus">
|
||||
<?= $canUseM3 ? 'Use Fetch From ABDM M3 to request consent and poll approved records, or load already fetched documents.' : 'ABHA address is required before M3 fetch can be started.' ?>
|
||||
<?php if ($canUseM3) : ?>
|
||||
Use Fetch From ABDM M3 to request consent and poll approved records, or load already fetched documents.
|
||||
<?php elseif ($abhaAddress === '') : ?>
|
||||
ABHA address is required before M3 fetch can be started.
|
||||
<?php else : ?>
|
||||
Verified ABHA status is required before M3 fetch can be started.
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-5">
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
var latestFileId = 0;
|
||||
var cameraStorageKey = 'opd_scan_preferred_camera';
|
||||
var selectedDeviceId = '';
|
||||
var cameraCleanupRequested = false;
|
||||
var editState = {
|
||||
originalDataUrl: '',
|
||||
currentDataUrl: '',
|
||||
|
|
@ -417,9 +418,14 @@
|
|||
return;
|
||||
}
|
||||
|
||||
cameraCleanupRequested = false;
|
||||
stopCamera();
|
||||
|
||||
function attachStream(stream) {
|
||||
if (cameraCleanupRequested) {
|
||||
stream.getTracks().forEach(function(track) { track.stop(); });
|
||||
return;
|
||||
}
|
||||
streamRef = stream;
|
||||
var video = $video.get(0);
|
||||
video.srcObject = stream;
|
||||
|
|
@ -486,9 +492,22 @@
|
|||
streamRef.getTracks().forEach(function(track) { track.stop(); });
|
||||
streamRef = null;
|
||||
}
|
||||
var video = $video.get(0);
|
||||
if (video) {
|
||||
try {
|
||||
video.pause();
|
||||
} catch (e) {
|
||||
}
|
||||
video.srcObject = null;
|
||||
}
|
||||
$resolution.text('Camera: stopped');
|
||||
}
|
||||
|
||||
function cleanupCamera() {
|
||||
cameraCleanupRequested = true;
|
||||
stopCamera();
|
||||
}
|
||||
|
||||
function loadLastList() {
|
||||
// Refresh latest uploaded/captured files for this OPD.
|
||||
if (opdid <= 0) {
|
||||
|
|
@ -774,8 +793,23 @@
|
|||
startCamera(selectedDeviceId);
|
||||
loadLastList();
|
||||
|
||||
var previousPageCleanup = window.pageCleanup;
|
||||
window.pageCleanup = function() {
|
||||
cleanupCamera();
|
||||
if (typeof previousPageCleanup === 'function') {
|
||||
previousPageCleanup();
|
||||
}
|
||||
};
|
||||
|
||||
$(window).off('beforeunload.opdscan pagehide.opdscan').on('beforeunload.opdscan pagehide.opdscan', cleanupCamera);
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
cleanupCamera();
|
||||
}
|
||||
});
|
||||
|
||||
$('#tallModal').off('hidden.bs.modal.opdscan').on('hidden.bs.modal.opdscan', function() {
|
||||
stopCamera();
|
||||
cleanupCamera();
|
||||
$('#opd_scan_results').html('Captured image will appear here.');
|
||||
$resolution.text('Camera: closed');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -609,7 +609,9 @@ $editReason = trim((string) ($edit_reason ?? ''));
|
|||
var chEl = document.getElementById('diag_abdm_consent_handle');
|
||||
if (chEl && !chEl.value) { chEl.value = resp.consent_handle; }
|
||||
}
|
||||
setStatus(resp && resp.status ? ('Queued: ' + resp.status) : (resp && resp.message ? resp.message : 'Share request sent.'), !(resp && (resp.ok || resp.queue_id)));
|
||||
var msg = resp && resp.status ? ('Queued: ' + resp.status) : (resp && resp.message ? resp.message : 'Share request sent.');
|
||||
if (resp && resp.warning) { msg += ' Warning: ' + resp.warning; }
|
||||
setStatus(msg, !(resp && (resp.ok || resp.queue_id)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user