diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 924594b..3e5b041 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1647,10 +1647,10 @@ $routes->get('app/nursing', 'Api\v1\NursingApi::pwaIndex'); $routes->get('app/nursing/(:any)', 'Api\v1\NursingApi::pwaIndex'); $routes->get('app/doctor', 'Api\v1\DoctorApi::pwaIndex'); $routes->get('app/doctor/(:any)', 'Api\v1\DoctorApi::pwaIndex'); -$routes->match(['get', 'head'], 'app/queue', 'Api\v1\DoctorApi::queueIndex'); -$routes->match(['get', 'head'], 'app/queue/sw.js', 'Api\v1\DoctorApi::queueSw'); -$routes->match(['get', 'head'], 'app/queue/manifest.json', 'Api\v1\DoctorApi::queueManifest'); -$routes->match(['get', 'head'], 'app/queue/(:any)', 'Api\v1\DoctorApi::queueIndex'); +$routes->match(['GET', 'HEAD'], 'app/queue', 'Api\v1\DoctorApi::queueIndex'); +$routes->match(['GET', 'HEAD'], 'app/queue/sw.js', 'Api\v1\DoctorApi::queueSw'); +$routes->match(['GET', 'HEAD'], 'app/queue/manifest.json', 'Api\v1\DoctorApi::queueManifest'); +$routes->match(['GET', 'HEAD'], 'app/queue/(:any)', 'Api\v1\DoctorApi::queueIndex'); // Dedicated RESTful JSON API v1 Group for React PWAs & Mobile Apps $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static function ($routes) { diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php index 1881e08..0393f80 100644 --- a/app/Controllers/Ipd_discharge.php +++ b/app/Controllers/Ipd_discharge.php @@ -9845,10 +9845,17 @@ class Ipd_discharge extends BaseController 'updated_at' => date('Y-m-d H:i:s'), ]; + $hrFields = $this->db->getFieldNames('health_records') ?? []; + if (! empty($hrFields)) { + $hrData = array_intersect_key($hrData, array_flip($hrFields)); + } + if (! empty($existingHr)) { $this->db->table('health_records')->where('id', (int) $existingHr['id'])->update($hrData); } else { - $hrData['created_at'] = date('Y-m-d H:i:s'); + if (in_array('created_at', $hrFields, true)) { + $hrData['created_at'] = date('Y-m-d H:i:s'); + } $this->db->table('health_records')->insert($hrData); } } diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index 0a53a3a..6459712 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -2116,19 +2116,30 @@ class Patient extends BaseController $nowIst = new \DateTimeImmutable('now', $istTz); try { - $fromIst = $dateFromInput !== '' ? new \DateTimeImmutable($dateFromInput, $istTz) : $nowIst->modify('-365 days'); + if ($dateFromInput !== '') { + $fromIst = new \DateTimeImmutable($dateFromInput, $istTz); + if (strpos($dateFromInput, 'T') === false && strpos($dateFromInput, ' ') === false) { + $fromIst = $fromIst->setTime(0, 0, 0); + } + } else { + $fromIst = $nowIst->modify('-365 days')->setTime(0, 0, 0); + } } catch (\Throwable $e) { return $this->response->setStatusCode(422)->setJSON([ 'ok' => 0, - 'error' => 'Invalid "Date From" date.', + 'error' => 'Invalid "Date From" date/time.', ]); } try { - $toIst = $dateToInput !== '' ? new \DateTimeImmutable($dateToInput, $istTz) : $nowIst; + if ($dateToInput !== '') { + $toIst = new \DateTimeImmutable($dateToInput, $istTz); + } else { + $toIst = $nowIst; + } } catch (\Throwable $e) { return $this->response->setStatusCode(422)->setJSON([ 'ok' => 0, - 'error' => 'Invalid "Date To" date.', + 'error' => 'Invalid "Date To" date/time.', ]); } if ($fromIst->getTimestamp() >= $toIst->getTimestamp()) { @@ -2139,14 +2150,21 @@ class Patient extends BaseController } try { - $eraseIst = $eraseDateInput !== '' ? new \DateTimeImmutable($eraseDateInput, $istTz) : $nowIst->modify('+1 year'); + if ($eraseDateInput !== '') { + $eraseIst = new \DateTimeImmutable($eraseDateInput, $istTz); + if (strpos($eraseDateInput, 'T') === false && strpos($eraseDateInput, ' ') === false) { + $eraseIst = $eraseIst->setTime(23, 59, 59); + } + } else { + $eraseIst = $nowIst->modify('+1 year')->setTime(23, 59, 59); + } } catch (\Throwable $e) { return $this->response->setStatusCode(422)->setJSON([ 'ok' => 0, - 'error' => 'Invalid "Expiry Date" date.', + 'error' => 'Invalid "Expiry Date" date/time.', ]); } - if ($eraseIst->setTime(23, 59, 59)->getTimestamp() < $toIst->getTimestamp()) { + if ($eraseIst->getTimestamp() < $toIst->getTimestamp()) { return $this->response->setStatusCode(422)->setJSON([ 'ok' => 0, 'error' => '"Expiry Date" must be on or after "Date To".', @@ -2158,9 +2176,9 @@ class Patient extends BaseController $toIst = $safeNowIst; } - $fromUtc = $fromIst->setTime(0, 0, 0)->setTimezone($utcTz)->format('Y-m-d\TH:i:s.000\Z'); + $fromUtc = $fromIst->setTimezone($utcTz)->format('Y-m-d\TH:i:s.000\Z'); $toUtc = $toIst->setTimezone($utcTz)->format('Y-m-d\TH:i:s.000\Z'); - $eraseAtUtc = $eraseIst->setTime(23, 59, 59)->setTimezone($utcTz)->format('Y-m-d\TH:i:s.000\Z'); + $eraseAtUtc = $eraseIst->setTimezone($utcTz)->format('Y-m-d\TH:i:s.000\Z'); $consent = [ 'purpose' => [ @@ -2384,52 +2402,41 @@ class Patient extends BaseController } // If we already have a granted snapshot with a concrete consent reference, - // continue directly with fetch instead of re-reconciling a stale request_id. + // check if records exist or let background worker fetch them without blocking the browser. if ($resumedFromLastSync && strtoupper(trim((string) ($lastSync['phase'] ?? ''))) === 'GRANTED') { if ($consentArtifactRef === '' && $consentRequestRef === '') { $consentRequestRef = trim((string) ($lastSync['consent_request_id'] ?? '')); $consentArtifactRef = trim((string) ($lastSync['consent_id'] ?? '')); } - $canDirectFetch = ($consentArtifactRef !== '' && preg_match('/^REQ-/i', $consentArtifactRef) !== 1) - || ($consentRequestRef !== '' && preg_match('/^REQ-/i', $consentRequestRef) !== 1); - - if ($canDirectFetch) { - $fetchPayload = [ - 'abha_address' => $abhaContext['abha_address'], - ]; - if ($consentArtifactRef !== '' && preg_match('/^REQ-/i', $consentArtifactRef) !== 1) { - $fetchPayload['consentId'] = $consentArtifactRef; - } elseif ($consentRequestRef !== '' && preg_match('/^REQ-/i', $consentRequestRef) !== 1) { - $fetchPayload['consentRequestId'] = $consentRequestRef; - } - - 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) { - return $this->response->setJSON([ - 'ok' => 1, - 'phase' => 'COMPLETED', - 'poll_again' => 0, - 'request_id' => $flowRefId, - 'message' => 'Resumed previous granted consent flow and fetched data successfully.', - 'data' => [ - 'consent_request' => $consentResult, - 'consent_reconcile' => $reconcileResult, - 'data_fetch' => $fetchResult, - ], - ]); - } + $docCount = $this->countAbdmDocumentsForPatient($pno, $consentRequestRef); + if ($docCount > 0) { + return $this->response->setJSON([ + 'ok' => 1, + 'phase' => 'COMPLETED', + 'poll_again' => 0, + 'request_id' => $flowRefId, + 'message' => 'Resumed previous granted consent flow and ' . $docCount . ' record(s) are available.', + 'data' => [ + 'consent_request' => $consentResult, + 'consent_reconcile' => $reconcileResult, + ], + ]); } + + // Records not yet stored locally; trigger background HIU poll so browser doesn't block + $this->triggerBackgroundHiuPoll(); + return $this->response->setJSON([ + 'ok' => 1, + 'phase' => 'GRANTED', + 'poll_again' => 1, + 'request_id' => $flowRefId, + 'message' => 'Consent is granted. Records are being fetched in the background...', + 'data' => [ + 'consent_request' => $consentResult, + 'consent_reconcile' => $reconcileResult, + ], + ]); } $reconcilePayload = [ @@ -2497,88 +2504,37 @@ class Patient extends BaseController $reconcileResult = $fallbackReconcile; $state = $this->deriveAutoFlowState($reconcileResult); if ($state === 'GRANTED') { - $fetchPayload = [ - 'abha_address' => $abhaContext['abha_address'], - 'consentId' => $consentArtifactRef, - ]; - if ($fetchPayload['consentId'] === '') { - unset($fetchPayload['consentId']); - if ($consentRequestRef !== '') { - $fetchPayload['consentRequestId'] = $consentRequestRef; - } - } - - 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 after fallback reconcile: ' . $e->getMessage(), - 'request_id' => $flowRefId, - ]); - } - - if ((int) ($fetchResult['ok'] ?? 0) === 1) { + $docCount = $this->countAbdmDocumentsForPatient($pno, $consentRequestRef); + if ($docCount > 0) { return $this->response->setJSON([ 'ok' => 1, 'phase' => 'COMPLETED', 'poll_again' => 0, 'request_id' => $flowRefId, - 'message' => 'Consent status was resolved using fallback bridge reference and data fetched successfully.', + 'message' => 'Consent resolved and ' . $docCount . ' record(s) are available.', 'data' => [ 'consent_request' => $consentResult, 'consent_reconcile' => $reconcileResult, - 'data_fetch' => $fetchResult, ], ]); } + + $this->triggerBackgroundHiuPoll(); + return $this->response->setJSON([ + 'ok' => 1, + 'phase' => 'GRANTED', + 'poll_again' => 1, + 'request_id' => $flowRefId, + 'message' => 'Consent granted. Records are being fetched in the background...', + 'data' => [ + 'consent_request' => $consentResult, + 'consent_reconcile' => $reconcileResult, + ], + ]); } } } - $altConsentRequestRef = trim((string) ($consentRequestRef !== '' ? $consentRequestRef : ($lastSync['consent_request_id'] ?? ''))); - $altConsentArtifactRef = trim((string) ($consentArtifactRef !== '' ? $consentArtifactRef : ($lastSync['consent_id'] ?? ''))); - $canDirectFetch = ($altConsentArtifactRef !== '' && preg_match('/^REQ-/i', $altConsentArtifactRef) !== 1) - || ($altConsentRequestRef !== '' && preg_match('/^REQ-/i', $altConsentRequestRef) !== 1); - - if ($canDirectFetch) { - $fetchPayload = [ - 'abha_address' => $abhaContext['abha_address'], - ]; - if ($altConsentArtifactRef !== '' && preg_match('/^REQ-/i', $altConsentArtifactRef) !== 1) { - $fetchPayload['consentId'] = $altConsentArtifactRef; - } elseif ($altConsentRequestRef !== '' && preg_match('/^REQ-/i', $altConsentRequestRef) !== 1) { - $fetchPayload['consentRequestId'] = $altConsentRequestRef; - } - - 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) { - return $this->response->setJSON([ - 'ok' => 1, - 'phase' => 'COMPLETED', - 'poll_again' => 0, - 'request_id' => $flowRefId, - 'message' => 'Consent status lookup returned not found for old request id, but data fetch succeeded using saved consent reference.', - 'data' => [ - 'consent_request' => $consentResult, - 'consent_reconcile' => $reconcileResult, - 'data_fetch' => $fetchResult, - ], - ]); - } - } - return $this->response->setJSON([ 'ok' => 1, 'phase' => 'REQUESTED', @@ -2613,64 +2569,28 @@ class Patient extends BaseController $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; + $docCount = $this->countAbdmDocumentsForPatient($pno, $consentRequestRef); + if ($docCount > 0) { + $state = 'COMPLETED'; + $pollAgain = false; + $message = 'Data fetch completed and ' . $docCount . ' document(s) are now available.'; } 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; - } - - $fetchError = (string) ($fetchResult['error_text'] ?? 'Data fetch failed.'); - if ($this->isPendingBridgeSessionError($fetchError, $httpCode)) { - return $this->response->setJSON([ - 'ok' => 1, - 'phase' => 'GRANTED', - 'poll_again' => 1, - 'request_id' => $flowRefId, - 'message' => 'Consent granted. Waiting for the health records to arrive from the facilities.', - 'data' => [ - 'consent_request' => $consentResult, - 'consent_reconcile' => $reconcileResult, - 'data_fetch' => $fetchResult, - ], - ]); - } - - return $this->response->setStatusCode($httpCode)->setJSON([ - 'ok' => 0, - 'phase' => 'FETCH_FAILED', - 'error' => $fetchError, + // Consent is granted in PHR, but documents are not yet downloaded/decrypted. + // Trigger background HIU poll worker so the browser does NOT hang on synchronous data_fetch. + $this->triggerBackgroundHiuPoll(); + + return $this->response->setJSON([ + 'ok' => 1, + 'phase' => 'GRANTED', + 'poll_again' => 1, 'request_id' => $flowRefId, + 'message' => 'Consent granted. Health records are being fetched in the background...', 'data' => [ 'consent_request' => $consentResult, 'consent_reconcile' => $reconcileResult, - 'data_fetch' => $fetchResult, ], ]); } - - $state = 'COMPLETED'; - $pollAgain = false; } $message = match ($state) { @@ -5608,9 +5528,15 @@ class Patient extends BaseController $containers = [ is_array($rowReq) ? ($rowReq['consent'] ?? null) : null, is_array($rowReq) ? ($rowReq['consentDetail'] ?? null) : null, + is_array($rowReq) ? ($rowReq['data']['consent'] ?? null) : null, + is_array($rowReq) ? ($rowReq['data']['consentDetail'] ?? null) : null, + is_array($rowReq) ? ($rowReq['data'] ?? null) : null, is_array($rowReq) ? $rowReq : null, is_array($rowResp) ? ($rowResp['consent'] ?? null) : null, is_array($rowResp) ? ($rowResp['consentDetail'] ?? null) : null, + is_array($rowResp) ? ($rowResp['data']['consent'] ?? null) : null, + is_array($rowResp) ? ($rowResp['data']['consentDetail'] ?? null) : null, + is_array($rowResp) ? ($rowResp['data'] ?? null) : null, is_array($rowResp) ? $rowResp : null, ]; @@ -5624,6 +5550,10 @@ class Patient extends BaseController ?? $c['permission']['date_range']['from'] ?? $c['date_range']['from'] ?? $c['dateRange']['from'] + ?? $c['valid_from'] + ?? $c['validFrom'] + ?? $c['date_from'] + ?? $c['dateFrom'] ?? '' )); } @@ -5633,6 +5563,10 @@ class Patient extends BaseController ?? $c['permission']['date_range']['to'] ?? $c['date_range']['to'] ?? $c['dateRange']['to'] + ?? $c['valid_to'] + ?? $c['validTo'] + ?? $c['date_to'] + ?? $c['dateTo'] ?? '' )); } @@ -5641,6 +5575,8 @@ class Patient extends BaseController $c['permission']['dataEraseAt'] ?? $c['permission']['data_erase_at'] ?? $c['expiry'] + ?? $c['erase_at'] + ?? $c['eraseAt'] ?? $c['dataEraseAt'] ?? $c['data_erase_at'] ?? '' @@ -6280,5 +6216,39 @@ class Patient extends BaseController 'csrfHash' => csrf_hash(), ]); } + + private function countAbdmDocumentsForPatient(int $pno, string $consentRequestRef = ''): int + { + if (! $this->db->tableExists('abdm_hiu_documents')) { + return 0; + } + + $builder = $this->db->table('abdm_hiu_documents')->where('patient_id', $pno); + if ($consentRequestRef !== '') { + $builder->groupStart() + ->where('consent_request_id', $consentRequestRef) + ->orWhere('consent_ref', $consentRequestRef) + ->groupEnd(); + } + + return (int) $builder->countAllResults(); + } + + private function triggerBackgroundHiuPoll(): void + { + try { + $sparkPath = ROOTPATH . 'spark'; + if (DIRECTORY_SEPARATOR === '\\') { + $cmd = 'start /B php "' . $sparkPath . '" abdm:hiu-poll --limit=10 > NUL 2>&1'; + @pclose(@popen($cmd, 'r')); + } else { + $phpBin = is_executable('/usr/bin/php8.3') ? '/usr/bin/php8.3' : (is_executable('/usr/bin/php') ? '/usr/bin/php' : 'php'); + $cmd = $phpBin . ' "' . $sparkPath . '" abdm:hiu-poll --limit=10 > /dev/null 2>&1 &'; + @exec($cmd); + } + } catch (\Throwable $e) { + log_message('warning', '[triggerBackgroundHiuPoll] ' . $e->getMessage()); + } + } } diff --git a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php index c2fe027..53dc926 100644 --- a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php +++ b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php @@ -1360,10 +1360,35 @@ class AbdmTaskBoardSyncService // 6. IPD Discharge: ipd_discharge if ($entityType === 'ipd_discharge' || $taskType === 'ipd_discharge_publish') { if ($dbConn->tableExists('ipd_discharge')) { - $dRow = $dbConn->table('ipd_discharge')->select('discharge_date, created_at')->where('id', $entityId)->get(1)->getRowArray(); - $date = ! empty($dRow['discharge_date']) ? $dRow['discharge_date'] : ($dRow['created_at'] ?? null); - if (! empty($date) && strtotime((string) $date) > 0) { - return (string) $date; + $dFields = $dbConn->getFieldNames('ipd_discharge') ?? []; + $sel = in_array('ipd_id', $dFields, true) ? 'id, ipd_id' : 'id'; + if (in_array('created_datetime', $dFields, true)) { + $sel .= ', created_datetime'; + } + $dRow = $dbConn->table('ipd_discharge')->select($sel)->where('id', $entityId)->get(1)->getRowArray(); + $targetIpdId = ! empty($dRow['ipd_id']) ? (int) $dRow['ipd_id'] : (int) $entityId; + if ($dbConn->tableExists('ipd_master')) { + $mFields = $dbConn->getFieldNames('ipd_master') ?? []; + $mSel = []; + if (in_array('discharge_date', $mFields, true)) { + $mSel[] = 'discharge_date'; + } + if (in_array('discharge_time', $mFields, true)) { + $mSel[] = 'discharge_time'; + } + if (! empty($mSel)) { + $mRow = $dbConn->table('ipd_master')->select(implode(', ', $mSel))->where('id', $targetIpdId)->get(1)->getRowArray(); + if (! empty($mRow['discharge_date']) && strtotime((string) $mRow['discharge_date']) > 0) { + $dt = (string) $mRow['discharge_date']; + if (! empty($mRow['discharge_time'])) { + $dt .= ' ' . $mRow['discharge_time']; + } + return $dt; + } + } + } + if (! empty($dRow['created_datetime']) && strtotime((string) $dRow['created_datetime']) > 0) { + return (string) $dRow['created_datetime']; } } } diff --git a/app/Views/billing/Patient_Profile_Opd_V.php b/app/Views/billing/Patient_Profile_Opd_V.php index 3204290..c2b3431 100644 --- a/app/Views/billing/Patient_Profile_Opd_V.php +++ b/app/Views/billing/Patient_Profile_Opd_V.php @@ -1061,15 +1061,15 @@ if ($patientPhotoPath === '') {