Add ABDM documents and patient profile sync UI
This commit is contained in:
parent
0e8e164335
commit
2b53c05a68
|
|
@ -496,6 +496,11 @@ $routes->group('billing', function($routes) {
|
|||
$routes->get('patient/show_profile_image/(:num)/(:num)', 'Patient::show_profile_image/$1/$2');
|
||||
$routes->get('patient/show_profile_opd/(:num)', 'Patient::show_profile_opd/$1');
|
||||
$routes->get('patient/show_profile_opd/(:num)/(:num)', 'Patient::show_profile_opd/$1/$2');
|
||||
$routes->get('patient/abdm_documents/(:num)', 'Patient::abdm_documents/$1');
|
||||
$routes->get('patient/abdm_document_detail/(:num)/(:num)', 'Patient::abdm_document_detail/$1/$2');
|
||||
$routes->get('patient/abdm_content_request/(:num)', 'Patient::abdm_content_request/$1');
|
||||
$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->get('patient/city', 'Patient::city');
|
||||
|
|
@ -632,13 +637,14 @@ $routes->post('AbdmGateway/consent_revoked_callback', 'AbdmGateway::consentRevok
|
|||
$routes->post('AbdmGateway/record_linked_callback', 'AbdmGateway::recordLinkedCallback');
|
||||
// ABDM M3 HIU (HMS -> Gateway)
|
||||
$routes->get('AbdmHiu', 'AbdmHiu::index', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmHiu/documents', 'AbdmHiu::documents', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmHiu/patient_lookup', 'AbdmHiu::patientLookup', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmHiu/documents_list', 'AbdmHiu::documentsList', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmHiu/document_detail/(:num)', 'AbdmHiu::documentDetail/$1', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmHiu/timeline', 'AbdmHiu::timeline', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmHiu/poll_summary', 'AbdmHiu::pollSummary', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmHiu/consent_request', 'AbdmHiu::consentRequest', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmHiu/consent_request_status', 'AbdmHiu::consentRequestStatus', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmHiu/consent_request_fetch', 'AbdmHiu::consentRequestFetch', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmHiu/health_information_request', 'AbdmHiu::healthInformationRequest', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmHiu/consent_reconcile', 'AbdmHiu::consentReconcile', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmHiu/data_fetch', 'AbdmHiu::dataFetch', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmHiu/consent_update_callback', 'AbdmHiu::consentUpdateWebhook');
|
||||
|
|
@ -646,13 +652,11 @@ $routes->post('AbdmHiu/health_information_on_request_callback', 'AbdmHiu::health
|
|||
$routes->post('AbdmHiu/health_information_data_push_callback', 'AbdmHiu::healthInformationDataPushWebhook');
|
||||
$routes->post('api/v3/hiu/consent/request', 'AbdmHiu::consentRequest', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('api/v3/hiu/consent/request/status', 'AbdmHiu::consentRequestStatus', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('api/v3/hiu/consent/request/fetch', 'AbdmHiu::consentRequestFetch', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('api/v3/hiu/consent/on-update', 'AbdmHiu::consentUpdateWebhook');
|
||||
$routes->post('api/v3/hiu/consent/on_update', 'AbdmHiu::consentUpdateWebhook');
|
||||
$routes->post('api/v3/hiu/health-information/on-request', 'AbdmHiu::healthInformationOnRequestWebhook');
|
||||
$routes->post('api/v3/hiu/health-information/on_request', 'AbdmHiu::healthInformationOnRequestWebhook');
|
||||
$routes->post('api/v3/hiu/data/push', 'AbdmHiu::healthInformationDataPushWebhook');
|
||||
$routes->post('api/v3/hiu/health-information/request', 'AbdmHiu::healthInformationRequest', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('api/v1/hiu/consent/status', 'AbdmHiu::consentReconcile', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('api/v1/hiu/data/fetch', 'AbdmHiu::dataFetch', ['filter' => $abdmPermFilter]);
|
||||
// HMS M2 adapter endpoints (Gateway -> HMS) - keep lowercase v1 path
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\Abdm\M3HiuWorkflowService;
|
||||
use App\Libraries\Abdm\M3HiuDocumentRepository;
|
||||
|
||||
class AbdmHiu extends BaseController
|
||||
{
|
||||
private M3HiuWorkflowService $service;
|
||||
private M3HiuDocumentRepository $documentRepository;
|
||||
|
||||
public function initController(
|
||||
\CodeIgniter\HTTP\RequestInterface $request,
|
||||
|
|
@ -16,6 +18,7 @@ class AbdmHiu extends BaseController
|
|||
parent::initController($request, $response, $logger);
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->service = new M3HiuWorkflowService();
|
||||
$this->documentRepository = new M3HiuDocumentRepository();
|
||||
}
|
||||
|
||||
public function index()
|
||||
|
|
@ -23,6 +26,48 @@ class AbdmHiu extends BaseController
|
|||
return view('abdm/hiu_m3');
|
||||
}
|
||||
|
||||
public function documents()
|
||||
{
|
||||
return view('abdm/hiu_documents');
|
||||
}
|
||||
|
||||
public function documentsList()
|
||||
{
|
||||
$filters = [
|
||||
'patient_id' => (int) ($this->request->getGet('patient_id') ?? 0),
|
||||
'abha_address' => trim((string) ($this->request->getGet('abha_address') ?? '')),
|
||||
'q' => trim((string) ($this->request->getGet('q') ?? '')),
|
||||
];
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 100);
|
||||
if ($limit <= 0 || $limit > 300) {
|
||||
$limit = 100;
|
||||
}
|
||||
|
||||
$rows = $this->documentRepository->listDocuments($filters, $limit);
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'count' => count($rows),
|
||||
'items' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
public function documentDetail($id = null)
|
||||
{
|
||||
$docId = (int) ($id ?? 0);
|
||||
$row = $this->documentRepository->getDocument($docId);
|
||||
if (empty($row)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Document not found',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'item' => $row,
|
||||
]);
|
||||
}
|
||||
|
||||
public function consentRequest()
|
||||
{
|
||||
return $this->handleOperation('consent_request', 'REQUESTED');
|
||||
|
|
@ -33,16 +78,6 @@ class AbdmHiu extends BaseController
|
|||
return $this->handleOperation('consent_status', 'STATUS_CHECKED');
|
||||
}
|
||||
|
||||
public function consentRequestFetch()
|
||||
{
|
||||
return $this->handleOperation('consent_fetch', 'CONSENT_FETCHED');
|
||||
}
|
||||
|
||||
public function healthInformationRequest()
|
||||
{
|
||||
return $this->handleOperation('hi_request', 'DATA_REQUESTED');
|
||||
}
|
||||
|
||||
public function consentReconcile()
|
||||
{
|
||||
return $this->handleOperation('consent_reconcile', 'STATUS_CHECKED');
|
||||
|
|
|
|||
|
|
@ -1942,6 +1942,10 @@ class Ipd_discharge extends BaseController
|
|||
|
||||
// Decode HTML entities that might have been double-encoded
|
||||
$value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
|
||||
if ($this->looksLikeMalformedDischargeHtml($value)) {
|
||||
$value = $this->normalizeMalformedDischargeHtmlForMpdf($value);
|
||||
}
|
||||
|
||||
return trim($value);
|
||||
}
|
||||
|
|
@ -1972,6 +1976,10 @@ class Ipd_discharge extends BaseController
|
|||
// Check if content contains HTML tags (from HTML editor)
|
||||
if (preg_match('/<(?:p|div|span|strong|b|i|em|ul|ol|li|table|tr|td|th|thead|tbody|h[1-6])\b[^>]*>/i', $text)) {
|
||||
// Content is HTML - return as-is (already sanitized by normalizeRichText)
|
||||
if ($this->looksLikeMalformedDischargeHtml($text)) {
|
||||
$text = $this->normalizeMalformedDischargeHtmlForMpdf($text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1109,6 +1109,452 @@ class Patient extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
public function abdm_documents(int $pno)
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'patient_master table not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $pno)->get()->getRowArray();
|
||||
if (! is_array($patientRow)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Patient not found',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists('abdm_hiu_documents')) {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'count' => 0,
|
||||
'items' => [],
|
||||
'abha' => $this->buildAbhaProfileContext($patientRow),
|
||||
]);
|
||||
}
|
||||
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 100);
|
||||
if ($limit <= 0 || $limit > 300) {
|
||||
$limit = 100;
|
||||
}
|
||||
|
||||
$q = trim((string) ($this->request->getGet('q') ?? ''));
|
||||
$abhaContext = $this->buildAbhaProfileContext($patientRow);
|
||||
|
||||
$builder = $this->db->table('abdm_hiu_documents d')
|
||||
->select('d.id, d.patient_id, d.abha_address, d.document_title, d.document_date, d.care_context_reference, d.practitioner_name, d.organization_name, d.bundle_type, d.created_at')
|
||||
->orderBy('d.document_date', 'DESC')
|
||||
->orderBy('d.id', 'DESC')
|
||||
->limit($limit);
|
||||
|
||||
$builder->groupStart()
|
||||
->where('d.patient_id', $pno);
|
||||
if ($abhaContext['abha_address'] !== '') {
|
||||
$builder->orWhere('d.abha_address', $abhaContext['abha_address']);
|
||||
}
|
||||
$builder->groupEnd();
|
||||
|
||||
if ($q !== '') {
|
||||
$builder->groupStart()
|
||||
->like('d.document_title', $q)
|
||||
->orLike('d.care_context_reference', $q)
|
||||
->orLike('d.practitioner_name', $q)
|
||||
->orLike('d.organization_name', $q)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$rows = $builder->get()->getResultArray();
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'count' => count($rows),
|
||||
'items' => $rows,
|
||||
'abha' => $abhaContext,
|
||||
]);
|
||||
}
|
||||
|
||||
public function abdm_document_detail(int $pno, int $docId)
|
||||
{
|
||||
if ($docId <= 0 || ! $this->db->tableExists('abdm_hiu_documents')) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Document not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $pno)->get()->getRowArray();
|
||||
if (! is_array($patientRow)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Patient not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$abhaContext = $this->buildAbhaProfileContext($patientRow);
|
||||
|
||||
$builder = $this->db->table('abdm_hiu_documents d')
|
||||
->select('*')
|
||||
->where('d.id', $docId)
|
||||
->groupStart()
|
||||
->where('d.patient_id', $pno);
|
||||
if ($abhaContext['abha_address'] !== '') {
|
||||
$builder->orWhere('d.abha_address', $abhaContext['abha_address']);
|
||||
}
|
||||
$builder->groupEnd();
|
||||
|
||||
$row = $builder->get(1)->getRowArray();
|
||||
if (! is_array($row) || empty($row)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Document not found for patient',
|
||||
]);
|
||||
}
|
||||
|
||||
$row['summary'] = json_decode((string) ($row['summary_json'] ?? ''), true) ?: [];
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'item' => $row,
|
||||
]);
|
||||
}
|
||||
|
||||
public function abdm_content_request(int $pno)
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'patient_master table not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $pno)->get()->getRowArray();
|
||||
if (! is_array($patientRow)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Patient not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$abhaContext = $this->buildAbhaProfileContext($patientRow);
|
||||
if ($abhaContext['abha_address'] === '') {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'ABHA Address not available for this patient.',
|
||||
'abha' => $abhaContext,
|
||||
]);
|
||||
}
|
||||
|
||||
if ((int) ($abhaContext['is_verified'] ?? 0) !== 1) {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'ABHA is not marked as verified in HMS. Please verify ABHA first, then request content.',
|
||||
'abha' => $abhaContext,
|
||||
]);
|
||||
}
|
||||
|
||||
$service = new \App\Libraries\Abdm\M3HiuWorkflowService();
|
||||
$payload = [
|
||||
'patient_id' => $pno,
|
||||
'abha_address' => $abhaContext['abha_address'],
|
||||
'hiTypes' => ['OPConsultation', 'Prescription', 'DiagnosticReport', 'HealthDocument'],
|
||||
];
|
||||
|
||||
try {
|
||||
$result = $service->runOperation('consent_request', $payload);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Unable to start content request: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$ok = (int) ($result['ok'] ?? 0) === 1;
|
||||
$httpCode = (int) ($result['http_code'] ?? ($ok ? 200 : 422));
|
||||
if ($httpCode < 100 || $httpCode > 599) {
|
||||
$httpCode = $ok ? 200 : 422;
|
||||
}
|
||||
|
||||
if (! $ok) {
|
||||
return $this->response->setStatusCode($httpCode)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => (string) ($result['error_text'] ?? 'Content request failed.'),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode($httpCode)->setJSON([
|
||||
'ok' => 1,
|
||||
'message' => 'Consent/content request created successfully.',
|
||||
'workflow_state' => 'REQUESTED',
|
||||
'request_id' => (string) ($result['request_id'] ?? ''),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function abdm_content_auto_flow(int $pno)
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'patient_master table not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $pno)->get()->getRowArray();
|
||||
if (! is_array($patientRow)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Patient not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$abhaContext = $this->buildAbhaProfileContext($patientRow);
|
||||
if ($abhaContext['abha_address'] === '') {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'ABHA Address not available for this patient.',
|
||||
'abha' => $abhaContext,
|
||||
]);
|
||||
}
|
||||
|
||||
if ((int) ($abhaContext['is_verified'] ?? 0) !== 1) {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'ABHA is not marked as verified in HMS. Please verify ABHA first, then request content.',
|
||||
'abha' => $abhaContext,
|
||||
]);
|
||||
}
|
||||
|
||||
$service = new \App\Libraries\Abdm\M3HiuWorkflowService();
|
||||
$flowRefId = trim((string) (
|
||||
$this->request->getGet('request_id')
|
||||
?? $this->request->getPost('request_id')
|
||||
?? ''
|
||||
));
|
||||
$consentRequestRef = '';
|
||||
$consentArtifactRef = '';
|
||||
|
||||
$consentResult = null;
|
||||
$reconcileResult = null;
|
||||
$fetchResult = null;
|
||||
|
||||
if ($flowRefId === '') {
|
||||
$consentPayload = [
|
||||
'patient_id' => $pno,
|
||||
'abha_address' => $abhaContext['abha_address'],
|
||||
'hiTypes' => ['OPConsultation', 'Prescription', 'DiagnosticReport', 'HealthDocument'],
|
||||
];
|
||||
|
||||
try {
|
||||
$consentResult = $service->runOperation('consent_request', $consentPayload);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Unable to start consent request: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ((int) ($consentResult['ok'] ?? 0) !== 1) {
|
||||
$httpCode = (int) ($consentResult['http_code'] ?? 422);
|
||||
if ($httpCode < 100 || $httpCode > 599) {
|
||||
$httpCode = 422;
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode($httpCode)->setJSON([
|
||||
'ok' => 0,
|
||||
'phase' => 'REQUEST_FAILED',
|
||||
'error' => (string) ($consentResult['error_text'] ?? 'Consent request failed.'),
|
||||
'request_id' => (string) ($consentResult['request_id'] ?? ''),
|
||||
'data' => $consentResult,
|
||||
]);
|
||||
}
|
||||
|
||||
$flowRefId = trim((string) (
|
||||
$consentResult['consent_request_id']
|
||||
?? $consentResult['abdm_consent_request_id']
|
||||
?? $consentResult['consent_id']
|
||||
?? $consentResult['request_id']
|
||||
?? ''
|
||||
));
|
||||
$consentRequestRef = trim((string) (
|
||||
$consentResult['consent_request_id']
|
||||
?? $consentResult['abdm_consent_request_id']
|
||||
?? ''
|
||||
));
|
||||
if ($consentRequestRef === '') {
|
||||
$consentRequestRef = $flowRefId;
|
||||
}
|
||||
$consentArtifactRef = trim((string) (
|
||||
$consentResult['consent_id']
|
||||
?? $consentResult['abdm_consent_artifact_id']
|
||||
?? ''
|
||||
));
|
||||
if ($consentArtifactRef === '' && $consentRequestRef !== '' && preg_match('/^REQ-/i', $consentRequestRef) !== 1) {
|
||||
$consentArtifactRef = $consentRequestRef;
|
||||
}
|
||||
}
|
||||
|
||||
if ($flowRefId === '') {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'phase' => 'REQUEST_MISSING',
|
||||
'error' => 'Unable to determine consent reference for auto flow.',
|
||||
]);
|
||||
}
|
||||
|
||||
$reconcilePayload = [
|
||||
'abha_address' => $abhaContext['abha_address'],
|
||||
];
|
||||
if ($consentRequestRef !== '') {
|
||||
$reconcilePayload['consentRequestId'] = $consentRequestRef;
|
||||
} else {
|
||||
$reconcilePayload['request_id'] = $flowRefId;
|
||||
}
|
||||
|
||||
try {
|
||||
$reconcileResult = $service->runOperation('consent_reconcile', $reconcilePayload);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'phase' => 'STATUS_FAILED',
|
||||
'error' => 'Unable to reconcile consent status: ' . $e->getMessage(),
|
||||
'request_id' => $flowRefId,
|
||||
]);
|
||||
}
|
||||
|
||||
if ((int) ($reconcileResult['ok'] ?? 0) !== 1) {
|
||||
$httpCode = (int) ($reconcileResult['http_code'] ?? 422);
|
||||
if ($httpCode < 100 || $httpCode > 599) {
|
||||
$httpCode = 422;
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode($httpCode)->setJSON([
|
||||
'ok' => 0,
|
||||
'phase' => 'STATUS_FAILED',
|
||||
'error' => (string) ($reconcileResult['error_text'] ?? 'Consent status check failed.'),
|
||||
'request_id' => $flowRefId,
|
||||
'data' => [
|
||||
'consent_request' => $consentResult,
|
||||
'consent_reconcile' => $reconcileResult,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$state = $this->deriveAutoFlowState($reconcileResult);
|
||||
$pollAgain = in_array($state, ['REQUESTED', 'PENDING'], true);
|
||||
|
||||
if ($state === 'GRANTED') {
|
||||
$fetchPayload = [
|
||||
'abha_address' => $abhaContext['abha_address'],
|
||||
];
|
||||
if ($consentArtifactRef !== '') {
|
||||
$fetchPayload['consentId'] = $consentArtifactRef;
|
||||
} elseif ($consentRequestRef !== '') {
|
||||
$fetchPayload['consentRequestId'] = $consentRequestRef;
|
||||
} else {
|
||||
$fetchPayload['request_id'] = $flowRefId;
|
||||
}
|
||||
try {
|
||||
$fetchResult = $service->runOperation('data_fetch', $fetchPayload);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'phase' => 'FETCH_FAILED',
|
||||
'error' => 'Unable to fetch ABDM content: ' . $e->getMessage(),
|
||||
'request_id' => $flowRefId,
|
||||
]);
|
||||
}
|
||||
|
||||
if ((int) ($fetchResult['ok'] ?? 0) !== 1) {
|
||||
$httpCode = (int) ($fetchResult['http_code'] ?? 422);
|
||||
if ($httpCode < 100 || $httpCode > 599) {
|
||||
$httpCode = 422;
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode($httpCode)->setJSON([
|
||||
'ok' => 0,
|
||||
'phase' => 'FETCH_FAILED',
|
||||
'error' => (string) ($fetchResult['error_text'] ?? 'Data fetch failed.'),
|
||||
'request_id' => $flowRefId,
|
||||
'data' => [
|
||||
'consent_request' => $consentResult,
|
||||
'consent_reconcile' => $reconcileResult,
|
||||
'data_fetch' => $fetchResult,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$state = 'COMPLETED';
|
||||
$pollAgain = false;
|
||||
}
|
||||
|
||||
$message = match ($state) {
|
||||
'COMPLETED' => 'Data fetch completed and documents should now be available.',
|
||||
'GRANTED' => 'Consent granted. Proceeding to fetch data.',
|
||||
'DENIED' => 'Consent denied/revoked/expired by patient.',
|
||||
default => 'Consent request created. Waiting for patient approval.',
|
||||
};
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'phase' => $state,
|
||||
'poll_again' => $pollAgain ? 1 : 0,
|
||||
'request_id' => $flowRefId,
|
||||
'message' => $message,
|
||||
'data' => [
|
||||
'consent_request' => $consentResult,
|
||||
'consent_reconcile' => $reconcileResult,
|
||||
'data_fetch' => $fetchResult,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function abdm_timeline(int $pno)
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'patient_master table not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $pno)->get()->getRowArray();
|
||||
if (! is_array($patientRow)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Patient not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$abhaContext = $this->buildAbhaProfileContext($patientRow);
|
||||
if ($abhaContext['abha_address'] === '') {
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'count' => 0,
|
||||
'items' => [],
|
||||
'note' => 'ABHA address not available for timeline filter.',
|
||||
]);
|
||||
}
|
||||
|
||||
$limit = (int) ($this->request->getGet('limit') ?? 15);
|
||||
if ($limit <= 0 || $limit > 100) {
|
||||
$limit = 15;
|
||||
}
|
||||
|
||||
$service = new \App\Libraries\Abdm\M3HiuWorkflowService();
|
||||
$rows = $service->listTimeline([
|
||||
'abha_address' => (string) $abhaContext['abha_address'],
|
||||
], $limit);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'count' => count($rows),
|
||||
'items' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
public function save_profile_image(int $pid)
|
||||
{
|
||||
$patient = $this->db->table('patient_master')->where('id', $pid)->get()->getRow();
|
||||
|
|
@ -2201,5 +2647,69 @@ class Patient extends BaseController
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildAbhaProfileContext(array $row): array
|
||||
{
|
||||
$abhaField = $this->resolvePatientAbhaIdField();
|
||||
$abhaIdRaw = $abhaField !== null ? trim((string) ($row[$abhaField] ?? '')) : '';
|
||||
$abhaAddress = trim((string) ($row['abha_address'] ?? ''));
|
||||
|
||||
if ($abhaAddress === '' && $abhaIdRaw !== '' && $this->isLikelyAbhaAddress($abhaIdRaw)) {
|
||||
$abhaAddress = $abhaIdRaw;
|
||||
}
|
||||
|
||||
if ($abhaAddress === '' && preg_match('/abha_address\s*:\s*([A-Za-z0-9._-]+@[A-Za-z0-9.-]+)/i', (string) ($row['log'] ?? ''), $m) === 1) {
|
||||
$abhaAddress = trim((string) ($m[1] ?? ''));
|
||||
}
|
||||
|
||||
$verifiedStatus = strtoupper(trim((string) ($row['abha_verified_status'] ?? '')));
|
||||
$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);
|
||||
if (! $hasStatusColumn && $abhaAddress !== '') {
|
||||
$isVerified = true;
|
||||
}
|
||||
|
||||
return [
|
||||
'abha_id' => $abhaIdRaw,
|
||||
'abha_address' => $abhaAddress,
|
||||
'verified_status' => $verifiedStatus,
|
||||
'is_verified' => $isVerified ? 1 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function deriveAutoFlowState(array $result): string
|
||||
{
|
||||
$blob = strtolower(json_encode($result, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '');
|
||||
|
||||
if (str_contains($blob, 'granted') || str_contains($blob, 'approved') || str_contains($blob, 'consent_granted')) {
|
||||
return 'GRANTED';
|
||||
}
|
||||
|
||||
if (str_contains($blob, 'denied') || str_contains($blob, 'revoked') || str_contains($blob, 'expired') || str_contains($blob, 'rejected')) {
|
||||
return 'DENIED';
|
||||
}
|
||||
|
||||
if (str_contains($blob, 'requested') || str_contains($blob, 'pending') || str_contains($blob, 'await')) {
|
||||
return 'PENDING';
|
||||
}
|
||||
|
||||
$explicit = strtoupper(trim((string) ($result['workflow_state'] ?? $result['state'] ?? $result['status'] ?? '')));
|
||||
if ($explicit !== '') {
|
||||
if (in_array($explicit, ['GRANTED', 'APPROVED', 'COMPLETED'], true)) {
|
||||
return 'GRANTED';
|
||||
}
|
||||
if (in_array($explicit, ['REVOKED', 'EXPIRED', 'DENIED', 'REJECTED'], true)) {
|
||||
return 'DENIED';
|
||||
}
|
||||
}
|
||||
|
||||
return 'REQUESTED';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateAbdmHiuDocuments extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'workflow_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'request_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 180,
|
||||
'null' => true,
|
||||
],
|
||||
'transaction_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 180,
|
||||
'null' => true,
|
||||
],
|
||||
'consent_ref' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 220,
|
||||
'null' => true,
|
||||
],
|
||||
'consent_artifact_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 180,
|
||||
'null' => true,
|
||||
],
|
||||
'abha_address' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 120,
|
||||
'null' => true,
|
||||
],
|
||||
'patient_id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
],
|
||||
'patient_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 190,
|
||||
'null' => true,
|
||||
],
|
||||
'care_context_reference' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 190,
|
||||
'null' => true,
|
||||
],
|
||||
'media' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'bundle_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 190,
|
||||
'null' => true,
|
||||
],
|
||||
'bundle_type' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 80,
|
||||
'null' => true,
|
||||
],
|
||||
'document_title' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'document_date' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'practitioner_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 190,
|
||||
'null' => true,
|
||||
],
|
||||
'organization_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 190,
|
||||
'null' => true,
|
||||
],
|
||||
'summary_json' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'raw_bundle' => [
|
||||
'type' => 'LONGTEXT',
|
||||
'null' => true,
|
||||
],
|
||||
'doc_hash' => [
|
||||
'type' => 'CHAR',
|
||||
'constraint' => 40,
|
||||
'null' => false,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'updated_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey('doc_hash', 'uniq_abdm_hiu_doc_hash');
|
||||
$this->forge->addKey(['patient_id', 'document_date'], false, false, 'idx_abdm_hiu_doc_patient_date');
|
||||
$this->forge->addKey(['abha_address', 'document_date'], false, false, 'idx_abdm_hiu_doc_abha_date');
|
||||
$this->forge->addKey(['request_id', 'created_at'], false, false, 'idx_abdm_hiu_doc_request');
|
||||
$this->forge->createTable('abdm_hiu_documents', true);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->forge->dropTable('abdm_hiu_documents', true);
|
||||
}
|
||||
}
|
||||
349
app/Libraries/Abdm/M3HiuDocumentRepository.php
Normal file
349
app/Libraries/Abdm/M3HiuDocumentRepository.php
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
<?php
|
||||
|
||||
namespace App\Libraries\Abdm;
|
||||
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class M3HiuDocumentRepository
|
||||
{
|
||||
private \CodeIgniter\Database\BaseConnection $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
}
|
||||
|
||||
public function persistFromDataFetch(array $payload, array $result, int $workflowId = 0): array
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_hiu_documents')) {
|
||||
return ['saved' => 0, 'updated' => 0, 'skipped' => 0];
|
||||
}
|
||||
|
||||
$sessions = $this->extractSessions($result);
|
||||
if (empty($sessions)) {
|
||||
return ['saved' => 0, 'updated' => 0, 'skipped' => 0];
|
||||
}
|
||||
|
||||
$requestId = trim((string) ($result['request_id'] ?? $payload['request_id'] ?? $payload['requestId'] ?? ''));
|
||||
$abhaAddress = trim((string) ($payload['abha_address'] ?? $result['abha_address'] ?? ''));
|
||||
|
||||
$saved = 0;
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
if (! is_array($session)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$transactionId = trim((string) ($session['transaction_id'] ?? ''));
|
||||
$consentRef = trim((string) ($session['consent_id'] ?? ''));
|
||||
$consentArtifactId = $this->extractConsentArtifactId($consentRef);
|
||||
$records = is_array($session['decrypted_data'] ?? null) ? (array) $session['decrypted_data'] : [];
|
||||
|
||||
foreach ($records as $record) {
|
||||
if (! is_array($record)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$careContextRef = trim((string) ($record['careContextReference'] ?? ''));
|
||||
$media = trim((string) ($record['media'] ?? ''));
|
||||
$bundle = $this->decodeBundle($record['decrypted_data'] ?? null);
|
||||
if (! is_array($bundle) || empty($bundle)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$summary = $this->buildSummary($bundle);
|
||||
$resolvedAbha = $abhaAddress !== '' ? $abhaAddress : trim((string) ($summary['abha_address'] ?? ''));
|
||||
$patientMap = $this->mapPatientByAbha($resolvedAbha, (string) ($summary['abha_number'] ?? ''));
|
||||
|
||||
$docHash = sha1(implode('|', [
|
||||
$requestId,
|
||||
$transactionId,
|
||||
$consentRef,
|
||||
$careContextRef,
|
||||
(string) ($summary['bundle_id'] ?? ''),
|
||||
]));
|
||||
|
||||
$row = [
|
||||
'workflow_id' => $workflowId > 0 ? $workflowId : null,
|
||||
'request_id' => $requestId !== '' ? $requestId : null,
|
||||
'transaction_id' => $transactionId !== '' ? $transactionId : null,
|
||||
'consent_ref' => $consentRef !== '' ? $consentRef : null,
|
||||
'consent_artifact_id' => $consentArtifactId !== '' ? $consentArtifactId : null,
|
||||
'abha_address' => $resolvedAbha !== '' ? $resolvedAbha : null,
|
||||
'patient_id' => (int) ($patientMap['patient_id'] ?? 0) > 0 ? (int) $patientMap['patient_id'] : null,
|
||||
'patient_name' => trim((string) ($summary['patient_name'] ?? '')) !== '' ? trim((string) ($summary['patient_name'] ?? '')) : null,
|
||||
'care_context_reference' => $careContextRef !== '' ? $careContextRef : null,
|
||||
'media' => $media !== '' ? $media : null,
|
||||
'bundle_id' => trim((string) ($summary['bundle_id'] ?? '')) !== '' ? trim((string) ($summary['bundle_id'] ?? '')) : null,
|
||||
'bundle_type' => trim((string) ($summary['bundle_type'] ?? '')) !== '' ? trim((string) ($summary['bundle_type'] ?? '')) : null,
|
||||
'document_title' => trim((string) ($summary['document_title'] ?? '')) !== '' ? trim((string) ($summary['document_title'] ?? '')) : null,
|
||||
'document_date' => $this->toDateTimeString((string) ($summary['document_date'] ?? '')),
|
||||
'practitioner_name' => trim((string) ($summary['practitioner_name'] ?? '')) !== '' ? trim((string) ($summary['practitioner_name'] ?? '')) : null,
|
||||
'organization_name' => trim((string) ($summary['organization_name'] ?? '')) !== '' ? trim((string) ($summary['organization_name'] ?? '')) : null,
|
||||
'summary_json' => json_encode($summary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'raw_bundle' => json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
$existing = $this->db->table('abdm_hiu_documents')
|
||||
->select('id')
|
||||
->where('doc_hash', $docHash)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (! empty($existing)) {
|
||||
$this->db->table('abdm_hiu_documents')->where('id', (int) $existing['id'])->update($row);
|
||||
$updated++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$row['doc_hash'] = $docHash;
|
||||
$row['created_at'] = $now;
|
||||
$this->db->table('abdm_hiu_documents')->insert($row);
|
||||
$saved++;
|
||||
}
|
||||
}
|
||||
|
||||
return ['saved' => $saved, 'updated' => $updated, 'skipped' => $skipped];
|
||||
}
|
||||
|
||||
public function listDocuments(array $filters, int $limit = 100): array
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_hiu_documents')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$builder = $this->db->table('abdm_hiu_documents')
|
||||
->select('*')
|
||||
->orderBy('id', 'DESC')
|
||||
->limit($limit);
|
||||
|
||||
$patientId = (int) ($filters['patient_id'] ?? 0);
|
||||
if ($patientId > 0) {
|
||||
$builder->where('patient_id', $patientId);
|
||||
}
|
||||
|
||||
$abhaAddress = trim((string) ($filters['abha_address'] ?? ''));
|
||||
if ($abhaAddress !== '') {
|
||||
$builder->where('abha_address', $abhaAddress);
|
||||
}
|
||||
|
||||
$q = trim((string) ($filters['q'] ?? ''));
|
||||
if ($q !== '') {
|
||||
$builder->groupStart()
|
||||
->like('patient_name', $q)
|
||||
->orLike('care_context_reference', $q)
|
||||
->orLike('document_title', $q)
|
||||
->orLike('practitioner_name', $q)
|
||||
->orLike('organization_name', $q)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getDocument(int $id): ?array
|
||||
{
|
||||
if ($id <= 0 || ! $this->db->tableExists('abdm_hiu_documents')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $this->db->table('abdm_hiu_documents')->where('id', $id)->get(1)->getRowArray();
|
||||
if (empty($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row['summary'] = json_decode((string) ($row['summary_json'] ?? ''), true) ?: [];
|
||||
$row['bundle'] = json_decode((string) ($row['raw_bundle'] ?? ''), true) ?: [];
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function extractSessions(array $result): array
|
||||
{
|
||||
$sessions = $result['sessions'] ?? null;
|
||||
if (is_array($sessions)) {
|
||||
return $sessions;
|
||||
}
|
||||
|
||||
$nested = $result['data']['sessions'] ?? null;
|
||||
return is_array($nested) ? $nested : [];
|
||||
}
|
||||
|
||||
private function extractConsentArtifactId(string $consentRef): string
|
||||
{
|
||||
$consentRef = trim($consentRef);
|
||||
if ($consentRef === '') {
|
||||
return '';
|
||||
}
|
||||
$parts = explode(':', $consentRef);
|
||||
return trim((string) ($parts[0] ?? ''));
|
||||
}
|
||||
|
||||
private function decodeBundle($raw): array
|
||||
{
|
||||
if (is_array($raw)) {
|
||||
return $raw;
|
||||
}
|
||||
|
||||
$text = trim((string) $raw);
|
||||
if ($text === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode($text, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
private function buildSummary(array $bundle): array
|
||||
{
|
||||
$entries = is_array($bundle['entry'] ?? null) ? (array) $bundle['entry'] : [];
|
||||
$patient = [];
|
||||
$composition = [];
|
||||
$practitioner = [];
|
||||
$organization = [];
|
||||
$conditions = [];
|
||||
$vitals = [];
|
||||
$medications = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$resource = is_array($entry['resource'] ?? null) ? (array) $entry['resource'] : [];
|
||||
$type = trim((string) ($resource['resourceType'] ?? ''));
|
||||
if ($type === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type === 'Patient' && empty($patient)) {
|
||||
$patient = $resource;
|
||||
} elseif ($type === 'Composition' && empty($composition)) {
|
||||
$composition = $resource;
|
||||
} elseif ($type === 'Practitioner' && empty($practitioner)) {
|
||||
$practitioner = $resource;
|
||||
} elseif ($type === 'Organization' && empty($organization)) {
|
||||
$organization = $resource;
|
||||
} elseif ($type === 'Condition') {
|
||||
$conditions[] = [
|
||||
'text' => trim((string) (($resource['code']['text'] ?? '') ?: ($resource['code']['coding'][0]['display'] ?? ''))),
|
||||
];
|
||||
} elseif ($type === 'Observation') {
|
||||
$codeText = trim((string) (($resource['code']['text'] ?? '') ?: ($resource['code']['coding'][0]['display'] ?? '')));
|
||||
$val = '';
|
||||
if (isset($resource['valueQuantity']) && is_array($resource['valueQuantity'])) {
|
||||
$val = trim((string) (($resource['valueQuantity']['value'] ?? '') . ' ' . ($resource['valueQuantity']['unit'] ?? '')));
|
||||
}
|
||||
if ($codeText !== '' || $val !== '') {
|
||||
$vitals[] = ['name' => $codeText, 'value' => $val];
|
||||
}
|
||||
} elseif ($type === 'MedicationRequest') {
|
||||
$medications[] = [
|
||||
'name' => trim((string) (($resource['medicationCodeableConcept']['text'] ?? '') ?: ($resource['medicationCodeableConcept']['coding'][0]['display'] ?? ''))),
|
||||
'dose' => trim((string) ($resource['dosageInstruction'][0]['text'] ?? '')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$patientName = trim((string) (($patient['name'][0]['text'] ?? '') ?: implode(' ', (array) ($patient['name'][0]['given'] ?? []))));
|
||||
$abhaNumber = '';
|
||||
foreach ((array) ($patient['identifier'] ?? []) as $identifier) {
|
||||
$system = trim((string) ($identifier['system'] ?? ''));
|
||||
if (stripos($system, 'healthid.ndhm.gov.in') !== false) {
|
||||
$abhaNumber = preg_replace('/\D+/', '', (string) ($identifier['value'] ?? '')) ?? '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$practitionerName = trim((string) (($practitioner['name'][0]['text'] ?? '') ?: implode(' ', (array) ($practitioner['name'][0]['given'] ?? []))));
|
||||
$organizationName = trim((string) ($organization['name'] ?? ''));
|
||||
|
||||
return [
|
||||
'bundle_id' => trim((string) ($bundle['id'] ?? '')),
|
||||
'bundle_type' => trim((string) (($bundle['resourceType'] ?? '') . '/' . ($bundle['type'] ?? ''))),
|
||||
'document_title' => trim((string) ($composition['title'] ?? '')),
|
||||
'document_date' => trim((string) ($composition['date'] ?? $bundle['timestamp'] ?? '')),
|
||||
'patient_name' => $patientName,
|
||||
'patient_gender' => trim((string) ($patient['gender'] ?? '')),
|
||||
'patient_birth_date' => trim((string) ($patient['birthDate'] ?? '')),
|
||||
'abha_number' => $abhaNumber,
|
||||
'abha_address' => $abhaNumber !== '' ? ($abhaNumber . '@sbx') : '',
|
||||
'practitioner_name' => $practitionerName,
|
||||
'organization_name' => $organizationName,
|
||||
'conditions' => $conditions,
|
||||
'vitals' => $vitals,
|
||||
'medications' => $medications,
|
||||
];
|
||||
}
|
||||
|
||||
private function mapPatientByAbha(string $abhaAddress, string $abhaNumber): array
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return ['patient_id' => 0];
|
||||
}
|
||||
|
||||
$fields = $this->db->getFieldNames('patient_master') ?? [];
|
||||
$idCol = $this->resolveExistingColumn($fields, ['id']);
|
||||
if ($idCol === null) {
|
||||
return ['patient_id' => 0];
|
||||
}
|
||||
|
||||
$abhaAddressCol = $this->resolveExistingColumn($fields, ['abha_address', 'abha_addr']);
|
||||
$abhaNumberCol = $this->resolveExistingColumn($fields, ['abha_id', 'abha_no', 'abha_number', 'abha']);
|
||||
|
||||
if ($abhaAddress !== '' && $abhaAddressCol !== null) {
|
||||
$row = $this->db->table('patient_master')
|
||||
->select($idCol . ' AS patient_id')
|
||||
->where($abhaAddressCol, $abhaAddress)
|
||||
->orderBy($idCol, 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($row)) {
|
||||
return ['patient_id' => (int) ($row['patient_id'] ?? 0)];
|
||||
}
|
||||
}
|
||||
|
||||
$digits = preg_replace('/\D+/', '', $abhaNumber) ?? '';
|
||||
if ($digits !== '' && $abhaNumberCol !== null) {
|
||||
$row = $this->db->table('patient_master')
|
||||
->select($idCol . ' AS patient_id')
|
||||
->where($abhaNumberCol, $digits)
|
||||
->orderBy($idCol, 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($row)) {
|
||||
return ['patient_id' => (int) ($row['patient_id'] ?? 0)];
|
||||
}
|
||||
}
|
||||
|
||||
return ['patient_id' => 0];
|
||||
}
|
||||
|
||||
private function resolveExistingColumn(array $fields, array $candidates): ?string
|
||||
{
|
||||
foreach ($candidates as $candidate) {
|
||||
if (in_array($candidate, $fields, true)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function toDateTimeString(string $value): ?string
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new \DateTime($value);
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -41,16 +41,6 @@ class M3HiuGatewayClient
|
|||
return $this->call('/v3/hiu/consent/request/status', $payload, 'consent.request.status');
|
||||
}
|
||||
|
||||
public function consentRequestFetch(array $payload): array
|
||||
{
|
||||
return $this->call('/v3/hiu/consent/request/fetch', $payload, 'consent.request.fetch');
|
||||
}
|
||||
|
||||
public function healthInformationRequest(array $payload): array
|
||||
{
|
||||
return $this->call('/v1/hiu/data/request', $payload, 'health-information.request');
|
||||
}
|
||||
|
||||
public function reconcileConsentStatus(array $payload): array
|
||||
{
|
||||
return $this->callGet('/v1/hiu/consent/status', $payload, 'consent.status.reconcile');
|
||||
|
|
@ -121,9 +111,7 @@ class M3HiuGatewayClient
|
|||
if ($curlErr !== '') {
|
||||
$errorText = $curlErr;
|
||||
} elseif ($ok !== 1) {
|
||||
$errorText = $isJson
|
||||
? (string) ($decoded['message'] ?? $decoded['error_text'] ?? $decoded['error'] ?? ('HTTP ' . $httpCode))
|
||||
: ('HTTP ' . $httpCode . ' non-JSON response');
|
||||
$errorText = $this->extractErrorText($decoded, $httpCode, $isJson);
|
||||
}
|
||||
|
||||
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $decoded ?? [])) {
|
||||
|
|
@ -267,9 +255,7 @@ class M3HiuGatewayClient
|
|||
if ($curlErr !== '') {
|
||||
$errorText = $curlErr;
|
||||
} elseif ($ok !== 1) {
|
||||
$errorText = $isJson
|
||||
? (string) ($decoded['message'] ?? $decoded['error_text'] ?? $decoded['error'] ?? ('HTTP ' . $httpCode))
|
||||
: ('HTTP ' . $httpCode . ' non-JSON response');
|
||||
$errorText = $this->extractErrorText($decoded, $httpCode, $isJson);
|
||||
}
|
||||
|
||||
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $decoded ?? [])) {
|
||||
|
|
@ -440,6 +426,33 @@ class M3HiuGatewayClient
|
|||
]);
|
||||
}
|
||||
|
||||
private function extractErrorText($decoded, int $httpCode, bool $isJson): string
|
||||
{
|
||||
if (! $isJson || ! is_array($decoded)) {
|
||||
return 'HTTP ' . $httpCode . ' non-JSON response';
|
||||
}
|
||||
|
||||
$code = trim((string) (
|
||||
$decoded['data']['error']['code']
|
||||
?? $decoded['error']['code']
|
||||
?? ''
|
||||
));
|
||||
$message = trim((string) (
|
||||
$decoded['data']['error']['message']
|
||||
?? $decoded['error']['message']
|
||||
?? $decoded['message']
|
||||
?? $decoded['error_text']
|
||||
?? (is_string($decoded['error'] ?? null) ? $decoded['error'] : '')
|
||||
));
|
||||
|
||||
$combined = trim(($code !== '' ? $code . ' ' : '') . $message);
|
||||
if ($combined !== '') {
|
||||
return $combined;
|
||||
}
|
||||
|
||||
return 'HTTP ' . $httpCode;
|
||||
}
|
||||
|
||||
private function findFirstValueByKeys(array $root, array $keys): string
|
||||
{
|
||||
$queue = [$root];
|
||||
|
|
|
|||
|
|
@ -4,16 +4,19 @@ namespace App\Libraries\Abdm;
|
|||
|
||||
use CodeIgniter\I18n\Time;
|
||||
use App\Libraries\Abdm\M3HiuGatewayClient;
|
||||
use App\Libraries\Abdm\M3HiuDocumentRepository;
|
||||
|
||||
class M3HiuWorkflowService
|
||||
{
|
||||
private \CodeIgniter\Database\BaseConnection $db;
|
||||
private M3HiuGatewayClient $client;
|
||||
private M3HiuDocumentRepository $documentRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->client = new M3HiuGatewayClient();
|
||||
$this->documentRepository = new M3HiuDocumentRepository();
|
||||
}
|
||||
|
||||
public function runOperation(string $operation, array $payload): array
|
||||
|
|
@ -21,6 +24,24 @@ class M3HiuWorkflowService
|
|||
$hasWorkflowTable = $this->db->tableExists('abdm_hiu_workflows');
|
||||
|
||||
$normalized = $this->normalizePayload($payload);
|
||||
$retiredError = $this->retiredOperationError($operation, $normalized);
|
||||
if ($retiredError !== null) {
|
||||
if ($hasWorkflowTable) {
|
||||
$this->upsertWorkflow(
|
||||
$operation,
|
||||
$normalized,
|
||||
$retiredError,
|
||||
'RETIRED',
|
||||
'failed',
|
||||
(int) ($retiredError['http_code'] ?? 410),
|
||||
false
|
||||
);
|
||||
$retiredError['workflow_persisted'] = 1;
|
||||
}
|
||||
|
||||
return $retiredError;
|
||||
}
|
||||
|
||||
$prepared = $this->preparePayloadForOperation($operation, $normalized);
|
||||
if (($prepared['ok'] ?? 0) !== 1) {
|
||||
$failedResult = [
|
||||
|
|
@ -50,7 +71,7 @@ class M3HiuWorkflowService
|
|||
}
|
||||
$dispatchPayload = (array) ($prepared['payload'] ?? []);
|
||||
|
||||
if ($hasWorkflowTable && ! in_array($operation, ['consent_status', 'consent_reconcile', 'data_fetch', 'hi_request'], true)) {
|
||||
if ($hasWorkflowTable && ! in_array($operation, ['consent_status', 'consent_reconcile', 'data_fetch'], true)) {
|
||||
$duplicate = $this->findDuplicate($operation, $normalized);
|
||||
if ($duplicate !== null) {
|
||||
$dupResponse = json_decode((string) ($duplicate['response_json'] ?? ''), true);
|
||||
|
|
@ -82,8 +103,14 @@ class M3HiuWorkflowService
|
|||
}
|
||||
|
||||
if ($hasWorkflowTable) {
|
||||
$this->upsertWorkflow($operation, $persistPayload, $result, $state, $status, $httpCode, $retryable);
|
||||
$workflowId = $this->upsertWorkflow($operation, $persistPayload, $result, $state, $status, $httpCode, $retryable);
|
||||
$result['workflow_persisted'] = 1;
|
||||
|
||||
if ($operation === 'data_fetch' && $ok) {
|
||||
$docPersist = $this->documentRepository->persistFromDataFetch($persistPayload, $result, $workflowId);
|
||||
$result['documents_persisted'] = (int) ($docPersist['saved'] ?? 0);
|
||||
$result['documents_updated'] = (int) ($docPersist['updated'] ?? 0);
|
||||
}
|
||||
} else {
|
||||
$result['workflow_persisted'] = 0;
|
||||
$result['workflow_warning'] = 'abdm_hiu_workflows table not found; operation executed without timeline persistence';
|
||||
|
|
@ -92,6 +119,26 @@ class M3HiuWorkflowService
|
|||
return $result;
|
||||
}
|
||||
|
||||
private function retiredOperationError(string $operation, array $payload): ?array
|
||||
{
|
||||
if (! in_array($operation, ['consent_fetch', 'hi_request'], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => 0,
|
||||
'http_code' => 410,
|
||||
'error_text' => 'Operation ' . $operation . ' is retired. Use consent_reconcile and data_fetch polling only.',
|
||||
'retryable' => 0,
|
||||
'request_id' => (string) ($payload['request_id'] ?? ''),
|
||||
'transaction_id' => (string) ($payload['transaction_id'] ?? ''),
|
||||
'consent_id' => (string) ($payload['consent_id'] ?? ''),
|
||||
'abha_address' => (string) ($payload['abha_address'] ?? ''),
|
||||
'workflow_state' => 'RETIRED',
|
||||
'replacement_ops' => ['consent_reconcile', 'data_fetch'],
|
||||
];
|
||||
}
|
||||
|
||||
public function retryDue(int $limit = 20): array
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
||||
|
|
@ -122,6 +169,11 @@ class M3HiuWorkflowService
|
|||
continue;
|
||||
}
|
||||
|
||||
if (in_array($operation, ['consent_fetch', 'hi_request'], true)) {
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->dispatch($operation, $payload);
|
||||
$ok = (int) ($result['ok'] ?? 0) === 1;
|
||||
$state = $this->resolveState($operation, $result);
|
||||
|
|
@ -179,58 +231,41 @@ class M3HiuWorkflowService
|
|||
if ((int) ($consentResult['ok'] ?? 0) === 1) {
|
||||
$summary['consent_updates']++;
|
||||
|
||||
$resolvedConsentId = trim((string) ($consentResult['abdm_consent_artifact_id'] ?? $consentResult['consent_id'] ?? ''));
|
||||
if ($resolvedConsentId !== '' && ! $this->hasSuccessfulHiRequest($requestId, $resolvedConsentId)) {
|
||||
if (! $this->shouldAttemptHiRequest($requestId, $resolvedConsentId)) {
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$hiPayload = $payload;
|
||||
$hiPayload['consent_id'] = $resolvedConsentId;
|
||||
$hiPayload['abdm_consent_artifact_id'] = $resolvedConsentId;
|
||||
$hiPayload['transaction_id'] = $this->generateFreshHiTransactionId($requestId, trim((string) ($payload['transaction_id'] ?? '')));
|
||||
$hiResult = $this->runOperation('hi_request', $hiPayload);
|
||||
if ((int) ($hiResult['ok'] ?? 0) !== 1) {
|
||||
$summary['failed']++;
|
||||
}
|
||||
$consentState = strtolower(trim((string) (
|
||||
$consentResult['workflow_state']
|
||||
?? $consentResult['status']
|
||||
?? $consentResult['consent_status']
|
||||
?? ''
|
||||
)));
|
||||
if ($consentState !== 'granted') {
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$summary['failed']++;
|
||||
}
|
||||
}
|
||||
|
||||
$hiRows = $this->db->table('abdm_hiu_workflows')
|
||||
->select('*')
|
||||
->where('operation', 'hi_request')
|
||||
->where('status', 'success')
|
||||
->orderBy('id', 'DESC')
|
||||
->get($limit)
|
||||
->getResultArray();
|
||||
$resolvedConsentId = trim((string) (
|
||||
$consentResult['abdm_consent_artifact_id']
|
||||
?? $consentResult['consent_id']
|
||||
?? $consentResult['abdm_consent_request_id']
|
||||
?? $consentResult['consent_request_id']
|
||||
?? ''
|
||||
));
|
||||
if ($resolvedConsentId === '' || $this->isGatewayRequestIdPattern($resolvedConsentId)) {
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$seenTransactions = [];
|
||||
foreach ($hiRows as $row) {
|
||||
$transactionId = trim((string) ($row['transaction_id'] ?? ''));
|
||||
$requestId = trim((string) ($row['request_id'] ?? ''));
|
||||
$key = $transactionId !== '' ? $transactionId : $requestId;
|
||||
if ($key === '' || isset($seenTransactions[$key])) {
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
$seenTransactions[$key] = true;
|
||||
$dataPayload = $payload;
|
||||
$dataPayload['consent_id'] = $resolvedConsentId;
|
||||
$dataPayload['consent_request_id'] = $resolvedConsentId;
|
||||
$dataPayload['abdm_consent_artifact_id'] = $resolvedConsentId;
|
||||
$dataPayload['abdm_consent_request_id'] = $resolvedConsentId;
|
||||
|
||||
$payload = [
|
||||
'request_id' => $requestId,
|
||||
'transaction_id' => $transactionId,
|
||||
'consent_id' => trim((string) ($row['consent_id'] ?? '')),
|
||||
'abdm_consent_artifact_id' => trim((string) ($row['abdm_consent_artifact_id'] ?? '')),
|
||||
'abha_address' => trim((string) ($row['abha_address'] ?? '')),
|
||||
'hfr_id' => $this->normalizeStoredHfrId((string) ($row['hfr_id'] ?? '')),
|
||||
];
|
||||
|
||||
$dataResult = $this->runOperation('data_fetch', $payload);
|
||||
if ((int) ($dataResult['ok'] ?? 0) === 1) {
|
||||
$summary['data_updates']++;
|
||||
$dataResult = $this->runOperation('data_fetch', $dataPayload);
|
||||
if ((int) ($dataResult['ok'] ?? 0) === 1) {
|
||||
$summary['data_updates']++;
|
||||
} else {
|
||||
$summary['failed']++;
|
||||
}
|
||||
} else {
|
||||
$summary['failed']++;
|
||||
}
|
||||
|
|
@ -558,8 +593,6 @@ class M3HiuWorkflowService
|
|||
return match ($operation) {
|
||||
'consent_request' => $this->client->consentRequest($payload),
|
||||
'consent_status' => $this->client->consentRequestStatus($payload),
|
||||
'consent_fetch' => $this->client->consentRequestFetch($payload),
|
||||
'hi_request' => $this->client->healthInformationRequest($payload),
|
||||
'consent_reconcile' => $this->client->reconcileConsentStatus($payload),
|
||||
'data_fetch' => $this->client->fetchDecryptedData($payload),
|
||||
default => ['ok' => 0, 'http_code' => 400, 'error_text' => 'Unsupported operation: ' . $operation, 'retryable' => 0],
|
||||
|
|
@ -701,6 +734,7 @@ class M3HiuWorkflowService
|
|||
}
|
||||
}
|
||||
|
||||
// Preserve requester.identifier for bridge deployments that validate strict requester schema.
|
||||
$requesterIdentifierValue = trim((string) ($consent['requester']['identifier']['value'] ?? ''));
|
||||
if ($requesterIdentifierValue === '') {
|
||||
$requesterIdentifierValue = $hiuId !== '' ? $hiuId : trim((string) ($clean['hfr_id'] ?? ''));
|
||||
|
|
@ -717,6 +751,15 @@ class M3HiuWorkflowService
|
|||
$consent['requester']['identifier']['value'] = $requesterIdentifierValue;
|
||||
}
|
||||
|
||||
$consent['hiTypes'] = $this->normalizeConsentHiTypes($consent['hiTypes'] ?? []);
|
||||
if (empty($consent['hiTypes'])) {
|
||||
return [
|
||||
'ok' => 0,
|
||||
'http_code' => 422,
|
||||
'error_text' => 'consent.hiTypes is required for consent request.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => 1,
|
||||
'payload' => $base + ['consent' => $consent],
|
||||
|
|
@ -749,83 +792,6 @@ class M3HiuWorkflowService
|
|||
];
|
||||
}
|
||||
|
||||
if ($operation === 'consent_fetch') {
|
||||
$consentId = trim((string) ($clean['abdm_consent_artifact_id'] ?? $clean['consentId'] ?? $clean['consent_id'] ?? ''));
|
||||
if ($consentId === '') {
|
||||
$consentId = $this->resolveAbdmConsentArtifactIdFromWorkflow($clean);
|
||||
}
|
||||
if ($consentId === '') {
|
||||
return [
|
||||
'ok' => 0,
|
||||
'http_code' => 422,
|
||||
'error_text' => 'consentId is required for consent fetch. Consent artifact is not available yet; run Check Consent Status until consent is GRANTED, then fetch using artifact id.',
|
||||
];
|
||||
}
|
||||
if ($this->isGatewayRequestIdPattern($consentId)) {
|
||||
return [
|
||||
'ok' => 0,
|
||||
'http_code' => 422,
|
||||
'error_text' => 'mapping_error: consentId appears to be a local temporary request id; use ABDM consent artifact id only.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => 1,
|
||||
'payload' => $base + ['consentId' => $consentId],
|
||||
];
|
||||
}
|
||||
|
||||
if ($operation === 'hi_request') {
|
||||
$hiRequest = $clean['hiRequest'] ?? null;
|
||||
if (! is_array($hiRequest) || empty($hiRequest)) {
|
||||
$hiRequest = $this->buildDefaultHiRequest($clean);
|
||||
}
|
||||
if (! is_array($hiRequest) || empty($hiRequest)) {
|
||||
return [
|
||||
'ok' => 0,
|
||||
'http_code' => 422,
|
||||
'error_text' => 'hiRequest object is required for health information request.',
|
||||
];
|
||||
}
|
||||
|
||||
$consentId = trim((string) ($hiRequest['consent']['id'] ?? $clean['consentId'] ?? $clean['consent_id'] ?? ''));
|
||||
if ($consentId === '') {
|
||||
return [
|
||||
'ok' => 0,
|
||||
'http_code' => 422,
|
||||
'error_text' => 'hiRequest.consent.id is required for health information request.',
|
||||
];
|
||||
}
|
||||
|
||||
if (! isset($hiRequest['consent']) || ! is_array($hiRequest['consent'])) {
|
||||
$hiRequest['consent'] = [];
|
||||
}
|
||||
$hiRequest['consent']['id'] = $consentId;
|
||||
|
||||
$requestIdRef = trim((string) ($clean['request_id'] ?? $clean['requestId'] ?? ''));
|
||||
$transactionId = $this->generateFreshHiTransactionId(
|
||||
$requestIdRef,
|
||||
trim((string) ($hiRequest['transactionId'] ?? $clean['transactionId'] ?? $clean['transaction_id'] ?? ''))
|
||||
);
|
||||
$hiRequest['transactionId'] = $transactionId;
|
||||
|
||||
if (! isset($hiRequest['dateRange']) || ! is_array($hiRequest['dateRange'])) {
|
||||
$hiRequest['dateRange'] = [
|
||||
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-365 days')),
|
||||
'to' => gmdate('Y-m-d\T00:00:00.000\Z'),
|
||||
];
|
||||
}
|
||||
$hiRequest['dateRange'] = $this->resolveSafeHiDateRange($requestIdRef, $consentId, $hiRequest['dateRange']);
|
||||
if (trim((string) ($hiRequest['dataPushUrl'] ?? '')) === '') {
|
||||
$hiRequest['dataPushUrl'] = rtrim((string) (getenv('EATRIA_BRIDGE_URL') ?: 'https://abdm-bridge.e-atria.in/api'), '/') . '/v3/hiu/data/push';
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => 1,
|
||||
'payload' => $base + ['transactionId' => $transactionId, 'transaction_id' => $transactionId, 'hiRequest' => $hiRequest],
|
||||
];
|
||||
}
|
||||
|
||||
if ($operation === 'consent_reconcile') {
|
||||
$requestIdRef = trim((string) ($clean['request_id'] ?? $clean['requestId'] ?? ''));
|
||||
$consentRequestId = trim((string) ($clean['abdm_consent_request_id'] ?? $clean['consentRequestId'] ?? ''));
|
||||
|
|
@ -857,27 +823,34 @@ class M3HiuWorkflowService
|
|||
}
|
||||
|
||||
if ($operation === 'data_fetch') {
|
||||
$transactionId = trim((string) ($clean['transaction_id'] ?? $clean['transactionId'] ?? ''));
|
||||
$consentId = trim((string) ($clean['abdm_consent_artifact_id'] ?? $clean['consentId'] ?? $clean['consent_id'] ?? ''));
|
||||
if ($consentId !== '' && $this->isGatewayRequestIdPattern($consentId)) {
|
||||
$consentId = '';
|
||||
$consentId = trim((string) (
|
||||
$clean['abdm_consent_artifact_id']
|
||||
?? $clean['consentId']
|
||||
?? $clean['consent_id']
|
||||
?? $clean['abdm_consent_request_id']
|
||||
?? $clean['consentRequestId']
|
||||
?? $clean['consent_request_id']
|
||||
?? $clean['request_id']
|
||||
?? $clean['requestId']
|
||||
?? ''
|
||||
));
|
||||
if ($consentId === '') {
|
||||
$consentId = $this->resolveAbdmConsentArtifactIdFromWorkflow($clean);
|
||||
}
|
||||
if ($consentId === '') {
|
||||
$consentId = $this->resolveAbdmConsentRequestIdFromWorkflow($clean);
|
||||
}
|
||||
|
||||
if ($transactionId === '' && $consentId === '') {
|
||||
if ($consentId === '') {
|
||||
return [
|
||||
'ok' => 0,
|
||||
'http_code' => 422,
|
||||
'error_text' => 'One of transaction_id or consent_id is required for data fetch polling.',
|
||||
'error_text' => 'consent_id is required for decrypted data fetch polling. Use consent artifact id, consent request id, or original request_id after consent is GRANTED.',
|
||||
];
|
||||
}
|
||||
|
||||
$query = [];
|
||||
if ($transactionId !== '') {
|
||||
$query['transaction_id'] = $transactionId;
|
||||
}
|
||||
if ($consentId !== '') {
|
||||
$query['consent_id'] = $consentId;
|
||||
}
|
||||
$query['consent_id'] = $consentId;
|
||||
|
||||
return [
|
||||
'ok' => 1,
|
||||
|
|
@ -977,272 +950,55 @@ class M3HiuWorkflowService
|
|||
return $consent;
|
||||
}
|
||||
|
||||
private function buildDefaultHiRequest(array $payload): array
|
||||
{
|
||||
$consentId = trim((string) ($payload['abdm_consent_artifact_id'] ?? $payload['consentId'] ?? $payload['consent_id'] ?? ''));
|
||||
if ($consentId === '' || $this->isGatewayRequestIdPattern($consentId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$requestIdRef = trim((string) ($payload['request_id'] ?? $payload['requestId'] ?? ''));
|
||||
$transactionId = $this->generateFreshHiTransactionId(
|
||||
$requestIdRef,
|
||||
trim((string) ($payload['transactionId'] ?? $payload['transaction_id'] ?? ''))
|
||||
);
|
||||
|
||||
return [
|
||||
'consent' => [
|
||||
'id' => $consentId,
|
||||
],
|
||||
'dateRange' => $this->resolveSafeHiDateRange($requestIdRef, $consentId, [
|
||||
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-365 days')),
|
||||
'to' => gmdate('Y-m-d\T00:00:00.000\Z'),
|
||||
]),
|
||||
'dataPushUrl' => rtrim((string) (getenv('EATRIA_BRIDGE_URL') ?: 'https://abdm-bridge.e-atria.in/api'), '/') . '/v3/hiu/data/push',
|
||||
'transactionId' => $transactionId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a safe HI date range by enforcing consent-granted permission window
|
||||
* (from consent fetch/reconcile details), then clamping with from <= to <= now.
|
||||
* Normalize legacy/record hiTypes to HIU consent hiTypes accepted by bridge.
|
||||
*
|
||||
* @param array<string, mixed> $fallbackRange
|
||||
* @return array<string, string>
|
||||
* @param mixed $hiTypes
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function resolveSafeHiDateRange(string $requestIdRef, string $consentId, array $fallbackRange): array
|
||||
private function normalizeConsentHiTypes($hiTypes): array
|
||||
{
|
||||
$grantedRange = $this->resolveGrantedConsentDateRange($requestIdRef, $consentId);
|
||||
$consentRange = ! empty($grantedRange)
|
||||
? $grantedRange
|
||||
: $this->resolveConsentRequestDateRangeByRequestId($requestIdRef);
|
||||
|
||||
$fromRaw = trim((string) ($consentRange['from'] ?? $fallbackRange['from'] ?? ''));
|
||||
$toRaw = trim((string) ($consentRange['to'] ?? $fallbackRange['to'] ?? ''));
|
||||
|
||||
$nowMs = (int) floor(microtime(true) * 1000);
|
||||
$fromMs = $this->parseIsoUtcToEpochMillis($fromRaw);
|
||||
$toMs = $this->parseIsoUtcToEpochMillis($toRaw);
|
||||
$grantedFromMs = $this->parseIsoUtcToEpochMillis((string) ($grantedRange['from'] ?? ''));
|
||||
$grantedToMs = $this->parseIsoUtcToEpochMillis((string) ($grantedRange['to'] ?? ''));
|
||||
|
||||
if ($fromMs === null) {
|
||||
$fromMs = $nowMs - (365 * 24 * 60 * 60 * 1000);
|
||||
if (is_string($hiTypes)) {
|
||||
$hiTypes = [$hiTypes];
|
||||
}
|
||||
if ($toMs === null) {
|
||||
$toMs = $nowMs;
|
||||
}
|
||||
|
||||
// HMS-side requirement: keep HI range equal to or strictly within granted consent permission range.
|
||||
if ($grantedFromMs !== null && $fromMs < $grantedFromMs) {
|
||||
$fromMs = $grantedFromMs;
|
||||
}
|
||||
if ($grantedToMs !== null && $toMs > $grantedToMs) {
|
||||
$toMs = $grantedToMs;
|
||||
}
|
||||
|
||||
if ($toMs > $nowMs) {
|
||||
$toMs = $nowMs;
|
||||
}
|
||||
if ($fromMs > $toMs) {
|
||||
if ($grantedFromMs !== null && $grantedFromMs <= $toMs) {
|
||||
$fromMs = $grantedFromMs;
|
||||
} else {
|
||||
$fromMs = $toMs;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'from' => $this->formatEpochMillisToIsoUtc($fromMs),
|
||||
'to' => $this->formatEpochMillisToIsoUtc($toMs),
|
||||
];
|
||||
}
|
||||
|
||||
private function parseIsoUtcToEpochMillis(string $value): ?int
|
||||
{
|
||||
$raw = trim($value);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new \DateTimeImmutable($raw);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sec = (int) $dt->format('U');
|
||||
$micro = (int) $dt->format('u');
|
||||
|
||||
return ($sec * 1000) + intdiv($micro, 1000);
|
||||
}
|
||||
|
||||
private function formatEpochMillisToIsoUtc(int $epochMs): string
|
||||
{
|
||||
$sec = intdiv($epochMs, 1000);
|
||||
$ms = $epochMs - ($sec * 1000);
|
||||
if ($ms < 0) {
|
||||
$ms = 0;
|
||||
}
|
||||
|
||||
$dt = (new \DateTimeImmutable('@' . $sec))->setTimezone(new \DateTimeZone('UTC'));
|
||||
|
||||
return $dt->format('Y-m-d\TH:i:s') . '.' . str_pad((string) $ms, 3, '0', STR_PAD_LEFT) . 'Z';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{from?: string, to?: string}
|
||||
*/
|
||||
private function resolveGrantedConsentDateRange(string $requestId, string $consentId): array
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
||||
if (! is_array($hiTypes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
|
||||
$builder = $this->db->table('abdm_hiu_workflows')
|
||||
->select('response_json, request_json')
|
||||
->whereIn('operation', ['consent_fetch', 'consent_reconcile', 'consent_status', 'consent_callback']);
|
||||
|
||||
$hasFilter = false;
|
||||
$builder->groupStart();
|
||||
if ($requestId !== '') {
|
||||
$builder->orWhere('request_id', $requestId);
|
||||
$hasFilter = true;
|
||||
}
|
||||
if ($consentId !== '') {
|
||||
$builder->orWhere('consent_id', $consentId);
|
||||
$hasFilter = true;
|
||||
if (in_array('abdm_consent_artifact_id', $fields, true)) {
|
||||
$builder->orWhere('abdm_consent_artifact_id', $consentId);
|
||||
}
|
||||
}
|
||||
$builder->groupEnd();
|
||||
|
||||
if (! $hasFilter) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('id', 'DESC')->get(30)->getResultArray();
|
||||
foreach ($rows as $row) {
|
||||
foreach (['response_json', 'request_json'] as $jsonCol) {
|
||||
$decoded = json_decode((string) ($row[$jsonCol] ?? ''), true);
|
||||
if (! is_array($decoded)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$range = $this->extractPermissionDateRange($decoded);
|
||||
if (! empty($range)) {
|
||||
return $range;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array{from?: string, to?: string}
|
||||
*/
|
||||
private function extractPermissionDateRange(array $data): array
|
||||
{
|
||||
$pathCandidates = [
|
||||
$data['consent']['permission']['dateRange'] ?? null,
|
||||
$data['consentDetail']['permission']['dateRange'] ?? null,
|
||||
$data['data']['consent']['permission']['dateRange'] ?? null,
|
||||
$data['data']['consentDetail']['permission']['dateRange'] ?? null,
|
||||
$data['response']['consent']['permission']['dateRange'] ?? null,
|
||||
$data['response']['consentDetail']['permission']['dateRange'] ?? null,
|
||||
$map = [
|
||||
'opconsultrecord' => 'OPConsultation',
|
||||
'opconsultation' => 'OPConsultation',
|
||||
'diagnosticreportrecord' => 'DiagnosticReport',
|
||||
'diagnosticreport' => 'DiagnosticReport',
|
||||
'prescriptionrecord' => 'Prescription',
|
||||
'prescription' => 'Prescription',
|
||||
'dischargesummaryrecord' => 'DischargeSummary',
|
||||
'dischargesummary' => 'DischargeSummary',
|
||||
'healthdocumentrecord' => 'HealthDocument',
|
||||
'healthdocument' => 'HealthDocument',
|
||||
'immunization' => 'ImmunizationRecord',
|
||||
'immunizationrecord' => 'ImmunizationRecord',
|
||||
'wellness' => 'Wellness',
|
||||
'wellnessrecord' => 'Wellness',
|
||||
'invoice' => 'Invoice',
|
||||
'invoicerecord' => 'Invoice',
|
||||
];
|
||||
|
||||
foreach ($pathCandidates as $candidate) {
|
||||
if (is_array($candidate)) {
|
||||
$from = trim((string) ($candidate['from'] ?? ''));
|
||||
$to = trim((string) ($candidate['to'] ?? ''));
|
||||
if ($from !== '' || $to !== '') {
|
||||
$out = [];
|
||||
if ($from !== '') {
|
||||
$out['from'] = $from;
|
||||
}
|
||||
if ($to !== '') {
|
||||
$out['to'] = $to;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['permission']) && is_array($data['permission'])) {
|
||||
$range = $data['permission']['dateRange'] ?? null;
|
||||
if (is_array($range)) {
|
||||
$from = trim((string) ($range['from'] ?? ''));
|
||||
$to = trim((string) ($range['to'] ?? ''));
|
||||
if ($from !== '' || $to !== '') {
|
||||
$out = [];
|
||||
if ($from !== '') {
|
||||
$out['from'] = $from;
|
||||
}
|
||||
if ($to !== '') {
|
||||
$out['to'] = $to;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data as $value) {
|
||||
if (! is_array($value)) {
|
||||
$normalized = [];
|
||||
foreach ($hiTypes as $type) {
|
||||
$raw = trim((string) $type);
|
||||
if ($raw === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$nested = $this->extractPermissionDateRange($value);
|
||||
if (! empty($nested)) {
|
||||
return $nested;
|
||||
$key = strtolower($raw);
|
||||
$resolved = $map[$key] ?? $raw;
|
||||
if (! in_array($resolved, $normalized, true)) {
|
||||
$normalized[] = $resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{from?: string, to?: string}
|
||||
*/
|
||||
private function resolveConsentRequestDateRangeByRequestId(string $requestId): array
|
||||
{
|
||||
if ($requestId === '' || ! $this->db->tableExists('abdm_hiu_workflows')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$row = $this->db->table('abdm_hiu_workflows')
|
||||
->select('request_json')
|
||||
->where('operation', 'consent_request')
|
||||
->where('request_id', $requestId)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (empty($row['request_json'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode((string) $row['request_json'], true);
|
||||
if (! is_array($decoded)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$from = trim((string) ($decoded['consent']['permission']['dateRange']['from'] ?? ''));
|
||||
$to = trim((string) ($decoded['consent']['permission']['dateRange']['to'] ?? ''));
|
||||
|
||||
$out = [];
|
||||
if ($from !== '') {
|
||||
$out['from'] = $from;
|
||||
}
|
||||
if ($to !== '') {
|
||||
$out['to'] = $to;
|
||||
}
|
||||
|
||||
return $out;
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function readSettingValue(string $key): string
|
||||
|
|
@ -1460,102 +1216,6 @@ class M3HiuWorkflowService
|
|||
return null;
|
||||
}
|
||||
|
||||
private function hasSuccessfulHiRequest(string $requestId, string $consentId): bool
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$builder = $this->db->table('abdm_hiu_workflows')
|
||||
->select('id')
|
||||
->where('operation', 'hi_request')
|
||||
->where('status', 'success');
|
||||
|
||||
$builder->groupStart();
|
||||
if ($requestId !== '') {
|
||||
$builder->orWhere('request_id', $requestId);
|
||||
}
|
||||
if ($consentId !== '') {
|
||||
$builder->orWhere('consent_id', $consentId);
|
||||
if (in_array('abdm_consent_artifact_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) {
|
||||
$builder->orWhere('abdm_consent_artifact_id', $consentId);
|
||||
}
|
||||
}
|
||||
$builder->groupEnd();
|
||||
|
||||
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
||||
return ! empty($row);
|
||||
}
|
||||
|
||||
private function shouldAttemptHiRequest(string $requestId, string $consentId): bool
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$row = $this->latestHiRequestRow($requestId, $consentId);
|
||||
if (empty($row)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$status = strtolower(trim((string) ($row['status'] ?? '')));
|
||||
$retryable = (int) ($row['is_retryable'] ?? 0) === 1;
|
||||
$retryCount = (int) ($row['retry_count'] ?? 0);
|
||||
$nextRetryAt = trim((string) ($row['next_retry_at'] ?? ''));
|
||||
if ($status !== 'failed' || ! $retryable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($nextRetryAt === '') {
|
||||
return $retryCount < 4;
|
||||
}
|
||||
|
||||
return strtotime($nextRetryAt) <= time();
|
||||
}
|
||||
|
||||
private function latestHiRequestRow(string $requestId, string $consentId): array
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
|
||||
$builder = $this->db->table('abdm_hiu_workflows')
|
||||
->select('id, status, is_retryable, retry_count, next_retry_at')
|
||||
->where('operation', 'hi_request');
|
||||
|
||||
$builder->groupStart();
|
||||
$hasCondition = false;
|
||||
if ($requestId !== '') {
|
||||
$builder->where('request_id', $requestId);
|
||||
$hasCondition = true;
|
||||
}
|
||||
if ($consentId !== '') {
|
||||
if ($hasCondition) {
|
||||
$builder->orWhere('consent_id', $consentId);
|
||||
} else {
|
||||
$builder->where('consent_id', $consentId);
|
||||
$hasCondition = true;
|
||||
}
|
||||
|
||||
if (in_array('abdm_consent_artifact_id', $fields, true)) {
|
||||
$builder->orWhere('abdm_consent_artifact_id', $consentId);
|
||||
}
|
||||
}
|
||||
$builder->groupEnd();
|
||||
|
||||
if (! $hasCondition) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $builder->orderBy('id', 'DESC')->get(1)->getRowArray() ?? [];
|
||||
}
|
||||
|
||||
private function generateFreshHiTransactionId(string $requestId, string $currentTransactionId): string
|
||||
{
|
||||
return $this->generateUuidV4();
|
||||
}
|
||||
|
||||
private function generateUuidV4(): string
|
||||
{
|
||||
$bytes = random_bytes(16);
|
||||
|
|
@ -1574,7 +1234,7 @@ class M3HiuWorkflowService
|
|||
int $httpCode,
|
||||
bool $retryable,
|
||||
int $forceId = 0
|
||||
): void {
|
||||
): int {
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$hmsRequestId = $this->firstNonEmptyString([
|
||||
$payload['request_id'] ?? null,
|
||||
|
|
@ -1706,11 +1366,12 @@ class M3HiuWorkflowService
|
|||
|
||||
if ($id > 0) {
|
||||
$this->db->table('abdm_hiu_workflows')->where('id', $id)->update($row);
|
||||
return;
|
||||
return $id;
|
||||
}
|
||||
|
||||
$row['created_at'] = $now;
|
||||
$this->db->table('abdm_hiu_workflows')->insert($row);
|
||||
return (int) ($this->db->insertID() ?: 0);
|
||||
}
|
||||
|
||||
private function isGatewayRequestIdPattern(string $value): bool
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ class AbdmGatewayPushClient
|
|||
'OPConsultRecord',
|
||||
'DiagnosticReportRecord',
|
||||
'DischargeSummaryRecord',
|
||||
'ImmunizationRecord',
|
||||
'InvoiceRecord',
|
||||
'PrescriptionRecord',
|
||||
'HealthDocumentRecord',
|
||||
'WellnessRecord',
|
||||
];
|
||||
|
||||
public function __construct(?BaseConnection $db = null)
|
||||
|
|
|
|||
269
app/Views/abdm/hiu_documents.php
Normal file
269
app/Views/abdm/hiu_documents.php
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
<div class="container-fluid py-3">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">ABDM Fetched Patient Documents</h5>
|
||||
<a class="btn btn-sm btn-outline-primary" href="<?= base_url('AbdmHiu') ?>">Back To HIU Console</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Search Patient</label>
|
||||
<input id="patient_search" class="form-control" placeholder="Name / UHID / ABHA / Mobile">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button id="btnPatientSearch" class="btn btn-outline-secondary w-100">Search</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Search Documents</label>
|
||||
<input id="doc_search" class="form-control" placeholder="title / care context / doctor">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button id="btnDocSearch" class="btn btn-outline-primary w-100">Load Documents</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-muted mt-2" id="patient_status">Select a patient to view mapped ABDM documents.</div>
|
||||
|
||||
<div class="table-responsive mt-3">
|
||||
<table class="table table-sm table-hover" id="patient_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>Patient</th>
|
||||
<th>UHID</th>
|
||||
<th>ABHA Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-5">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header">Document List</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 560px;">
|
||||
<table class="table table-sm table-striped mb-0" id="doc_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Title</th>
|
||||
<th>Care Context</th>
|
||||
<th>Doctor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="4" class="text-muted text-center">No documents loaded.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-7">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header">Document Detail</div>
|
||||
<div class="card-body" id="doc_detail_box">
|
||||
<div class="text-muted">Select a document to view human-readable summary.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var selectedPatient = null;
|
||||
var docs = [];
|
||||
|
||||
var patientLookupUrl = '<?= base_url('AbdmHiu/patient_lookup') ?>';
|
||||
var docsListUrl = '<?= base_url('AbdmHiu/documents_list') ?>';
|
||||
var docDetailBase = '<?= base_url('AbdmHiu/document_detail') ?>';
|
||||
|
||||
function esc(v) {
|
||||
return (v || '').toString()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function parseDate(v) {
|
||||
var t = (v || '').toString().trim();
|
||||
if (!t) return '-';
|
||||
var d = new Date(t.replace(' ', 'T'));
|
||||
if (isNaN(d.getTime())) return t;
|
||||
return d.toLocaleString();
|
||||
}
|
||||
|
||||
function renderPatientRows(items) {
|
||||
var tbody = document.querySelector('#patient_table tbody');
|
||||
tbody.innerHTML = '';
|
||||
if (!items.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-muted text-center">No patients found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(function (p, idx) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '' +
|
||||
'<td><button class="btn btn-sm btn-outline-primary" data-p-idx="' + idx + '">Select</button></td>' +
|
||||
'<td>' + esc(p.patient_name) + '</td>' +
|
||||
'<td>' + esc(p.patient_code) + '</td>' +
|
||||
'<td>' + esc(p.abha_address) + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('button[data-p-idx]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var idx = parseInt(btn.getAttribute('data-p-idx'), 10);
|
||||
if (!Number.isNaN(idx) && items[idx]) {
|
||||
selectedPatient = items[idx];
|
||||
document.getElementById('patient_status').textContent = 'Selected: ' +
|
||||
(selectedPatient.patient_name || '-') + ' | ABHA: ' + (selectedPatient.abha_address || '-');
|
||||
loadDocuments();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function searchPatients() {
|
||||
var q = document.getElementById('patient_search').value.trim();
|
||||
var url = patientLookupUrl + '?q=' + encodeURIComponent(q);
|
||||
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if ((res.ok || 0) !== 1) {
|
||||
document.getElementById('patient_status').textContent = res.error || 'Search failed';
|
||||
return;
|
||||
}
|
||||
renderPatientRows(res.items || []);
|
||||
document.getElementById('patient_status').textContent = (res.count || 0) + ' patient(s) found';
|
||||
})
|
||||
.catch(function (e) {
|
||||
document.getElementById('patient_status').textContent = e.message || 'Search failed';
|
||||
});
|
||||
}
|
||||
|
||||
function renderDocs(items) {
|
||||
docs = items || [];
|
||||
var tbody = document.querySelector('#doc_table tbody');
|
||||
tbody.innerHTML = '';
|
||||
if (!docs.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-muted text-center">No ABDM documents mapped for selected patient yet.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
docs.forEach(function (doc, idx) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.setAttribute('data-doc-idx', idx);
|
||||
tr.innerHTML = '' +
|
||||
'<td>' + esc(parseDate(doc.document_date || doc.created_at)) + '</td>' +
|
||||
'<td>' + esc(doc.document_title || '-') + '</td>' +
|
||||
'<td>' + esc(doc.care_context_reference || '-') + '</td>' +
|
||||
'<td>' + esc(doc.practitioner_name || '-') + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('tr[data-doc-idx]').forEach(function (row) {
|
||||
row.addEventListener('click', function () {
|
||||
var idx = parseInt(row.getAttribute('data-doc-idx'), 10);
|
||||
if (!Number.isNaN(idx) && docs[idx]) {
|
||||
loadDocumentDetail(docs[idx].id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
loadDocumentDetail(docs[0].id);
|
||||
}
|
||||
|
||||
function loadDocuments() {
|
||||
if (!selectedPatient) {
|
||||
return;
|
||||
}
|
||||
|
||||
var q = document.getElementById('doc_search').value.trim();
|
||||
var url = docsListUrl +
|
||||
'?patient_id=' + encodeURIComponent(selectedPatient.patient_id || 0) +
|
||||
'&abha_address=' + encodeURIComponent(selectedPatient.abha_address || '') +
|
||||
'&q=' + encodeURIComponent(q);
|
||||
|
||||
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if ((res.ok || 0) !== 1) {
|
||||
return;
|
||||
}
|
||||
renderDocs(res.items || []);
|
||||
});
|
||||
}
|
||||
|
||||
function renderList(label, arr, itemFormatter) {
|
||||
arr = Array.isArray(arr) ? arr : [];
|
||||
if (!arr.length) {
|
||||
return '<div class="mb-2"><div class="fw-semibold">' + esc(label) + '</div><div class="text-muted">No data</div></div>';
|
||||
}
|
||||
|
||||
var html = '<div class="mb-2"><div class="fw-semibold">' + esc(label) + '</div><ul class="mb-0">';
|
||||
arr.forEach(function (item) {
|
||||
html += '<li>' + itemFormatter(item) + '</li>';
|
||||
});
|
||||
html += '</ul></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function loadDocumentDetail(id) {
|
||||
var box = document.getElementById('doc_detail_box');
|
||||
box.innerHTML = '<div class="text-muted">Loading...</div>';
|
||||
fetch(docDetailBase + '/' + encodeURIComponent(id), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if ((res.ok || 0) !== 1) {
|
||||
box.innerHTML = '<div class="text-danger">Unable to load detail.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var item = res.item || {};
|
||||
var s = item.summary || {};
|
||||
var html = '' +
|
||||
'<div class="row g-2 mb-3">' +
|
||||
'<div class="col-md-6"><div><span class="text-muted">Patient:</span> <strong>' + esc(s.patient_name || item.patient_name || '-') + '</strong></div></div>' +
|
||||
'<div class="col-md-6"><div><span class="text-muted">ABHA:</span> ' + esc(item.abha_address || '-') + '</div></div>' +
|
||||
'<div class="col-md-6"><div><span class="text-muted">Document:</span> ' + esc(item.document_title || '-') + '</div></div>' +
|
||||
'<div class="col-md-6"><div><span class="text-muted">Date:</span> ' + esc(parseDate(item.document_date || '')) + '</div></div>' +
|
||||
'<div class="col-md-6"><div><span class="text-muted">Doctor:</span> ' + esc(item.practitioner_name || '-') + '</div></div>' +
|
||||
'<div class="col-md-6"><div><span class="text-muted">Organization:</span> ' + esc(item.organization_name || '-') + '</div></div>' +
|
||||
'<div class="col-md-6"><div><span class="text-muted">Care Context:</span> ' + esc(item.care_context_reference || '-') + '</div></div>' +
|
||||
'<div class="col-md-6"><div><span class="text-muted">Bundle:</span> ' + esc(item.bundle_type || '-') + '</div></div>' +
|
||||
'</div>';
|
||||
|
||||
html += renderList('Diagnoses', s.conditions || [], function (x) {
|
||||
return esc((x && x.text) || '-');
|
||||
});
|
||||
|
||||
html += renderList('Vitals', s.vitals || [], function (x) {
|
||||
return esc(((x && x.name) || '-') + ': ' + ((x && x.value) || '-'));
|
||||
});
|
||||
|
||||
html += renderList('Medications', s.medications || [], function (x) {
|
||||
var name = (x && x.name) || '-';
|
||||
var dose = (x && x.dose) || '';
|
||||
return esc(name + (dose ? (' | ' + dose) : ''));
|
||||
});
|
||||
|
||||
box.innerHTML = html;
|
||||
})
|
||||
.catch(function (e) {
|
||||
box.innerHTML = '<div class="text-danger">' + esc(e.message || 'Unable to load detail') + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btnPatientSearch').addEventListener('click', searchPatients);
|
||||
document.getElementById('btnDocSearch').addEventListener('click', loadDocuments);
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -8,7 +8,10 @@
|
|||
<span id="poll_summary_badge" class="badge bg-secondary">Poll: waiting</span>
|
||||
<button class="btn btn-sm btn-outline-warning" id="btnPollErrors" type="button">Show Poll Errors</button>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" id="btnRefreshTimeline">Refresh Timeline</button>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-success" id="btnOpenDocumentsAjax" type="button">View Fetched Documents</button>
|
||||
<button class="btn btn-sm btn-outline-primary" id="btnRefreshTimeline">Refresh Timeline</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="poll_error_panel" class="alert alert-warning d-none mb-3">
|
||||
|
|
@ -88,8 +91,7 @@
|
|||
<span class="badge bg-secondary" data-step="1">1. Select Patient</span>
|
||||
<span class="badge bg-secondary" data-step="2">2. Create Consent Request</span>
|
||||
<span class="badge bg-secondary" data-step="3">3. Check Consent Status</span>
|
||||
<span class="badge bg-secondary" data-step="4">4. Access Consent Artifact</span>
|
||||
<span class="badge bg-secondary" data-step="5">5. Request Health Information</span>
|
||||
<span class="badge bg-secondary" data-step="4">4. Poll & Fetch Decrypted Data</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -113,8 +115,7 @@
|
|||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-primary" id="btnConsentRequest" data-op="consent_request" disabled>Create Consent Request</button>
|
||||
<button class="btn btn-outline-primary" id="btnConsentStatus" data-op="consent_status" disabled>Check Consent Status</button>
|
||||
<button class="btn btn-outline-primary" id="btnConsentFetch" data-op="consent_fetch" disabled>Fetch Consent Artifact</button>
|
||||
<button class="btn btn-success" id="btnHiRequest" data-op="hi_request" disabled>Request Health Information</button>
|
||||
<button class="btn btn-outline-success" id="btnConsentFetch" data-op="data_fetch" disabled>Poll & Fetch Decrypted Data</button>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-3 flex-wrap mt-2">
|
||||
|
|
@ -124,12 +125,38 @@
|
|||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="toggleAutoNextStep">
|
||||
<label class="form-check-label small" for="toggleAutoNextStep">Auto Next Step (Status -> Fetch -> HI)</label>
|
||||
<label class="form-check-label small" for="toggleAutoNextStep">Auto Next Step (Status -> Data Fetch)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<pre id="result_box" class="bg-dark text-light p-3 rounded" style="min-height:120px">{}</pre>
|
||||
|
||||
<div class="card border-success mt-3 d-none" id="data_preview_card">
|
||||
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
||||
<div class="fw-semibold">Decrypted Data Preview</div>
|
||||
<div id="data_preview_meta" class="small text-muted">No data sessions yet.</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped mb-0" id="data_preview_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Txn ID</th>
|
||||
<th>Consent Ref</th>
|
||||
<th>Status</th>
|
||||
<th>Records</th>
|
||||
<th>Care Contexts</th>
|
||||
<th>Bundle Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="6" class="text-muted text-center">Run Poll & Fetch Decrypted Data to load records.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -177,6 +204,85 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-none" id="ajax_docs_panel">
|
||||
<div class="card shadow-sm border-success">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Fetched ABDM Documents (AJAX View)</h6>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnCloseAjaxDocs" type="button">Close</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Search Patient</label>
|
||||
<input id="ajax_doc_patient_search" class="form-control" placeholder="Name / UHID / ABHA / Mobile">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button id="btnAjaxDocPatientSearch" class="btn btn-outline-secondary w-100" type="button">Search</button>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">Search Documents</label>
|
||||
<input id="ajax_doc_search" class="form-control" placeholder="title / care context / doctor">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button id="btnAjaxDocSearch" class="btn btn-outline-primary w-100" type="button">Load Documents</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="small text-muted" id="ajax_doc_patient_status">Select a patient to view mapped ABDM documents.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive mt-2">
|
||||
<table class="table table-sm table-hover mb-0" id="ajax_doc_patient_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>Patient</th>
|
||||
<th>UHID</th>
|
||||
<th>ABHA Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="4" class="text-muted text-center">Search patient to start.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-1">
|
||||
<div class="col-lg-5">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-2">Document List</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 420px;">
|
||||
<table class="table table-sm table-striped mb-0" id="ajax_doc_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Title</th>
|
||||
<th>Care Context</th>
|
||||
<th>Doctor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="4" class="text-muted text-center">No documents loaded.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="card h-100">
|
||||
<div class="card-header py-2">Document Detail</div>
|
||||
<div class="card-body" id="ajax_doc_detail_box">
|
||||
<div class="text-muted">Select a document to view human-readable summary.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -219,13 +325,15 @@
|
|||
var opRoute = {
|
||||
consent_request: '<?= base_url('AbdmHiu/consent_request') ?>',
|
||||
consent_status: '<?= base_url('AbdmHiu/consent_reconcile') ?>',
|
||||
consent_fetch: '<?= base_url('AbdmHiu/consent_request_fetch') ?>',
|
||||
hi_request: '<?= base_url('AbdmHiu/health_information_request') ?>',
|
||||
data_fetch: '<?= base_url('AbdmHiu/data_fetch') ?>'
|
||||
};
|
||||
var pollSummaryUrl = '<?= base_url('AbdmHiu/poll_summary') ?>';
|
||||
|
||||
var patientLookupUrl = '<?= base_url('AbdmHiu/patient_lookup') ?>';
|
||||
var docListUrl = '<?= base_url('AbdmHiu/documents_list') ?>';
|
||||
var docDetailBaseUrl = '<?= base_url('AbdmHiu/document_detail') ?>';
|
||||
var ajaxDocSelectedPatient = null;
|
||||
var ajaxDocItems = [];
|
||||
|
||||
function escHtml(v) {
|
||||
return (v || '').toString()
|
||||
|
|
@ -275,7 +383,7 @@
|
|||
var state = (consentStateText || '').trim();
|
||||
var lockedCreate = hasExistingConsentFlowContext();
|
||||
var policyNote = lockedCreate
|
||||
? '<span class="badge bg-warning text-dark ms-1">Policy: reuse logged consent flow (Status -> Fetch -> HI)</span>'
|
||||
? '<span class="badge bg-warning text-dark ms-1">Policy: reuse logged consent flow (Status -> Data Fetch)</span>'
|
||||
: '<span class="badge bg-light text-dark ms-1">Policy: create consent only once per active flow</span>';
|
||||
var cooldownNote = isAutoCoolingDown()
|
||||
? '<span class="badge bg-danger ms-1">Auto-next paused: ' + escHtml(autoCooldownReason || 'temporary cooldown') + '</span>'
|
||||
|
|
@ -310,13 +418,13 @@
|
|||
|
||||
function isValidConsentArtifactId(v) {
|
||||
var val = (v || '').toString().trim();
|
||||
return val !== '' && !isGatewayRequestId(val);
|
||||
return val !== '';
|
||||
}
|
||||
|
||||
function hasExistingConsentFlowContext() {
|
||||
var consentRequestRef = getFieldValue(['consent_request_id', 'consent_id']);
|
||||
var consentArtifactRef = getFieldValue(['consent_artifact_id', 'consent_id']);
|
||||
return (consentRequestRef !== '' && !isGatewayRequestId(consentRequestRef)) || isValidConsentArtifactId(consentArtifactRef);
|
||||
return consentRequestRef !== '' || isValidConsentArtifactId(consentArtifactRef);
|
||||
}
|
||||
|
||||
function canCreateConsentRequest(consentStateText) {
|
||||
|
|
@ -437,7 +545,7 @@
|
|||
transactionId: transactionId,
|
||||
abhaAddress: abhaAddress,
|
||||
hfrId: pickFirstNonEmpty([row && row.hfr_id, req.hfr_id]),
|
||||
consentRequestId: isGatewayRequestId(consentRequestId) ? '' : consentRequestId,
|
||||
consentRequestId: consentRequestId,
|
||||
consentArtifactId: isValidConsentArtifactId(consentArtifactIdRaw) ? consentArtifactIdRaw : '',
|
||||
operation: (row && row.operation ? row.operation : '').toString().toLowerCase(),
|
||||
workflowState: (row && row.workflow_state ? row.workflow_state : '').toString().toLowerCase(),
|
||||
|
|
@ -463,14 +571,14 @@
|
|||
if (wf === 'REQUESTED') {
|
||||
return 'REQUESTED';
|
||||
}
|
||||
if (wf === 'CONSENT_FETCHED') {
|
||||
return 'FETCHED';
|
||||
if (wf === 'CONSENT_FETCHED' || wf === 'DATA_RECEIVED') {
|
||||
return 'DATA_RECEIVED';
|
||||
}
|
||||
if (wf === 'STATUS_CHECKED') {
|
||||
return 'STATUS_CHECKED';
|
||||
}
|
||||
if (wf === 'DATA_REQUESTED' || wf === 'DATA_RECEIVED') {
|
||||
return 'FETCHED';
|
||||
return 'DATA_RECEIVED';
|
||||
}
|
||||
if ((ctx.status || '').toLowerCase() === 'failed') {
|
||||
return 'FAILED';
|
||||
|
|
@ -486,7 +594,7 @@
|
|||
cls = 'bg-success';
|
||||
} else if (['REQUESTED', 'PENDING', 'STATUS_CHECKED'].indexOf(s) !== -1) {
|
||||
cls = 'bg-warning text-dark';
|
||||
} else if (['FETCHED', 'CONSENT_FETCHED'].indexOf(s) !== -1) {
|
||||
} else if (['FETCHED', 'CONSENT_FETCHED', 'DATA_RECEIVED'].indexOf(s) !== -1) {
|
||||
cls = 'bg-info text-dark';
|
||||
} else if (['FAILED', 'DENIED', 'REVOKED', 'EXPIRED'].indexOf(s) !== -1) {
|
||||
cls = 'bg-danger';
|
||||
|
|
@ -567,10 +675,10 @@
|
|||
return { op: 'consent_status', label: 'Check Consent Status', cls: 'btn-outline-primary' };
|
||||
}
|
||||
if (status === 'GRANTED' || status === 'STATUS_CHECKED' || state === 'GRANTED' || state === 'STATUS_CHECKED') {
|
||||
return { op: 'consent_fetch', label: 'Access Consent Artifact', cls: 'btn-outline-primary' };
|
||||
return { op: 'data_fetch', label: 'Poll & Fetch Decrypted Data', cls: 'btn-outline-success' };
|
||||
}
|
||||
if (hasArtifact && (state === 'CONSENT_FETCHED' || status === 'FETCHED')) {
|
||||
return { op: 'hi_request', label: 'Request Health Information', cls: 'btn-outline-success' };
|
||||
if (hasArtifact && (state === 'CONSENT_FETCHED' || state === 'DATA_RECEIVED' || status === 'FETCHED' || status === 'DATA_RECEIVED')) {
|
||||
return { op: 'data_fetch', label: 'Poll & Fetch Decrypted Data', cls: 'btn-outline-success' };
|
||||
}
|
||||
|
||||
if (hasArtifact) {
|
||||
|
|
@ -659,19 +767,18 @@
|
|||
var hasArtifact = !!(latest && latest.consent_id);
|
||||
|
||||
var selectedAbha = (selectedPatient.abha_address || '').toString().trim();
|
||||
var hasHiRequest = (selectedItems || []).some(function (row) {
|
||||
var hasDataFetch = (selectedItems || []).some(function (row) {
|
||||
var ctx = extractWorkflowContext(row);
|
||||
var op = (row.operation || '').toString().toLowerCase();
|
||||
var status = (row.status || '').toString().toLowerCase();
|
||||
return ctx.abhaAddress === selectedAbha && op === 'hi_request' && status === 'success';
|
||||
return ctx.abhaAddress === selectedAbha && op === 'data_fetch' && status === 'success';
|
||||
});
|
||||
|
||||
var done = {
|
||||
1: true,
|
||||
2: hasConsentRequest,
|
||||
3: hasStatusChecked,
|
||||
4: hasArtifact,
|
||||
5: hasHiRequest
|
||||
4: hasDataFetch
|
||||
};
|
||||
|
||||
itemsHost.querySelectorAll('span[data-step]').forEach(function (el) {
|
||||
|
|
@ -725,8 +832,7 @@
|
|||
actions.push('<button type="button" class="btn btn-sm ' + ((nextAction && nextAction.op === 'consent_status') ? 'btn-primary' : 'btn-outline-primary') + '" data-row-action="consent_status" data-row-idx="' + rowIdx + '">Check Status</button>');
|
||||
}
|
||||
if (canFetch) {
|
||||
actions.push('<button type="button" class="btn btn-sm ' + ((nextAction && nextAction.op === 'consent_fetch') ? 'btn-primary' : 'btn-outline-primary') + '" data-row-action="consent_fetch" data-row-idx="' + rowIdx + '">Fetch Artifact</button>');
|
||||
actions.push('<button type="button" class="btn btn-sm ' + ((nextAction && nextAction.op === 'hi_request') ? 'btn-primary' : 'btn-outline-success') + '" data-row-action="hi_request" data-row-idx="' + rowIdx + '">Request HI</button>');
|
||||
actions.push('<button type="button" class="btn btn-sm ' + ((nextAction && nextAction.op === 'data_fetch') ? 'btn-primary' : 'btn-outline-success') + '" data-row-action="data_fetch" data-row-idx="' + rowIdx + '">Fetch Decrypted Data</button>');
|
||||
}
|
||||
|
||||
var tr = document.createElement('tr');
|
||||
|
|
@ -770,8 +876,8 @@
|
|||
if (ctx.lastError.indexOf('consent record not found') !== -1 && hasStatusRef) {
|
||||
return { op: 'consent_status', label: 'Check Consent Status' };
|
||||
}
|
||||
if (ctx.operation === 'hi_request' && hasArtifact) {
|
||||
return { op: 'hi_request', label: 'Retry Request HI' };
|
||||
if (ctx.operation === 'data_fetch' && hasArtifact) {
|
||||
return { op: 'data_fetch', label: 'Retry Data Fetch' };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -783,20 +889,16 @@
|
|||
if (ctx.workflowState === 'requested' && hasStatusRef) {
|
||||
return { op: 'consent_status', label: 'Check Consent Status' };
|
||||
}
|
||||
if ((ctx.workflowState === 'granted' || ctx.workflowState === 'approved' || ctx.workflowState === 'status_checked') && hasArtifact) {
|
||||
return { op: 'consent_fetch', label: 'Fetch Consent Artifact' };
|
||||
if ((ctx.workflowState === 'granted' || ctx.workflowState === 'approved' || ctx.workflowState === 'status_checked') && hasStatusRef) {
|
||||
return { op: 'data_fetch', label: 'Fetch Decrypted Data' };
|
||||
}
|
||||
if (hasStatusRef) {
|
||||
return { op: 'consent_status', label: 'Check Consent Status' };
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.operation === 'consent_fetch' && hasArtifact) {
|
||||
return { op: 'hi_request', label: 'Request Health Information' };
|
||||
}
|
||||
|
||||
if (ctx.operation === 'hi_request' && hasArtifact) {
|
||||
return { op: 'hi_request', label: 'Request Health Information' };
|
||||
if ((ctx.operation === 'consent_fetch' || ctx.operation === 'data_fetch') && hasArtifact) {
|
||||
return { op: 'data_fetch', label: 'Fetch Decrypted Data' };
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -827,7 +929,7 @@
|
|||
|
||||
if (status === 'failed') {
|
||||
if (error.indexOf('consentid is required') !== -1) {
|
||||
return { level: 'warning', message: 'Consent artifact id missing. Run Check Consent Status until consent is GRANTED, then Fetch Consent Artifact.' };
|
||||
return { level: 'warning', message: 'Consent id missing. Run Check Consent Status until consent is GRANTED, then fetch decrypted data.' };
|
||||
}
|
||||
if (error.indexOf('consent record not found') !== -1) {
|
||||
return { level: 'warning', message: 'Consent not found on gateway. Verify request_id/consentRequestId and run Check Consent Status again.' };
|
||||
|
|
@ -846,17 +948,14 @@
|
|||
return { level: 'info', message: 'Consent still REQUESTED. Wait for patient approval in ABDM app, then Check Consent Status again.' };
|
||||
}
|
||||
if ((state === 'granted' || state === 'approved' || state === 'status_checked') && hasArtifact) {
|
||||
return { level: 'success', message: 'Consent seems ready. Next step: ' + (nextAction ? nextAction.label : 'Fetch Consent Artifact') + ', then Request Health Information.' };
|
||||
return { level: 'success', message: 'Consent seems ready. Next step: ' + (nextAction ? nextAction.label : 'Fetch Decrypted Data') + '.' };
|
||||
}
|
||||
if (state === 'granted' || state === 'approved' || state === 'status_checked') {
|
||||
return { level: 'info', message: 'Consent state is ready, but artifact id is missing in this row. Use Latest Log Context and retry Fetch.' };
|
||||
return { level: 'info', message: 'Consent state is ready. Use Latest Log Context and run data fetch with consent id.' };
|
||||
}
|
||||
}
|
||||
if (operation === 'consent_fetch') {
|
||||
return { level: 'success', message: 'Consent artifact fetched. Next step: Request Health Information.' };
|
||||
}
|
||||
if (operation === 'hi_request') {
|
||||
return { level: 'success', message: 'Health information request sent. Next step: monitor timeline/poll summary for data updates.' };
|
||||
if (operation === 'consent_fetch' || operation === 'data_fetch') {
|
||||
return { level: 'success', message: 'Data polling executed. Monitor timeline/poll summary for decrypted records.' };
|
||||
}
|
||||
|
||||
return { level: 'muted', message: 'Use this row context and continue with the next HIU operation.' };
|
||||
|
|
@ -964,14 +1063,13 @@
|
|||
var hasPatient = !!selectedPatient;
|
||||
var consentRequestRef = getFieldValue(['consent_request_id', 'consent_id']);
|
||||
var consentArtifactRef = getFieldValue(['consent_artifact_id', 'consent_id']);
|
||||
var hasConsentRequestRef = (consentRequestRef !== '' && !isGatewayRequestId(consentRequestRef));
|
||||
var hasConsentArtifactRef = (consentArtifactRef !== '' && !isGatewayRequestId(consentArtifactRef));
|
||||
var hasConsentRequestRef = (consentRequestRef !== '');
|
||||
var hasConsentArtifactRef = (consentArtifactRef !== '');
|
||||
var canFetchByState = isConsentApproved(consentStateText) || isTerminalConsentState(consentStateText);
|
||||
|
||||
document.getElementById('btnConsentRequest').disabled = !canCreateConsentRequest(consentStateText);
|
||||
document.getElementById('btnConsentStatus').disabled = !hasConsentRequestRef;
|
||||
document.getElementById('btnConsentFetch').disabled = !(hasConsentArtifactRef && isSuccessConsentState(consentStateText) && canFetchByState);
|
||||
document.getElementById('btnHiRequest').disabled = !(hasConsentArtifactRef && isConsentApproved(consentStateText));
|
||||
document.getElementById('btnConsentFetch').disabled = !((hasConsentArtifactRef || hasConsentRequestRef) && isSuccessConsentState(consentStateText) && canFetchByState);
|
||||
updateReadinessBox(consentStateText);
|
||||
}
|
||||
|
||||
|
|
@ -986,7 +1084,7 @@
|
|||
var latest = p.latest_consent || {};
|
||||
var consentRequestRef = (latest.abdm_consent_request_id || '').toString();
|
||||
var consentArtifactRef = (latest.abdm_consent_artifact_id || latest.consent_id || '').toString();
|
||||
if (consentRequestRef && !isGatewayRequestId(consentRequestRef)) {
|
||||
if (consentRequestRef) {
|
||||
setFieldValue(['consent_request_id', 'consent_id'], consentRequestRef);
|
||||
}
|
||||
if (isValidConsentArtifactId(consentArtifactRef)) {
|
||||
|
|
@ -1088,6 +1186,43 @@
|
|||
return /^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$/.test((v || '').toString().trim());
|
||||
}
|
||||
|
||||
function normalizeConsentHiTypes(hiTypes) {
|
||||
var arr = Array.isArray(hiTypes) ? hiTypes : (hiTypes ? [hiTypes] : []);
|
||||
var map = {
|
||||
opconsultrecord: 'OPConsultation',
|
||||
opconsultation: 'OPConsultation',
|
||||
diagnosticreportrecord: 'DiagnosticReport',
|
||||
diagnosticreport: 'DiagnosticReport',
|
||||
prescriptionrecord: 'Prescription',
|
||||
prescription: 'Prescription',
|
||||
dischargesummaryrecord: 'DischargeSummary',
|
||||
dischargesummary: 'DischargeSummary',
|
||||
invoice: 'Invoice',
|
||||
invoicerecord: 'Invoice',
|
||||
healthdocumentrecord: 'HealthDocument',
|
||||
healthdocument: 'HealthDocument',
|
||||
immunization: 'ImmunizationRecord',
|
||||
immunizationrecord: 'ImmunizationRecord',
|
||||
wellness: 'Wellness',
|
||||
wellnessrecord: 'Wellness'
|
||||
};
|
||||
|
||||
var out = [];
|
||||
arr.forEach(function (item) {
|
||||
var raw = (item || '').toString().trim();
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
var key = raw.toLowerCase();
|
||||
var resolved = map[key] || raw;
|
||||
if (out.indexOf(resolved) === -1) {
|
||||
out.push(resolved);
|
||||
}
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildGatewayPayload(op, raw) {
|
||||
var requestId = (raw.requestId || raw.request_id || '').toString().trim();
|
||||
var timestamp = (raw.timestamp || '').toString().trim();
|
||||
|
|
@ -1141,6 +1276,11 @@
|
|||
throw new Error('Patient ABHA address must be in format like 91510165305101@sbx.');
|
||||
}
|
||||
|
||||
consent.hiTypes = normalizeConsentHiTypes(consent.hiTypes || []);
|
||||
if (!consent.hiTypes.length) {
|
||||
throw new Error('consent.hiTypes is required for consent request.');
|
||||
}
|
||||
|
||||
base.consent = consent;
|
||||
return base;
|
||||
}
|
||||
|
|
@ -1157,73 +1297,27 @@
|
|||
if (requestIdRef) {
|
||||
base.request_id = requestIdRef;
|
||||
}
|
||||
if (consentRequestId && !isGatewayRequestId(consentRequestId)) {
|
||||
if (consentRequestId) {
|
||||
base.consent_request_id = consentRequestId;
|
||||
}
|
||||
if (consentIdRef && !isGatewayRequestId(consentIdRef)) {
|
||||
if (consentIdRef) {
|
||||
base.consent_id = consentIdRef;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
if (op === 'consent_fetch') {
|
||||
var consentId = (raw.abdm_consent_artifact_id || raw.consentId || '').toString().trim();
|
||||
if (op === 'data_fetch') {
|
||||
var consentIdFetch = (raw.abdm_consent_artifact_id || raw.consentId || raw.consent_id || '').toString().trim();
|
||||
var consentRequestRefFetch = (raw.abdm_consent_request_id || raw.consentRequestId || raw.consent_request_id || '').toString().trim();
|
||||
|
||||
// Gateway accepts original consent request id in consent_id for NAT polling.
|
||||
var requestIdRefFetch = (raw.request_id || raw.requestId || '').toString().trim();
|
||||
var consentRequestIdFetch = (raw.abdm_consent_request_id || raw.consentRequestId || '').toString().trim();
|
||||
|
||||
if (consentId && isGatewayRequestId(consentId)) {
|
||||
throw new Error('mapping_error: consentId looks like gateway/local request_id (REQ-...). Use ABDM consent artifact id only.');
|
||||
var consentRef = consentIdFetch || consentRequestRefFetch || requestIdRefFetch;
|
||||
if (!consentRef) {
|
||||
throw new Error('consent_id is required for decrypted data fetch. Run status polling until consent is GRANTED.');
|
||||
}
|
||||
|
||||
if (consentId) {
|
||||
base.consentId = consentId;
|
||||
}
|
||||
if (requestIdRefFetch) {
|
||||
base.request_id = requestIdRefFetch;
|
||||
}
|
||||
if (consentRequestIdFetch && !isGatewayRequestId(consentRequestIdFetch)) {
|
||||
base.consent_request_id = consentRequestIdFetch;
|
||||
}
|
||||
|
||||
if (!base.consentId && !base.request_id && !base.consent_request_id) {
|
||||
throw new Error('No fetch context found. Select a consent row first, then click Access Consent Artifact.');
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
if (op === 'hi_request') {
|
||||
var hiRequest = raw.hiRequest;
|
||||
if (!hiRequest || typeof hiRequest !== 'object' || Array.isArray(hiRequest)) {
|
||||
hiRequest = {
|
||||
consent: {
|
||||
id: (raw.consentId || raw.consent_id || '').toString().trim()
|
||||
},
|
||||
dateRange: {
|
||||
from: new Date(Date.now() - (365 * 24 * 60 * 60 * 1000)).toISOString(),
|
||||
to: new Date().toISOString()
|
||||
},
|
||||
dataPushUrl: 'https://abdm-bridge.e-atria.in/api/v3/hiu/data/push',
|
||||
transactionId: (raw.transactionId || raw.transaction_id || '').toString().trim() || genReq('TXN-HIU')
|
||||
};
|
||||
}
|
||||
|
||||
if (!hiRequest.consent || typeof hiRequest.consent !== 'object') {
|
||||
hiRequest.consent = {};
|
||||
}
|
||||
if (!hiRequest.consent.id) {
|
||||
hiRequest.consent.id = (raw.consentId || raw.consent_id || '').toString().trim();
|
||||
}
|
||||
if (!(hiRequest.consent.id || '').toString().trim()) {
|
||||
throw new Error('hiRequest.consent.id is required for health information request.');
|
||||
}
|
||||
|
||||
if (!hiRequest.transactionId) {
|
||||
hiRequest.transactionId = (raw.transactionId || raw.transaction_id || '').toString().trim() || genReq('TXN-HIU');
|
||||
}
|
||||
setFieldValue(['transaction_id'], hiRequest.transactionId);
|
||||
|
||||
base.hiRequest = hiRequest;
|
||||
base.transactionId = hiRequest.transactionId;
|
||||
base.consent_id = consentRef;
|
||||
return base;
|
||||
}
|
||||
|
||||
|
|
@ -1231,9 +1325,92 @@
|
|||
}
|
||||
|
||||
function setResult(obj) {
|
||||
renderDataPreview(obj);
|
||||
document.getElementById('result_box').textContent = JSON.stringify(obj, null, 2);
|
||||
}
|
||||
|
||||
function parseMaybeJson(raw) {
|
||||
if (raw && typeof raw === 'object') {
|
||||
return raw;
|
||||
}
|
||||
var txt = (raw || '').toString().trim();
|
||||
if (!txt) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(txt);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractSessionsFromResponse(res) {
|
||||
var data = (res && typeof res === 'object') ? (res.data || {}) : {};
|
||||
var sessions = [];
|
||||
|
||||
if (Array.isArray(data.sessions)) {
|
||||
sessions = data.sessions;
|
||||
} else if (Array.isArray(res.sessions)) {
|
||||
sessions = res.sessions;
|
||||
}
|
||||
|
||||
return sessions;
|
||||
}
|
||||
|
||||
function summarizeBundleType(decryptedItem) {
|
||||
var parsed = parseMaybeJson((decryptedItem || {}).decrypted_data);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return '-';
|
||||
}
|
||||
var bundleType = (parsed.type || '').toString().trim();
|
||||
var resourceType = (parsed.resourceType || '').toString().trim();
|
||||
if (resourceType && bundleType) {
|
||||
return resourceType + ' / ' + bundleType;
|
||||
}
|
||||
return resourceType || bundleType || '-';
|
||||
}
|
||||
|
||||
function renderDataPreview(res) {
|
||||
var card = document.getElementById('data_preview_card');
|
||||
var meta = document.getElementById('data_preview_meta');
|
||||
var tbody = document.querySelector('#data_preview_table tbody');
|
||||
if (!card || !meta || !tbody) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sessions = extractSessionsFromResponse(res);
|
||||
if (!sessions.length) {
|
||||
card.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
card.classList.remove('d-none');
|
||||
var totalRecords = 0;
|
||||
sessions.forEach(function (s) {
|
||||
var items = Array.isArray(s.decrypted_data) ? s.decrypted_data : [];
|
||||
totalRecords += items.length;
|
||||
});
|
||||
meta.textContent = 'Sessions: ' + sessions.length + ' | Records: ' + totalRecords;
|
||||
|
||||
tbody.innerHTML = '';
|
||||
sessions.forEach(function (session) {
|
||||
var records = Array.isArray(session.decrypted_data) ? session.decrypted_data : [];
|
||||
var contexts = records.map(function (r) {
|
||||
return (r.careContextReference || '').toString().trim();
|
||||
}).filter(function (v) { return v !== ''; });
|
||||
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '' +
|
||||
'<td>' + escHtml((session.transaction_id || '-').toString()) + '</td>' +
|
||||
'<td>' + escHtml((session.consent_id || '-').toString()) + '</td>' +
|
||||
'<td>' + escHtml((session.status || '-').toString()) + '</td>' +
|
||||
'<td>' + records.length + '</td>' +
|
||||
'<td>' + escHtml((contexts.slice(0, 3).join(', ') || '-').toString()) + '</td>' +
|
||||
'<td>' + escHtml(summarizeBundleType(records[0] || {})) + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function postJson(url, body) {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
|
|
@ -1247,6 +1424,238 @@
|
|||
});
|
||||
}
|
||||
|
||||
function parseDisplayDate(v) {
|
||||
var txt = (v || '').toString().trim();
|
||||
if (!txt) {
|
||||
return '-';
|
||||
}
|
||||
var d = new Date(txt.replace(' ', 'T'));
|
||||
if (isNaN(d.getTime())) {
|
||||
return txt;
|
||||
}
|
||||
return d.toLocaleString();
|
||||
}
|
||||
|
||||
function openAjaxDocsPanel() {
|
||||
var panel = document.getElementById('ajax_docs_panel');
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
panel.classList.remove('d-none');
|
||||
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
if (!ajaxDocSelectedPatient) {
|
||||
ajaxDocsSearchPatients();
|
||||
}
|
||||
}
|
||||
|
||||
function closeAjaxDocsPanel() {
|
||||
var panel = document.getElementById('ajax_docs_panel');
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
panel.classList.add('d-none');
|
||||
}
|
||||
|
||||
function ajaxDocsRenderPatients(items) {
|
||||
var tbody = document.querySelector('#ajax_doc_patient_table tbody');
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = '';
|
||||
if (!items.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-muted text-center">No patients found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(function (p, idx) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '' +
|
||||
'<td><button class="btn btn-sm btn-outline-primary" data-ajax-doc-patient-idx="' + idx + '">Select</button></td>' +
|
||||
'<td>' + escHtml(p.patient_name || '') + '</td>' +
|
||||
'<td>' + escHtml(p.patient_code || '') + '</td>' +
|
||||
'<td>' + escHtml(p.abha_address || '') + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('button[data-ajax-doc-patient-idx]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var idx = parseInt(btn.getAttribute('data-ajax-doc-patient-idx'), 10);
|
||||
if (Number.isNaN(idx) || !items[idx]) {
|
||||
return;
|
||||
}
|
||||
ajaxDocSelectedPatient = items[idx];
|
||||
var status = document.getElementById('ajax_doc_patient_status');
|
||||
if (status) {
|
||||
status.textContent = 'Selected: ' + (ajaxDocSelectedPatient.patient_name || '-') + ' | ABHA: ' + (ajaxDocSelectedPatient.abha_address || '-');
|
||||
}
|
||||
ajaxDocsLoadDocuments();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ajaxDocsSearchPatients() {
|
||||
var q = getFieldValue(['ajax_doc_patient_search']);
|
||||
fetch(patientLookupUrl + '?q=' + encodeURIComponent(q), {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if ((res.ok || 0) !== 1) {
|
||||
var status = document.getElementById('ajax_doc_patient_status');
|
||||
if (status) {
|
||||
status.textContent = res.error || 'Patient search failed';
|
||||
}
|
||||
return;
|
||||
}
|
||||
ajaxDocsRenderPatients(Array.isArray(res.items) ? res.items : []);
|
||||
var s = document.getElementById('ajax_doc_patient_status');
|
||||
if (s) {
|
||||
s.textContent = (res.count || 0) + ' patient(s) found';
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
var status = document.getElementById('ajax_doc_patient_status');
|
||||
if (status) {
|
||||
status.textContent = e.message || 'Patient search failed';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ajaxDocsRenderList(items) {
|
||||
ajaxDocItems = items || [];
|
||||
var tbody = document.querySelector('#ajax_doc_table tbody');
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (!ajaxDocItems.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-muted text-center">No documents loaded.</td></tr>';
|
||||
var box = document.getElementById('ajax_doc_detail_box');
|
||||
if (box) {
|
||||
box.innerHTML = '<div class="text-muted">No documents found for selected patient.</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ajaxDocItems.forEach(function (doc, idx) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.style.cursor = 'pointer';
|
||||
tr.setAttribute('data-ajax-doc-idx', idx);
|
||||
tr.innerHTML = '' +
|
||||
'<td>' + escHtml(parseDisplayDate(doc.document_date || doc.created_at)) + '</td>' +
|
||||
'<td>' + escHtml(doc.document_title || '-') + '</td>' +
|
||||
'<td>' + escHtml(doc.care_context_reference || '-') + '</td>' +
|
||||
'<td>' + escHtml(doc.practitioner_name || '-') + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
tbody.querySelectorAll('tr[data-ajax-doc-idx]').forEach(function (row) {
|
||||
row.addEventListener('click', function () {
|
||||
var idx = parseInt(row.getAttribute('data-ajax-doc-idx'), 10);
|
||||
if (Number.isNaN(idx) || !ajaxDocItems[idx]) {
|
||||
return;
|
||||
}
|
||||
ajaxDocsLoadDetail((ajaxDocItems[idx].id || 0));
|
||||
});
|
||||
});
|
||||
|
||||
ajaxDocsLoadDetail((ajaxDocItems[0].id || 0));
|
||||
}
|
||||
|
||||
function ajaxDocsLoadDocuments() {
|
||||
if (!ajaxDocSelectedPatient) {
|
||||
setResult({ ok: 0, message: 'Select a patient first in fetched documents panel.' });
|
||||
return;
|
||||
}
|
||||
|
||||
var q = getFieldValue(['ajax_doc_search']);
|
||||
var url = docListUrl
|
||||
+ '?patient_id=' + encodeURIComponent(ajaxDocSelectedPatient.patient_id || 0)
|
||||
+ '&abha_address=' + encodeURIComponent(ajaxDocSelectedPatient.abha_address || '')
|
||||
+ '&q=' + encodeURIComponent(q);
|
||||
|
||||
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if ((res.ok || 0) !== 1) {
|
||||
ajaxDocsRenderList([]);
|
||||
return;
|
||||
}
|
||||
ajaxDocsRenderList(Array.isArray(res.items) ? res.items : []);
|
||||
})
|
||||
.catch(function () {
|
||||
ajaxDocsRenderList([]);
|
||||
});
|
||||
}
|
||||
|
||||
function ajaxDocsRenderBulletList(title, rows, formatter) {
|
||||
var list = Array.isArray(rows) ? rows : [];
|
||||
if (!list.length) {
|
||||
return '<div class="mb-2"><div class="fw-semibold">' + escHtml(title) + '</div><div class="text-muted">No data</div></div>';
|
||||
}
|
||||
var html = '<div class="mb-2"><div class="fw-semibold">' + escHtml(title) + '</div><ul class="mb-0">';
|
||||
list.forEach(function (row) {
|
||||
html += '<li>' + formatter(row) + '</li>';
|
||||
});
|
||||
html += '</ul></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function ajaxDocsLoadDetail(id) {
|
||||
var box = document.getElementById('ajax_doc_detail_box');
|
||||
if (!box) {
|
||||
return;
|
||||
}
|
||||
if (!id) {
|
||||
box.innerHTML = '<div class="text-muted">Select a document to view detail.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
box.innerHTML = '<div class="text-muted">Loading...</div>';
|
||||
fetch(docDetailBaseUrl + '/' + encodeURIComponent(id), {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if ((res.ok || 0) !== 1) {
|
||||
box.innerHTML = '<div class="text-danger">Unable to load detail.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var item = res.item || {};
|
||||
var s = item.summary || {};
|
||||
var html = ''
|
||||
+ '<div class="row g-2 mb-3">'
|
||||
+ '<div class="col-md-6"><span class="text-muted">Patient:</span> <strong>' + escHtml(s.patient_name || item.patient_name || '-') + '</strong></div>'
|
||||
+ '<div class="col-md-6"><span class="text-muted">ABHA:</span> ' + escHtml(item.abha_address || '-') + '</div>'
|
||||
+ '<div class="col-md-6"><span class="text-muted">Document:</span> ' + escHtml(item.document_title || '-') + '</div>'
|
||||
+ '<div class="col-md-6"><span class="text-muted">Date:</span> ' + escHtml(parseDisplayDate(item.document_date || '')) + '</div>'
|
||||
+ '<div class="col-md-6"><span class="text-muted">Doctor:</span> ' + escHtml(item.practitioner_name || '-') + '</div>'
|
||||
+ '<div class="col-md-6"><span class="text-muted">Organization:</span> ' + escHtml(item.organization_name || '-') + '</div>'
|
||||
+ '<div class="col-md-6"><span class="text-muted">Care Context:</span> ' + escHtml(item.care_context_reference || '-') + '</div>'
|
||||
+ '<div class="col-md-6"><span class="text-muted">Bundle:</span> ' + escHtml(item.bundle_type || '-') + '</div>'
|
||||
+ '</div>';
|
||||
|
||||
html += ajaxDocsRenderBulletList('Diagnoses', s.conditions || [], function (row) {
|
||||
return escHtml((row && row.text) || '-');
|
||||
});
|
||||
html += ajaxDocsRenderBulletList('Vitals', s.vitals || [], function (row) {
|
||||
return escHtml(((row && row.name) || '-') + ': ' + ((row && row.value) || '-'));
|
||||
});
|
||||
html += ajaxDocsRenderBulletList('Medications', s.medications || [], function (row) {
|
||||
var name = (row && row.name) || '-';
|
||||
var dose = (row && row.dose) || '';
|
||||
return escHtml(name + (dose ? (' | ' + dose) : ''));
|
||||
});
|
||||
|
||||
box.innerHTML = html;
|
||||
})
|
||||
.catch(function (e) {
|
||||
box.innerHTML = '<div class="text-danger">' + escHtml(e.message || 'Unable to load detail') + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function run(op, options) {
|
||||
options = options || {};
|
||||
var url = opRoute[op];
|
||||
|
|
@ -1278,7 +1687,7 @@
|
|||
if ((res.ok || 0) === 1) {
|
||||
var data = res.data || {};
|
||||
var consentReqRef = (data.abdm_consent_request_id || data.consentRequestId || data.consent_request_id || '').toString();
|
||||
if (consentReqRef && !isGatewayRequestId(consentReqRef)) {
|
||||
if (consentReqRef) {
|
||||
setFieldValue(['consent_request_id', 'consent_id'], consentReqRef);
|
||||
}
|
||||
|
||||
|
|
@ -1294,37 +1703,32 @@
|
|||
startConsentPolling();
|
||||
}
|
||||
|
||||
// Ordered workflow automation: status success -> fetch artifact, then fetch success -> request HI.
|
||||
if (op === 'consent_status' && isSuccessConsentState(state) && isValidConsentArtifactId(consentArtifactRef)) {
|
||||
// Ordered workflow automation: status success -> data fetch polling.
|
||||
if (op === 'consent_status' && isSuccessConsentState(state) && (isValidConsentArtifactId(consentArtifactRef) || consentReqRef)) {
|
||||
if (isAutoNextStepEnabled() && !isAutoCoolingDown()) {
|
||||
run('consent_fetch', { silent: true });
|
||||
run('data_fetch', { silent: true });
|
||||
}
|
||||
}
|
||||
if (op === 'consent_fetch' && isConsentApproved(state) && isValidConsentArtifactId(consentArtifactRef)) {
|
||||
if (op === 'data_fetch' && isConsentApproved(state) && isValidConsentArtifactId(consentArtifactRef)) {
|
||||
// Always stop status polling once artifact fetch succeeds.
|
||||
stopConsentPolling();
|
||||
if (isAutoNextStepEnabled() && !isAutoCoolingDown()) {
|
||||
run('hi_request', { silent: true });
|
||||
}
|
||||
}
|
||||
if (op === 'hi_request') {
|
||||
clearAutoCooldown();
|
||||
}
|
||||
if ((op === 'consent_status' || op === 'consent_fetch') && isTerminalConsentState(state)) {
|
||||
if ((op === 'consent_status' || op === 'data_fetch') && isTerminalConsentState(state)) {
|
||||
stopConsentPolling();
|
||||
}
|
||||
} else if (op === 'consent_status' || op === 'consent_fetch') {
|
||||
} else if (op === 'consent_status' || op === 'data_fetch') {
|
||||
var errState = resolveConsentStateFromResponse(res, res.data || {});
|
||||
var errText = (res.error || ((res.data || {}).error_text) || '').toString().toLowerCase();
|
||||
if (isTerminalConsentState(errState) || errText.indexOf('mapping_error') !== -1) {
|
||||
stopConsentPolling();
|
||||
}
|
||||
} else if (op === 'hi_request') {
|
||||
var hiErrText = (res.error || ((res.data || {}).error_text) || '').toString().toLowerCase();
|
||||
var hiCode = parseInt(((res.data || {}).http_code || res.http_code || 0), 10);
|
||||
if (hiCode === 403 || hiErrText.indexOf('cloudfront') !== -1 || hiErrText.indexOf('request blocked') !== -1) {
|
||||
setAutoCooldown(180000, 'HI request blocked (403). Retry after cooldown or after gateway unblocks endpoint.');
|
||||
syncButtons();
|
||||
if (op === 'data_fetch') {
|
||||
var dataCode = parseInt(((res.data || {}).http_code || res.http_code || 0), 10);
|
||||
if (dataCode === 403 || errText.indexOf('cloudfront') !== -1 || errText.indexOf('request blocked') !== -1) {
|
||||
setAutoCooldown(180000, 'Data fetch blocked (403). Retry after cooldown or after gateway unblocks endpoint.');
|
||||
syncButtons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1531,6 +1935,40 @@
|
|||
if (btnRefreshTimeline) {
|
||||
btnRefreshTimeline.addEventListener('click', fetchTimeline);
|
||||
}
|
||||
var btnOpenDocumentsAjax = document.getElementById('btnOpenDocumentsAjax');
|
||||
if (btnOpenDocumentsAjax) {
|
||||
btnOpenDocumentsAjax.addEventListener('click', openAjaxDocsPanel);
|
||||
}
|
||||
var btnCloseAjaxDocs = document.getElementById('btnCloseAjaxDocs');
|
||||
if (btnCloseAjaxDocs) {
|
||||
btnCloseAjaxDocs.addEventListener('click', closeAjaxDocsPanel);
|
||||
}
|
||||
var btnAjaxDocPatientSearch = document.getElementById('btnAjaxDocPatientSearch');
|
||||
if (btnAjaxDocPatientSearch) {
|
||||
btnAjaxDocPatientSearch.addEventListener('click', ajaxDocsSearchPatients);
|
||||
}
|
||||
var btnAjaxDocSearch = document.getElementById('btnAjaxDocSearch');
|
||||
if (btnAjaxDocSearch) {
|
||||
btnAjaxDocSearch.addEventListener('click', ajaxDocsLoadDocuments);
|
||||
}
|
||||
var ajaxDocPatientSearch = document.getElementById('ajax_doc_patient_search');
|
||||
if (ajaxDocPatientSearch) {
|
||||
ajaxDocPatientSearch.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
ajaxDocsSearchPatients();
|
||||
}
|
||||
});
|
||||
}
|
||||
var ajaxDocSearch = document.getElementById('ajax_doc_search');
|
||||
if (ajaxDocSearch) {
|
||||
ajaxDocSearch.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
ajaxDocsLoadDocuments();
|
||||
}
|
||||
});
|
||||
}
|
||||
var btnUseLatestLog = document.getElementById('btnUseLatestLog');
|
||||
if (btnUseLatestLog) {
|
||||
btnUseLatestLog.addEventListener('click', function () {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,18 @@ $backTitle = trim((string) ($backTitle ?? 'Profile'));
|
|||
if ($backUrl === '') {
|
||||
$backUrl = base_url('billing/patient/person_record') . '/' . (int) ($patient->id ?? 0) . '/0';
|
||||
}
|
||||
$patientAbhaId = trim((string) ($patient->abha_id ?? $patient->abha_no ?? $patient->abha ?? ''));
|
||||
$patientAbhaAddress = trim((string) ($patient->abha_address ?? ''));
|
||||
if ($patientAbhaAddress === '' && $patientAbhaId !== '' && strpos($patientAbhaId, '@') !== false) {
|
||||
$patientAbhaAddress = $patientAbhaId;
|
||||
}
|
||||
if ($patientAbhaAddress === '' && preg_match('/abha_address\s*:\s*([A-Za-z0-9._-]+@[A-Za-z0-9.-]+)/i', (string) ($patient->log ?? ''), $abhaLogMatch) === 1) {
|
||||
$patientAbhaAddress = trim((string) ($abhaLogMatch[1] ?? ''));
|
||||
}
|
||||
$abhaVerifiedStatus = strtoupper(trim((string) ($patient->abha_verified_status ?? '')));
|
||||
$abhaKycVerified = (int) ($patient->abha_kyc_verified ?? 0) === 1;
|
||||
$abhaMobileVerified = (int) ($patient->abha_mobile_verified ?? 0) === 1;
|
||||
$abhaIsVerified = $abhaVerifiedStatus === 'VERIFIED' || ($abhaKycVerified && $abhaMobileVerified);
|
||||
?>
|
||||
|
||||
<div class="pagetitle">
|
||||
|
|
@ -30,6 +42,18 @@ if ($backUrl === '') {
|
|||
<div class="card-body">
|
||||
<p class="mb-3">Patient: <strong><?= esc($patient->p_fname ?? '') ?></strong></p>
|
||||
|
||||
<ul class="nav nav-tabs nav-tabs-bordered" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#opd-history-tab" type="button" role="tab">OPD History</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#opd-abdm-tab" type="button" role="tab">ABDM Fetched Data</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content pt-2">
|
||||
<div class="tab-pane fade show active" id="opd-history-tab" role="tabpanel">
|
||||
|
||||
<style>
|
||||
.opd-history-file-card {
|
||||
border: 1px solid #dee2e6;
|
||||
|
|
@ -79,6 +103,28 @@ if ($backUrl === '') {
|
|||
.opd-scan-modal-img.is-dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.abdm-doc-detail-list {
|
||||
max-height: 45vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.abdm-flow-step {
|
||||
font-size: 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
color: #6c757d;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.abdm-flow-step.is-active {
|
||||
color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
background: #e7f1ff;
|
||||
}
|
||||
.abdm-flow-step.is-done {
|
||||
color: #198754;
|
||||
border-color: #198754;
|
||||
background: #e8f7ee;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php if (empty($opdGroups)) { ?>
|
||||
|
|
@ -174,6 +220,75 @@ if ($backUrl === '') {
|
|||
</div>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="opd-abdm-tab" role="tabpanel">
|
||||
<div class="alert alert-light border d-flex justify-content-between align-items-center flex-wrap gap-2 mt-2">
|
||||
<div>
|
||||
<div class="fw-semibold">ABHA Context</div>
|
||||
<div class="small text-muted">
|
||||
ABHA ID: <strong><?= $patientAbhaId !== '' ? esc($patientAbhaId) : 'Not available' ?></strong>
|
||||
|
|
||||
ABHA Address: <strong><?= $patientAbhaAddress !== '' ? esc($patientAbhaAddress) : 'Not available' ?></strong>
|
||||
|
|
||||
Status:
|
||||
<?php if ($abhaIsVerified) { ?>
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">VERIFIED</span>
|
||||
<?php } else { ?>
|
||||
<span class="badge bg-warning-subtle text-warning border border-warning-subtle"><?= $abhaVerifiedStatus !== '' ? esc($abhaVerifiedStatus) : 'UNVERIFIED' ?></span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="btnLoadAbdmDocs">Refresh Fetched Data</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btnAutoAbdmFlow" <?= ($abhaIsVerified && $patientAbhaAddress !== '') ? '' : 'disabled' ?>>Start ABDM Sync</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="abdmStatusBox" class="small text-muted mb-2">Click "Refresh Fetched Data" to load ABDM records mapped to this patient.</div>
|
||||
|
||||
<div class="border rounded p-2 mb-3 bg-light">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-2" id="abdmFlowSteps">
|
||||
<span class="abdm-flow-step" data-step="1">1. Requested</span>
|
||||
<span class="abdm-flow-step" data-step="2">2. Granted</span>
|
||||
<span class="abdm-flow-step" data-step="3">3. Fetched</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div id="abdmFlowProgressBar" class="progress-bar" role="progressbar" style="width:0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-5">
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<input id="abdmDocSearch" class="form-control" placeholder="Search title, care context, doctor">
|
||||
<button class="btn btn-outline-secondary" type="button" id="btnSearchAbdmDocs">Search</button>
|
||||
</div>
|
||||
<div class="table-responsive border rounded">
|
||||
<table class="table table-sm table-hover mb-0" id="abdmDocTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Title</th>
|
||||
<th>Care Context</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="3" class="text-muted text-center">No records loaded.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7">
|
||||
<div class="border rounded p-3 bg-light" id="abdmDocDetailBox">
|
||||
<div class="text-muted">Select a fetched document to view details.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -199,6 +314,16 @@ if ($backUrl === '') {
|
|||
|
||||
<script>
|
||||
$(function() {
|
||||
var patientId = <?= (int) ($patient->id ?? 0) ?>;
|
||||
var abdmDocumentsUrl = '<?= base_url('billing/patient/abdm_documents/' . (int) ($patient->id ?? 0)) ?>';
|
||||
var abdmAutoFlowUrl = '<?= base_url('billing/patient/abdm_content_auto_flow/' . (int) ($patient->id ?? 0)) ?>';
|
||||
var abdmDocDetailBaseUrl = '<?= base_url('billing/patient/abdm_document_detail/' . (int) ($patient->id ?? 0)) ?>';
|
||||
var currentAbdmRows = [];
|
||||
var autoFlowTimer = null;
|
||||
var autoFlowRequestId = '';
|
||||
var autoFlowAttempts = 0;
|
||||
var autoFlowMaxAttempts = 15;
|
||||
|
||||
var zoom = 1;
|
||||
var minZoom = 0.5;
|
||||
var maxZoom = 3;
|
||||
|
|
@ -295,5 +420,257 @@ $(function() {
|
|||
isDragging = false;
|
||||
$('#opdScanModalImg').removeClass('is-dragging');
|
||||
});
|
||||
|
||||
function escHtml(val) {
|
||||
return $('<div>').text(val == null ? '' : String(val)).html();
|
||||
}
|
||||
|
||||
function fmtDateTime(val) {
|
||||
var text = (val || '').toString().trim();
|
||||
if (!text) {
|
||||
return '-';
|
||||
}
|
||||
return text.replace('T', ' ');
|
||||
}
|
||||
|
||||
function renderAbdmRows(rows) {
|
||||
var html = '';
|
||||
if (!rows || !rows.length) {
|
||||
html = '<tr><td colspan="3" class="text-muted text-center">No ABDM fetched records found.</td></tr>';
|
||||
$('#abdmDocTable tbody').html(html);
|
||||
$('#abdmDocDetailBox').html('<div class="text-muted">No records available for this patient.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
rows.forEach(function(row) {
|
||||
html += '<tr class="abdm-doc-row" data-id="' + escHtml(row.id) + '">' +
|
||||
'<td>' + escHtml(fmtDateTime(row.document_date || row.created_at)) + '</td>' +
|
||||
'<td>' + escHtml(row.document_title || '-') + '</td>' +
|
||||
'<td>' + escHtml(row.care_context_reference || '-') + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
$('#abdmDocTable tbody').html(html);
|
||||
}
|
||||
|
||||
function renderAbdmDetail(item) {
|
||||
if (!item) {
|
||||
$('#abdmDocDetailBox').html('<div class="text-muted">Document details unavailable.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
var summary = item.summary || {};
|
||||
var conditions = Array.isArray(summary.conditions) ? summary.conditions : [];
|
||||
var vitals = Array.isArray(summary.vitals) ? summary.vitals : [];
|
||||
var meds = Array.isArray(summary.medications) ? summary.medications : [];
|
||||
|
||||
var html = '';
|
||||
html += '<div class="fw-semibold mb-2">' + escHtml(item.document_title || 'ABDM Document') + '</div>';
|
||||
html += '<div class="small text-muted mb-2">Date: ' + escHtml(fmtDateTime(item.document_date || item.created_at)) + '</div>';
|
||||
html += '<div class="small mb-2">Care Context: <strong>' + escHtml(item.care_context_reference || '-') + '</strong></div>';
|
||||
html += '<div class="small mb-2">Doctor: <strong>' + escHtml(item.practitioner_name || '-') + '</strong></div>';
|
||||
html += '<div class="small mb-3">Organization: <strong>' + escHtml(item.organization_name || '-') + '</strong></div>';
|
||||
|
||||
html += '<div class="row g-2">';
|
||||
html += '<div class="col-md-4"><div class="border rounded p-2 h-100"><div class="fw-semibold small mb-1">Diagnoses</div><ul class="mb-0 small abdm-doc-detail-list">';
|
||||
if (conditions.length) {
|
||||
conditions.forEach(function(c) {
|
||||
html += '<li>' + escHtml(c.text || '-') + '</li>';
|
||||
});
|
||||
} else {
|
||||
html += '<li class="text-muted">No diagnosis entries</li>';
|
||||
}
|
||||
html += '</ul></div></div>';
|
||||
|
||||
html += '<div class="col-md-4"><div class="border rounded p-2 h-100"><div class="fw-semibold small mb-1">Vitals</div><ul class="mb-0 small abdm-doc-detail-list">';
|
||||
if (vitals.length) {
|
||||
vitals.forEach(function(v) {
|
||||
html += '<li>' + escHtml((v.name || '-') + ': ' + (v.value || '-')) + '</li>';
|
||||
});
|
||||
} else {
|
||||
html += '<li class="text-muted">No vitals</li>';
|
||||
}
|
||||
html += '</ul></div></div>';
|
||||
|
||||
html += '<div class="col-md-4"><div class="border rounded p-2 h-100"><div class="fw-semibold small mb-1">Medications</div><ul class="mb-0 small abdm-doc-detail-list">';
|
||||
if (meds.length) {
|
||||
meds.forEach(function(m) {
|
||||
html += '<li>' + escHtml((m.name || '-') + (m.dose ? (' | ' + m.dose) : '')) + '</li>';
|
||||
});
|
||||
} else {
|
||||
html += '<li class="text-muted">No medications</li>';
|
||||
}
|
||||
html += '</ul></div></div>';
|
||||
html += '</div>';
|
||||
|
||||
$('#abdmDocDetailBox').html(html);
|
||||
}
|
||||
|
||||
function loadAbdmDocs() {
|
||||
var q = ($('#abdmDocSearch').val() || '').toString().trim();
|
||||
var url = abdmDocumentsUrl + '?limit=200';
|
||||
if (q !== '') {
|
||||
url += '&q=' + encodeURIComponent(q);
|
||||
}
|
||||
$('#abdmStatusBox').removeClass('text-danger').addClass('text-muted').text('Loading ABDM records...');
|
||||
|
||||
fetch(url, { credentials: 'same-origin' })
|
||||
.then(function(resp) { return resp.json(); })
|
||||
.then(function(data) {
|
||||
if (!data || data.ok !== 1) {
|
||||
throw new Error((data && data.error) || 'Unable to load ABDM records.');
|
||||
}
|
||||
currentAbdmRows = Array.isArray(data.items) ? data.items : [];
|
||||
renderAbdmRows(currentAbdmRows);
|
||||
$('#abdmStatusBox').removeClass('text-danger').addClass('text-muted').text('Loaded ' + currentAbdmRows.length + ' fetched ABDM record(s).');
|
||||
})
|
||||
.catch(function(err) {
|
||||
$('#abdmStatusBox').removeClass('text-muted').addClass('text-danger').text('ABDM load failed: ' + (err.message || err));
|
||||
});
|
||||
}
|
||||
|
||||
function setFlowProgress(step) {
|
||||
var current = Number(step || 0);
|
||||
if (current < 0) {
|
||||
current = 0;
|
||||
}
|
||||
if (current > 3) {
|
||||
current = 3;
|
||||
}
|
||||
|
||||
var width = 0;
|
||||
if (current === 1) {
|
||||
width = 34;
|
||||
} else if (current === 2) {
|
||||
width = 67;
|
||||
} else if (current >= 3) {
|
||||
width = 100;
|
||||
}
|
||||
|
||||
$('#abdmFlowProgressBar').css('width', width + '%');
|
||||
|
||||
$('#abdmFlowSteps .abdm-flow-step').each(function() {
|
||||
var $el = $(this);
|
||||
var stepNo = Number($el.data('step') || 0);
|
||||
$el.removeClass('is-active is-done');
|
||||
if (stepNo < current) {
|
||||
$el.addClass('is-done');
|
||||
} else if (stepNo === current && current > 0) {
|
||||
$el.addClass('is-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function stopAutoFlowLoop() {
|
||||
if (autoFlowTimer) {
|
||||
clearTimeout(autoFlowTimer);
|
||||
autoFlowTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function runAutoFlowStep() {
|
||||
var url = abdmAutoFlowUrl;
|
||||
if (autoFlowRequestId) {
|
||||
url += '?request_id=' + encodeURIComponent(autoFlowRequestId);
|
||||
}
|
||||
|
||||
fetch(url, { credentials: 'same-origin' })
|
||||
.then(function(resp) { return resp.json(); })
|
||||
.then(function(data) {
|
||||
if (!data || data.ok !== 1) {
|
||||
throw new Error((data && data.error) || 'Auto flow failed.');
|
||||
}
|
||||
|
||||
autoFlowRequestId = (data.request_id || autoFlowRequestId || '').toString();
|
||||
var phase = (data.phase || '').toString();
|
||||
var msg = (data.message || '').toString();
|
||||
var info = msg !== '' ? msg : ('Phase: ' + (phase || 'UNKNOWN'));
|
||||
|
||||
if (phase === 'COMPLETED') {
|
||||
setFlowProgress(3);
|
||||
} else if (phase === 'GRANTED') {
|
||||
setFlowProgress(2);
|
||||
} else if (phase === 'REQUESTED' || phase === 'PENDING') {
|
||||
setFlowProgress(1);
|
||||
}
|
||||
|
||||
$('#abdmStatusBox').removeClass('text-danger').addClass('text-muted').text(info + (autoFlowRequestId ? (' | Request ID: ' + autoFlowRequestId) : ''));
|
||||
|
||||
var shouldPoll = Number(data.poll_again || 0) === 1;
|
||||
if (shouldPoll && autoFlowAttempts < autoFlowMaxAttempts) {
|
||||
autoFlowAttempts += 1;
|
||||
autoFlowTimer = setTimeout(runAutoFlowStep, 8000);
|
||||
return;
|
||||
}
|
||||
|
||||
$('#btnAutoAbdmFlow').prop('disabled', false).text('One-Click Sync');
|
||||
if (phase === 'COMPLETED') {
|
||||
loadAbdmDocs();
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
stopAutoFlowLoop();
|
||||
$('#btnAutoAbdmFlow').prop('disabled', false).text('One-Click Sync');
|
||||
$('#abdmStatusBox').removeClass('text-muted').addClass('text-danger').text('Auto flow failed: ' + (err.message || err));
|
||||
});
|
||||
}
|
||||
|
||||
function loadAbdmDocDetail(docId) {
|
||||
if (!docId) {
|
||||
return;
|
||||
}
|
||||
fetch(abdmDocDetailBaseUrl + '/' + encodeURIComponent(docId), { credentials: 'same-origin' })
|
||||
.then(function(resp) { return resp.json(); })
|
||||
.then(function(data) {
|
||||
if (!data || data.ok !== 1) {
|
||||
throw new Error((data && data.error) || 'Unable to load document detail.');
|
||||
}
|
||||
renderAbdmDetail(data.item || null);
|
||||
})
|
||||
.catch(function(err) {
|
||||
$('#abdmDocDetailBox').html('<div class="text-danger">' + escHtml('Detail load failed: ' + (err.message || err)) + '</div>');
|
||||
});
|
||||
}
|
||||
|
||||
$(document).off('click.abdmOpd', '#btnLoadAbdmDocs').on('click.abdmOpd', '#btnLoadAbdmDocs', function() {
|
||||
loadAbdmDocs();
|
||||
});
|
||||
|
||||
$(document).off('click.abdmOpd', '#btnSearchAbdmDocs').on('click.abdmOpd', '#btnSearchAbdmDocs', function() {
|
||||
loadAbdmDocs();
|
||||
});
|
||||
|
||||
$(document).off('keypress.abdmOpd', '#abdmDocSearch').on('keypress.abdmOpd', '#abdmDocSearch', function(e) {
|
||||
if (e.which === 13) {
|
||||
e.preventDefault();
|
||||
loadAbdmDocs();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).off('click.abdmOpd', '#abdmDocTable .abdm-doc-row').on('click.abdmOpd', '#abdmDocTable .abdm-doc-row', function() {
|
||||
$('#abdmDocTable .abdm-doc-row').removeClass('table-active');
|
||||
$(this).addClass('table-active');
|
||||
var docId = $(this).data('id');
|
||||
loadAbdmDocDetail(docId);
|
||||
});
|
||||
|
||||
$(document).off('click.abdmOpd', '#btnAutoAbdmFlow').on('click.abdmOpd', '#btnAutoAbdmFlow', function() {
|
||||
var $btn = $(this);
|
||||
if ($btn.prop('disabled')) {
|
||||
return;
|
||||
}
|
||||
stopAutoFlowLoop();
|
||||
autoFlowRequestId = '';
|
||||
autoFlowAttempts = 0;
|
||||
setFlowProgress(1);
|
||||
$btn.prop('disabled', true).text('Sync Running...');
|
||||
$('#abdmStatusBox').removeClass('text-danger').addClass('text-muted').text('Starting one-click sync flow...');
|
||||
runAutoFlowStep();
|
||||
});
|
||||
|
||||
$(document).off('shown.bs.tab.abdmOpd', '[data-bs-target="#opd-abdm-tab"]').on('shown.bs.tab.abdmOpd', '[data-bs-target="#opd-abdm-tab"]', function() {
|
||||
if (!currentAbdmRows.length) {
|
||||
loadAbdmDocs();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user