feat(abdm): enforce consent expiration, request-specific printing, and push sync enhancements
This commit is contained in:
parent
7472364002
commit
39017a43cf
|
|
@ -108,6 +108,25 @@ class AbdmPushSync extends BaseCommand
|
|||
$patientId = (int) ($row['patient_id'] ?? 0);
|
||||
$patient = $this->loadPatientDemographics($db, $patientId);
|
||||
$abhaId = trim((string) ($row['abha_id'] ?? ''));
|
||||
$hiType = (string) ($row['hi_type'] ?? '');
|
||||
$ccRef = (string) ($row['care_context_reference'] ?? '');
|
||||
if ($hiType === '') {
|
||||
$hiType = (str_starts_with($ccRef, 'PRESC-') || str_starts_with($ccRef, 'RX-'))
|
||||
? 'PrescriptionRecord'
|
||||
: 'OPConsultRecord';
|
||||
}
|
||||
$vDate = substr((string) ($row['created_at'] ?? date('Y-m-d')), 0, 10);
|
||||
$dateStr = date('d M Y', strtotime($vDate));
|
||||
$ccDisplay = match ($hiType) {
|
||||
'PrescriptionRecord' => 'Prescription - ' . $dateStr,
|
||||
'OPConsultRecord' => 'Consultation Record - ' . $dateStr,
|
||||
'DischargeSummaryRecord' => 'Discharge Summary - ' . $dateStr,
|
||||
'DiagnosticReportRecord' => 'Diagnostic Report - ' . $dateStr,
|
||||
'ImmunizationRecord' => 'Immunization Record - ' . $dateStr,
|
||||
'WellnessRecord' => 'Wellness Record - ' . $dateStr,
|
||||
'InvoiceRecord' => 'Invoice - ' . $dateStr,
|
||||
default => (string) ($row['care_context_display'] ?? ($hiType . ' ' . $dateStr)),
|
||||
};
|
||||
|
||||
try {
|
||||
$result = $connector->pushRecord([
|
||||
|
|
@ -117,11 +136,11 @@ class AbdmPushSync extends BaseCommand
|
|||
'abha_address' => str_contains($abhaId, '@') ? $abhaId : $patient['abha_address'],
|
||||
'gender' => $patient['gender'],
|
||||
'year_of_birth' => $patient['year_of_birth'],
|
||||
'hi_type' => (string) ($row['hi_type'] ?? ''),
|
||||
'record_type' => (string) ($row['hi_type'] ?? ''),
|
||||
'visit_date' => substr((string) ($row['created_at'] ?? date('Y-m-d')), 0, 10),
|
||||
'care_context_reference' => (string) ($row['care_context_reference'] ?? ''),
|
||||
'care_context_display' => (string) ($row['care_context_display'] ?? (($row['hi_type'] ?? '') . ' ' . substr((string) ($row['created_at'] ?? ''), 0, 10))),
|
||||
'hi_type' => $hiType,
|
||||
'record_type' => $hiType,
|
||||
'visit_date' => $vDate,
|
||||
'care_context_reference' => $ccRef,
|
||||
'care_context_display' => $ccDisplay,
|
||||
'record_data' => $bundle,
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1730,7 +1730,7 @@ class AbdmGateway extends BaseController
|
|||
$healthRecordId = $this->storeHealthRecord([
|
||||
'patient_id' => $patientId,
|
||||
'abha_id' => $hasAbha ? $abhaId : '',
|
||||
'hi_type' => 'OPConsultRecord',
|
||||
'hi_type' => $hiType,
|
||||
'entity_type' => 'opd',
|
||||
'entity_id' => (string) $opdId,
|
||||
'fhir_bundle' => $bundleJson,
|
||||
|
|
@ -5649,15 +5649,20 @@ class AbdmGateway extends BaseController
|
|||
$bundleData = json_decode((string) $hrRow['record_data'], true);
|
||||
if (is_array($bundleData)) {
|
||||
$recHiType = $hrRow['hi_type'] ?? $hrRow['record_type'] ?? 'OPConsultRecord';
|
||||
if (str_starts_with($ref, 'PRESC-') || str_starts_with($ref, 'RX-')) {
|
||||
$recHiType = 'PrescriptionRecord';
|
||||
} elseif (str_starts_with($ref, 'OPD-')) {
|
||||
$recHiType = 'OPConsultRecord';
|
||||
}
|
||||
$defaultDisp = match ($recHiType) {
|
||||
'ImmunizationRecord' => 'Immunization Record',
|
||||
'ImmunizationRecord' => 'Immunization Record',
|
||||
'DiagnosticReportRecord' => 'Diagnostic Report',
|
||||
'DischargeSummaryRecord' => 'Discharge Summary',
|
||||
'PrescriptionRecord' => 'Prescription Record',
|
||||
'WellnessRecord' => 'Wellness Record',
|
||||
'InvoiceRecord' => 'Invoice Record',
|
||||
'HealthDocumentRecord' => 'Health Document',
|
||||
default => 'Consultation Record',
|
||||
'PrescriptionRecord' => 'Prescription Record',
|
||||
'WellnessRecord' => 'Wellness Record',
|
||||
'InvoiceRecord' => 'Invoice Record',
|
||||
'HealthDocumentRecord' => 'Health Document',
|
||||
default => 'Consultation Record',
|
||||
};
|
||||
$disp = ! empty($hrRow['care_context_display']) ? $hrRow['care_context_display'] : $defaultDisp;
|
||||
if ($recHiType === 'ImmunizationRecord' && ! empty($hrRow['entity_id']) && $db->tableExists('immunization_records')) {
|
||||
|
|
@ -5964,7 +5969,13 @@ class AbdmGateway extends BaseController
|
|||
$targetOpdId = 0;
|
||||
|
||||
if ($targetSessionId > 0 && $db->tableExists('opd_prescription')) {
|
||||
$pRow = $db->table('opd_prescription')->select('opd_id')->where('id', $targetSessionId)->get(1)->getRowArray();
|
||||
$pRow = $db->table('opd_prescription')
|
||||
->select('opd_id')
|
||||
->groupStart()
|
||||
->where('id', $targetSessionId)
|
||||
->orWhere('session_id', $targetSessionId)
|
||||
->groupEnd()
|
||||
->get(1)->getRowArray();
|
||||
$targetOpdId = (int) ($pRow['opd_id'] ?? 0);
|
||||
}
|
||||
if ($targetOpdId <= 0 && $targetPatientId > 0 && $db->tableExists('opd_master')) {
|
||||
|
|
@ -6020,13 +6031,12 @@ class AbdmGateway extends BaseController
|
|||
$bundleJson = json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$exHr = $db->table('health_records')->where('care_context_reference', $ref)->get(1)->getRowArray();
|
||||
if ($exHr) {
|
||||
if (empty($exHr['record_data'])) {
|
||||
$db->table('health_records')->where('id', (int) $exHr['id'])->update([
|
||||
'record_data' => $bundleJson,
|
||||
'push_status' => 'linked',
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
$db->table('health_records')->where('id', (int) $exHr['id'])->update([
|
||||
'hi_type' => $hiType,
|
||||
'record_data' => $bundleJson,
|
||||
'push_status' => 'linked',
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
} else {
|
||||
$db->table('health_records')->insert([
|
||||
'care_context_reference' => $ref,
|
||||
|
|
@ -7385,9 +7395,9 @@ class AbdmGateway extends BaseController
|
|||
$ccRef = 'HR-' . (int) ($row['id'] ?? 0);
|
||||
}
|
||||
$rowEntType = strtolower(trim((string) ($row['entity_type'] ?? '')));
|
||||
// Skip redundant/legacy separate PRESC- and WELLNESS- contexts (now unified under OPD-),
|
||||
// but allow genuine standalone nursing Wellness records (entity_type = 'wellness')
|
||||
if ($rowEntType !== 'wellness' && (str_starts_with($ccRef, 'PRESC-') || str_starts_with($ccRef, 'WELLNESS-'))) {
|
||||
// Allow genuine separate PRESC- contexts (PrescriptionRecord),
|
||||
// but skip redundant/legacy WELLNESS- aliases (now unified under OPD- or patient_wellness_records)
|
||||
if ($rowEntType !== 'wellness' && str_starts_with($ccRef, 'WELLNESS-')) {
|
||||
continue;
|
||||
}
|
||||
// OPD- and INVOICE- contexts are enriched with doctor/session/bill details in steps 4 & 6
|
||||
|
|
@ -7725,6 +7735,20 @@ class AbdmGateway extends BaseController
|
|||
'is_fhir_ready' => $hasFindings || $hasMeds || $hasVitals,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
|
||||
if ($hasMeds) {
|
||||
$prescCcRef = 'PRESC-' . $patientId . '-S' . $sessionId . '-' . $cleanDate;
|
||||
$prescDisplay = 'Prescription - ' . $dateStr;
|
||||
$addContext([
|
||||
'careContextId' => $prescCcRef,
|
||||
'referenceNumber' => $prescCcRef,
|
||||
'display' => $prescDisplay,
|
||||
'record_type' => 'PrescriptionRecord',
|
||||
'patient_id' => $patientId,
|
||||
'is_fhir_ready' => true,
|
||||
'is_primary' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2016,6 +2016,50 @@ class Patient extends BaseController
|
|||
// Resolve all related umbrella and artifact IDs bidirectionally so
|
||||
// documents from BOTH external and same-entity facilities are returned.
|
||||
$sessionIds = $this->resolveAllRelatedConsentIds($filterConsentRequestId);
|
||||
|
||||
// ABDM M3 HIU compliance: check if this consent request is expired
|
||||
$wfCheck = $this->db->table('abdm_hiu_workflows')
|
||||
->select(['id', 'workflow_state', 'request_json', 'response_json', 'expired_at'])
|
||||
->groupStart()
|
||||
->where('request_id', $filterConsentRequestId)
|
||||
->orWhere('consent_id', $filterConsentRequestId)
|
||||
->orWhereIn('abdm_consent_request_id', $sessionIds !== [] ? $sessionIds : [$filterConsentRequestId])
|
||||
->orWhereIn('abdm_consent_artifact_id', $sessionIds !== [] ? $sessionIds : [$filterConsentRequestId])
|
||||
->groupEnd()
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
$isFilterExpired = false;
|
||||
if (is_array($wfCheck)) {
|
||||
if (strtoupper(trim((string) ($wfCheck['workflow_state'] ?? ''))) === 'EXPIRED') {
|
||||
$isFilterExpired = true;
|
||||
} else {
|
||||
$filterEraseAt = $this->extractDataEraseAtFromWorkflowRow($wfCheck);
|
||||
if ($filterEraseAt !== '') {
|
||||
$filterTs = strtotime($filterEraseAt);
|
||||
if ($filterTs !== false && $filterTs <= time()) {
|
||||
$isFilterExpired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isFilterExpired) {
|
||||
// Purge cached records for this expired consent as per ABDM dataEraseAt guidelines
|
||||
$purgeRefs = array_unique(array_merge([$filterConsentRequestId], $sessionIds));
|
||||
$this->purgeAbdmDocumentsByRefs($purgeRefs);
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'count' => 0,
|
||||
'items' => [],
|
||||
'expired' => 1,
|
||||
'error' => 'This consent request has expired. Health records are erased and no longer accessible as per ABDM guidelines.',
|
||||
'abha' => $abhaContext,
|
||||
'last_sync' => $lastSync,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($sessionIds !== []) {
|
||||
$builder->groupStart();
|
||||
$hasClause = false;
|
||||
|
|
@ -2171,6 +2215,28 @@ class Patient extends BaseController
|
|||
} elseif ($consentRequestId !== '') {
|
||||
$docFields = $this->db->getFieldNames('abdm_hiu_documents') ?? [];
|
||||
$sessionIds = $this->resolveAllRelatedConsentIds($consentRequestId);
|
||||
|
||||
// ABDM M3 HIU compliance: check if consent has expired
|
||||
$wfCheck = $this->db->table('abdm_hiu_workflows')
|
||||
->select(['id', 'workflow_state', 'request_json', 'response_json', 'expired_at'])
|
||||
->groupStart()
|
||||
->where('request_id', $consentRequestId)
|
||||
->orWhere('consent_id', $consentRequestId)
|
||||
->orWhereIn('abdm_consent_request_id', $sessionIds !== [] ? $sessionIds : [$consentRequestId])
|
||||
->orWhereIn('abdm_consent_artifact_id', $sessionIds !== [] ? $sessionIds : [$consentRequestId])
|
||||
->groupEnd()
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (is_array($wfCheck)) {
|
||||
$eraseAt = $this->extractDataEraseAtFromWorkflowRow($wfCheck);
|
||||
if (strtoupper(trim((string) ($wfCheck['workflow_state'] ?? ''))) === 'EXPIRED' || ($eraseAt !== '' && strtotime($eraseAt) <= time())) {
|
||||
$this->purgeAbdmDocumentsByRefs(array_merge([$consentRequestId], $sessionIds));
|
||||
return $this->response->setStatusCode(403)->setBody('This consent request has expired. Health records are erased and cannot be viewed or printed as per ABDM guidelines.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($sessionIds !== []) {
|
||||
$builder->groupStart();
|
||||
$hasClause = false;
|
||||
|
|
@ -2198,7 +2264,7 @@ class Patient extends BaseController
|
|||
}
|
||||
$builder->limit(30);
|
||||
} else {
|
||||
$builder->limit(15);
|
||||
return $this->response->setStatusCode(400)->setBody('A valid consent request ID is required to print health records.');
|
||||
}
|
||||
|
||||
$rows = $builder->get()->getResultArray();
|
||||
|
|
@ -3033,12 +3099,59 @@ class Patient extends BaseController
|
|||
));
|
||||
|
||||
if ($overrideConsentId !== '' || $overrideConsentRequestId !== '') {
|
||||
$phase = 'GRANTED';
|
||||
$consentArtifactRef = $overrideConsentId;
|
||||
$consentRequestRef = $overrideConsentRequestId;
|
||||
$lookupTarget = $consentRequestRef !== '' ? $consentRequestRef : $consentArtifactRef;
|
||||
|
||||
// Verify whether this specific consent request is expired under ABDM dataEraseAt rules
|
||||
$checkWf = $this->db->table('abdm_hiu_workflows')
|
||||
->select(['id', 'workflow_state', 'request_json', 'response_json', 'expired_at'])
|
||||
->groupStart()
|
||||
->where('request_id', $lookupTarget)
|
||||
->orWhere('consent_id', $lookupTarget)
|
||||
->orWhere('abdm_consent_request_id', $lookupTarget)
|
||||
->orWhere('abdm_consent_artifact_id', $lookupTarget)
|
||||
->groupEnd()
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
$isTargetExpired = false;
|
||||
$targetEraseAt = '';
|
||||
if (is_array($checkWf)) {
|
||||
if (strtoupper(trim((string) ($checkWf['workflow_state'] ?? ''))) === 'EXPIRED') {
|
||||
$isTargetExpired = true;
|
||||
}
|
||||
$targetEraseAt = $this->extractDataEraseAtFromWorkflowRow($checkWf);
|
||||
if ($targetEraseAt !== '') {
|
||||
$targetTs = strtotime($targetEraseAt);
|
||||
if ($targetTs !== false && $targetTs <= time()) {
|
||||
$isTargetExpired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isTargetExpired) {
|
||||
$relatedPurgeIds = $this->resolveAllRelatedConsentIds($lookupTarget);
|
||||
$this->purgeAbdmDocumentsByRefs(array_merge([$lookupTarget], $relatedPurgeIds));
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'phase' => 'EXPIRED',
|
||||
'error' => 'This consent request expired on ' . ($targetEraseAt ? date('d/m/Y h:i:s A', strtotime($targetEraseAt)) : 'an earlier date') . '. Under ABDM guidelines, records cannot be fetched or accessed under an expired consent. Please create a new consent request.',
|
||||
]);
|
||||
}
|
||||
|
||||
$phase = 'GRANTED';
|
||||
} else {
|
||||
$lastSync = $this->getLatestAbdmSyncSnapshot((string) ($abhaContext['abha_address'] ?? ''));
|
||||
$phase = strtoupper(trim((string) ($lastSync['phase'] ?? '')));
|
||||
if ($phase === 'EXPIRED') {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'phase' => 'EXPIRED',
|
||||
'error' => 'The latest consent request has expired. Under ABDM guidelines, records cannot be fetched or accessed under an expired consent. Please create a new consent request.',
|
||||
]);
|
||||
}
|
||||
if (! in_array($phase, ['GRANTED', 'COMPLETED'], true)) {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
|
|
@ -5351,6 +5464,32 @@ class Patient extends BaseController
|
|||
$snapshot['operation'] = (string) ($best['operation'] ?? '');
|
||||
$snapshot['status'] = (string) ($best['status'] ?? '');
|
||||
|
||||
// Check if latest consent session has expired based on erase_at/dataEraseAt
|
||||
if ($latestSessionStartId > 0 && $this->db->tableExists('abdm_hiu_workflows')) {
|
||||
$anchorRow = $this->db->table('abdm_hiu_workflows')
|
||||
->select(['id', 'workflow_state', 'request_json', 'response_json', 'expired_at'])
|
||||
->where('id', $latestSessionStartId)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (is_array($anchorRow)) {
|
||||
$anchorEraseAt = $this->extractDataEraseAtFromWorkflowRow($anchorRow);
|
||||
$isAnchorExpired = false;
|
||||
if (strtoupper(trim((string) ($anchorRow['workflow_state'] ?? ''))) === 'EXPIRED') {
|
||||
$isAnchorExpired = true;
|
||||
} elseif ($anchorEraseAt !== '') {
|
||||
$anchorEraseTs = strtotime($anchorEraseAt);
|
||||
if ($anchorEraseTs !== false && $anchorEraseTs <= time()) {
|
||||
$isAnchorExpired = true;
|
||||
}
|
||||
}
|
||||
if ($isAnchorExpired) {
|
||||
$snapshot['phase'] = 'EXPIRED';
|
||||
$snapshot['message'] = 'Consent expired on ' . ($anchorEraseAt ? date('d/m/Y h:i:s A', strtotime($anchorEraseAt)) : 'an earlier date') . '. Start a new request to fetch updated records.';
|
||||
$snapshot['restart_required'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A consent request stuck in REQUESTED/PENDING for more than ABDM_PENDING_STALE_SECONDS
|
||||
// (1 hour) with no GRANTED/DENIED callback from the bridge is considered abandoned by
|
||||
// PHR/ABHA — the patient likely never saw/approved it, or the bridge silently dropped
|
||||
|
|
@ -6164,6 +6303,59 @@ class Patient extends BaseController
|
|||
$grantedOn = trim((string) ($bestDecoded['granted_at'] ?? $best['updated_at'] ?? ''));
|
||||
}
|
||||
|
||||
// ABDM M3 HIU compliance: evaluate consent expiration against erase_at (dataEraseAt)
|
||||
$isConsentExpired = false;
|
||||
if ($phase === 'EXPIRED') {
|
||||
$isConsentExpired = true;
|
||||
} elseif ($eraseAt !== '') {
|
||||
$eraseTimestamp = strtotime($eraseAt);
|
||||
if ($eraseTimestamp !== false && $eraseTimestamp <= time()) {
|
||||
$isConsentExpired = true;
|
||||
}
|
||||
}
|
||||
if (! $isConsentExpired && $expiredOn !== '') {
|
||||
$expTime = strtotime($expiredOn);
|
||||
if ($expTime !== false && $expTime <= time()) {
|
||||
$isConsentExpired = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isConsentExpired) {
|
||||
$phase = 'EXPIRED';
|
||||
if ($expiredOn === '') {
|
||||
$expiredOn = $eraseAt !== '' ? $eraseAt : (string) ($best['expired_at'] ?? date('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
// Persist terminal EXPIRED state to abdm_hiu_workflows for this session
|
||||
if ($this->db->tableExists('abdm_hiu_workflows')) {
|
||||
$sessionIdsToUpdate = [];
|
||||
foreach ($rows as $r) {
|
||||
$rState = strtoupper(trim((string) ($r['workflow_state'] ?? '')));
|
||||
if ($rState !== 'EXPIRED' && ! empty($r['id'])) {
|
||||
$sessionIdsToUpdate[] = (int) $r['id'];
|
||||
}
|
||||
}
|
||||
if ($sessionIdsToUpdate !== []) {
|
||||
$updateData = ['workflow_state' => 'EXPIRED'];
|
||||
$wfFields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
|
||||
if (in_array('expired_at', $wfFields, true)) {
|
||||
$updateData['expired_at'] = date('Y-m-d H:i:s', (isset($eraseTimestamp) && $eraseTimestamp) ? $eraseTimestamp : time());
|
||||
}
|
||||
$this->db->table('abdm_hiu_workflows')->whereIn('id', $sessionIdsToUpdate)->update($updateData);
|
||||
}
|
||||
}
|
||||
|
||||
// ABDM M3 HIU compliance: erase stored health records once dataEraseAt is reached
|
||||
$purgeRefs = array_filter([$consentRequestId, $consentId]);
|
||||
if ($purgeRefs !== []) {
|
||||
$sessionDocIds = $this->resolveAllRelatedConsentIds($consentRequestId !== '' ? $consentRequestId : $consentId);
|
||||
if ($sessionDocIds !== []) {
|
||||
$purgeRefs = array_merge($purgeRefs, $sessionDocIds);
|
||||
}
|
||||
$this->purgeAbdmDocumentsByRefs($purgeRefs);
|
||||
}
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$typesForItems = $requestedHiTypes !== [] ? $requestedHiTypes : $grantedHiTypes;
|
||||
foreach ($typesForItems as $hiType) {
|
||||
|
|
@ -6893,5 +7085,86 @@ class Patient extends BaseController
|
|||
log_message('warning', '[triggerBackgroundHiuPoll] ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts dataEraseAt timestamp from a workflow row's request_json or response_json.
|
||||
*/
|
||||
private function extractDataEraseAtFromWorkflowRow(array $row): string
|
||||
{
|
||||
$req = json_decode((string) ($row['request_json'] ?? ''), true);
|
||||
$resp = json_decode((string) ($row['response_json'] ?? ''), true);
|
||||
$containers = [
|
||||
is_array($req) ? ($req['consent'] ?? null) : null,
|
||||
is_array($req) ? ($req['consentDetail'] ?? null) : null,
|
||||
is_array($req) ? ($req['data']['consent'] ?? null) : null,
|
||||
is_array($req) ? $req : null,
|
||||
is_array($resp) ? ($resp['consent'] ?? null) : null,
|
||||
is_array($resp) ? ($resp['consentDetail'] ?? null) : null,
|
||||
is_array($resp) ? ($resp['data']['consent'] ?? null) : null,
|
||||
is_array($resp) ? $resp : null,
|
||||
];
|
||||
foreach ($containers as $c) {
|
||||
if (! is_array($c)) {
|
||||
continue;
|
||||
}
|
||||
$erase = trim((string) (
|
||||
$c['permission']['dataEraseAt']
|
||||
?? $c['permission']['data_erase_at']
|
||||
?? (is_array($c['expiry'] ?? null) ? ($c['expiry']['date'] ?? null) : ($c['expiry'] ?? null))
|
||||
?? $c['erase_at']
|
||||
?? $c['eraseAt']
|
||||
?? $c['dataEraseAt']
|
||||
?? $c['data_erase_at']
|
||||
?? ''
|
||||
));
|
||||
if ($erase !== '') {
|
||||
return $erase;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes cached health records from abdm_hiu_documents for the given consent request
|
||||
* or artifact references, complying with ABDM M3 dataEraseAt guidelines.
|
||||
*/
|
||||
private function purgeAbdmDocumentsByRefs(array $refs): int
|
||||
{
|
||||
$refs = array_values(array_unique(array_filter(array_map('trim', $refs))));
|
||||
if ($refs === [] || ! $this->db->tableExists('abdm_hiu_documents')) {
|
||||
return 0;
|
||||
}
|
||||
$docFields = $this->db->getFieldNames('abdm_hiu_documents') ?? [];
|
||||
$builder = $this->db->table('abdm_hiu_documents');
|
||||
$builder->groupStart();
|
||||
$hasClause = false;
|
||||
if (in_array('consent_request_id', $docFields, true)) {
|
||||
$builder->whereIn('consent_request_id', $refs);
|
||||
$hasClause = true;
|
||||
}
|
||||
if (in_array('consent_artifact_id', $docFields, true)) {
|
||||
if ($hasClause) {
|
||||
$builder->orWhereIn('consent_artifact_id', $refs);
|
||||
} else {
|
||||
$builder->whereIn('consent_artifact_id', $refs);
|
||||
$hasClause = true;
|
||||
}
|
||||
}
|
||||
if (in_array('consent_ref', $docFields, true)) {
|
||||
if ($hasClause) {
|
||||
$builder->orWhereIn('consent_ref', $refs);
|
||||
} else {
|
||||
$builder->whereIn('consent_ref', $refs);
|
||||
$hasClause = true;
|
||||
}
|
||||
}
|
||||
$builder->groupEnd();
|
||||
if (! $hasClause) {
|
||||
return 0;
|
||||
}
|
||||
$builder->delete();
|
||||
return (int) $this->db->affectedRows();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -702,6 +702,55 @@ class ConsentSessionListService
|
|||
));
|
||||
}
|
||||
|
||||
// ABDM M3 HIU compliance: evaluate consent expiration against erase_at (dataEraseAt)
|
||||
$isConsentExpired = false;
|
||||
if ($phase === 'EXPIRED') {
|
||||
$isConsentExpired = true;
|
||||
} elseif ($eraseAt !== '') {
|
||||
$eraseTimestamp = strtotime($eraseAt);
|
||||
if ($eraseTimestamp !== false && $eraseTimestamp <= time()) {
|
||||
$isConsentExpired = true;
|
||||
}
|
||||
}
|
||||
if (! $isConsentExpired && $expiredOn !== '') {
|
||||
$expTime = strtotime($expiredOn);
|
||||
if ($expTime !== false && $expTime <= time()) {
|
||||
$isConsentExpired = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isConsentExpired) {
|
||||
$phase = 'EXPIRED';
|
||||
if ($expiredOn === '') {
|
||||
$expiredOn = $eraseAt !== '' ? $eraseAt : (string) ($best['expired_at'] ?? date('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
// Persist terminal EXPIRED state to abdm_hiu_workflows for this session
|
||||
if ($this->db->tableExists('abdm_hiu_workflows')) {
|
||||
$sessionIdsToUpdate = [];
|
||||
foreach ($rows as $r) {
|
||||
$rState = strtoupper(trim((string) ($r['workflow_state'] ?? '')));
|
||||
if ($rState !== 'EXPIRED' && ! empty($r['id'])) {
|
||||
$sessionIdsToUpdate[] = (int) $r['id'];
|
||||
}
|
||||
}
|
||||
if ($sessionIdsToUpdate !== []) {
|
||||
$updateData = ['workflow_state' => 'EXPIRED'];
|
||||
$wfFields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
|
||||
if (in_array('expired_at', $wfFields, true)) {
|
||||
$updateData['expired_at'] = date('Y-m-d H:i:s', (isset($eraseTimestamp) && $eraseTimestamp) ? $eraseTimestamp : time());
|
||||
}
|
||||
$this->db->table('abdm_hiu_workflows')->whereIn('id', $sessionIdsToUpdate)->update($updateData);
|
||||
}
|
||||
}
|
||||
|
||||
// ABDM M3 HIU compliance: erase stored health records once dataEraseAt is reached
|
||||
$purgeRefs = array_filter([$consentRequestId, $consentId]);
|
||||
if ($purgeRefs !== []) {
|
||||
$this->purgeAbdmDocumentsByRefs($purgeRefs);
|
||||
}
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$typesForItems = $requestedHiTypes !== [] ? $requestedHiTypes : $grantedHiTypes;
|
||||
foreach ($typesForItems as $hiType) {
|
||||
|
|
@ -791,4 +840,64 @@ class ConsentSessionListService
|
|||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes cached health records from abdm_hiu_documents for the given consent request
|
||||
* or artifact references, complying with ABDM M3 dataEraseAt guidelines.
|
||||
*/
|
||||
private function purgeAbdmDocumentsByRefs(array $refs): int
|
||||
{
|
||||
$refs = array_values(array_unique(array_filter(array_map('trim', $refs))));
|
||||
if ($refs === [] || ! $this->db->tableExists('abdm_hiu_documents')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Also resolve any sibling artifact IDs under these umbrella consent_request_ids
|
||||
if ($this->db->tableExists('abdm_hiu_consent_artifacts')) {
|
||||
$siblingRows = $this->db->table('abdm_hiu_consent_artifacts')
|
||||
->select('artifact_id, consent_request_id')
|
||||
->whereIn('consent_request_id', $refs)
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($siblingRows as $sr) {
|
||||
$aId = trim((string) ($sr['artifact_id'] ?? ''));
|
||||
if ($aId !== '') {
|
||||
$refs[] = $aId;
|
||||
}
|
||||
}
|
||||
$refs = array_values(array_unique(array_filter($refs)));
|
||||
}
|
||||
|
||||
$docFields = $this->db->getFieldNames('abdm_hiu_documents') ?? [];
|
||||
$builder = $this->db->table('abdm_hiu_documents');
|
||||
$builder->groupStart();
|
||||
$hasClause = false;
|
||||
if (in_array('consent_request_id', $docFields, true)) {
|
||||
$builder->whereIn('consent_request_id', $refs);
|
||||
$hasClause = true;
|
||||
}
|
||||
if (in_array('consent_artifact_id', $docFields, true)) {
|
||||
if ($hasClause) {
|
||||
$builder->orWhereIn('consent_artifact_id', $refs);
|
||||
} else {
|
||||
$builder->whereIn('consent_artifact_id', $refs);
|
||||
$hasClause = true;
|
||||
}
|
||||
}
|
||||
if (in_array('consent_ref', $docFields, true)) {
|
||||
if ($hasClause) {
|
||||
$builder->orWhereIn('consent_ref', $refs);
|
||||
} else {
|
||||
$builder->whereIn('consent_ref', $refs);
|
||||
$hasClause = true;
|
||||
}
|
||||
}
|
||||
$builder->groupEnd();
|
||||
if (! $hasClause) {
|
||||
return 0;
|
||||
}
|
||||
$builder->delete();
|
||||
return (int) $this->db->affectedRows();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,22 +107,22 @@ class AbdmTaskBoardSyncService
|
|||
|
||||
$opdIds = array_values(array_unique(array_map(static fn ($r) => (int) ($r['opd_id'] ?? 0), $validRows)));
|
||||
|
||||
// 2. Fetch latest FHIR documents for these OPD records
|
||||
$docByOpd = [];
|
||||
// 2. Fetch latest FHIR documents for these OPD records (both OPConsultRecord and PrescriptionRecord)
|
||||
$docsByOpd = [];
|
||||
if (! empty($opdIds) && $this->db->tableExists('opd_fhir_documents')) {
|
||||
$docRows = $this->db->table('opd_fhir_documents')
|
||||
->select('id, opd_id, opd_session_id, bundle_type, bundle_json, generated_at')
|
||||
->select('id, opd_id, opd_session_id, bundle_type, bundle_json, generated_at, updated_at, created_at')
|
||||
->whereIn('opd_id', $opdIds)
|
||||
->whereIn('bundle_type', ['OPConsultRecord', 'MedicationRequestBundle', 'PrescriptionRecord'])
|
||||
->orderBy("CASE WHEN bundle_type = 'OPConsultRecord' THEN 1 ELSE 2 END", 'ASC', false)
|
||||
->orderBy('id', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($docRows as $doc) {
|
||||
$oid = (int) ($doc['opd_id'] ?? 0);
|
||||
if ($oid > 0 && ! isset($docByOpd[$oid])) {
|
||||
$docByOpd[$oid] = $doc;
|
||||
$bType = trim((string) ($doc['bundle_type'] ?? ''));
|
||||
if ($oid > 0 && $bType !== '' && ! isset($docsByOpd[$oid][$bType])) {
|
||||
$docsByOpd[$oid][$bType] = $doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -131,20 +131,22 @@ class AbdmTaskBoardSyncService
|
|||
$hrByOpd = [];
|
||||
if (! empty($opdIds) && $this->db->tableExists('health_records')) {
|
||||
$sessionIds = [];
|
||||
foreach ($docByOpd as $d) {
|
||||
$sId = (int) ($d['opd_session_id'] ?? 0);
|
||||
if ($sId > 0) {
|
||||
$sessionIds[] = (string) $sId;
|
||||
foreach ($docsByOpd as $oid => $bTypes) {
|
||||
foreach ($bTypes as $d) {
|
||||
$sId = (int) ($d['opd_session_id'] ?? 0);
|
||||
if ($sId > 0) {
|
||||
$sessionIds[] = (string) $sId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$searchEntityIds = array_merge(
|
||||
$searchEntityIds = array_values(array_unique(array_merge(
|
||||
array_map(static fn ($v) => (string) $v, $opdIds),
|
||||
$sessionIds
|
||||
);
|
||||
)));
|
||||
|
||||
$hrRows = $this->db->table('health_records')
|
||||
->select('id, entity_id, push_status, abdm_txn_id, bridge_record_id, care_context_reference, push_at, linked_at, updated_at')
|
||||
->select('id, entity_id, hi_type, push_status, abdm_txn_id, bridge_record_id, care_context_reference, push_at, linked_at, updated_at')
|
||||
->where('entity_type', 'opd')
|
||||
->whereIn('entity_id', $searchEntityIds)
|
||||
->orderBy('id', 'DESC')
|
||||
|
|
@ -153,11 +155,14 @@ class AbdmTaskBoardSyncService
|
|||
|
||||
foreach ($hrRows as $hr) {
|
||||
$eid = (int) ($hr['entity_id'] ?? 0);
|
||||
$ccRef = trim((string) ($hr['care_context_reference'] ?? ''));
|
||||
$ht = trim((string) ($hr['hi_type'] ?? ''));
|
||||
if ($eid <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (! isset($hrByOpd[$eid])) {
|
||||
$hrByOpd[$eid] = $hr;
|
||||
$typeKey = (str_starts_with($ccRef, 'PRESC-') || $ht === 'PrescriptionRecord') ? 'PrescriptionRecord' : 'OPConsultRecord';
|
||||
if (! isset($hrByOpd[$eid][$typeKey])) {
|
||||
$hrByOpd[$eid][$typeKey] = $hr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -175,54 +180,24 @@ class AbdmTaskBoardSyncService
|
|||
continue;
|
||||
}
|
||||
|
||||
$doc = $docByOpd[$opdId] ?? null;
|
||||
if ($doc === null) {
|
||||
$opdDocs = $docsByOpd[$opdId] ?? [];
|
||||
if (empty($opdDocs)) {
|
||||
// No FHIR bundle generated yet for this OPD
|
||||
continue;
|
||||
}
|
||||
|
||||
$sessionId = (int) ($doc['opd_session_id'] ?? 0);
|
||||
|
||||
// Reconcile: check if health_records row was stored under opd_id or session_id
|
||||
$hr = $hrByOpd[$opdId] ?? ($sessionId > 0 ? ($hrByOpd[$sessionId] ?? null) : null);
|
||||
|
||||
// If stored under session_id, reconcile entity_id to opdId
|
||||
if ($hr !== null && (string) ($hr['entity_id'] ?? '') === (string) $sessionId && (string) $sessionId !== (string) $opdId) {
|
||||
try {
|
||||
$this->db->table('health_records')
|
||||
->where('id', (int) $hr['id'])
|
||||
->update(['entity_id' => (string) $opdId]);
|
||||
$hr['entity_id'] = (string) $opdId;
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
$pushStatus = strtolower(trim((string) ($hr['push_status'] ?? '')));
|
||||
|
||||
// Already pushed/submitted/linked successfully?
|
||||
if (in_array($pushStatus, ['queued', 'linked', 'pushed'], true)) {
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$consultDate = (string) ($row['apointment_date'] ?? '');
|
||||
$visitDate = $consultDate !== '' ? date('Y-m-d', strtotime($consultDate)) : date('Y-m-d');
|
||||
$cleanDate = str_replace('-', '', $visitDate);
|
||||
$derivedCcRef = 'OPD-' . $patientId . '-S' . ($sessionId > 0 ? $sessionId : 0) . '-' . $cleanDate;
|
||||
|
||||
$careContextRef = trim((string) ($hr['care_context_reference'] ?? ''));
|
||||
if ($careContextRef === '') {
|
||||
$careContextRef = $derivedCcRef;
|
||||
}
|
||||
|
||||
// Check cooling period from the latest modification/generation timestamp
|
||||
$docTimestamps = array_filter([
|
||||
! empty($doc['updated_at']) ? (string) $doc['updated_at'] : null,
|
||||
! empty($doc['generated_at']) ? (string) $doc['generated_at'] : null,
|
||||
! empty($hr['updated_at']) ? (string) $hr['updated_at'] : null,
|
||||
! empty($doc['created_at']) ? (string) $doc['created_at'] : null,
|
||||
]);
|
||||
$lastModified = ! empty($docTimestamps) ? max($docTimestamps) : ($row['apointment_date'] ?? date('Y-m-d H:i:s'));
|
||||
$allTimestamps = [];
|
||||
foreach ($opdDocs as $d) {
|
||||
if (! empty($d['updated_at'])) $allTimestamps[] = (string) $d['updated_at'];
|
||||
if (! empty($d['generated_at'])) $allTimestamps[] = (string) $d['generated_at'];
|
||||
if (! empty($d['created_at'])) $allTimestamps[] = (string) $d['created_at'];
|
||||
}
|
||||
$lastModified = ! empty($allTimestamps) ? max($allTimestamps) : ($row['apointment_date'] ?? date('Y-m-d H:i:s'));
|
||||
|
||||
$cooling = self::calculateCooling('opd_prescription_publish', $lastModified);
|
||||
if ($cooling['is_cooling_active']) {
|
||||
|
|
@ -231,7 +206,7 @@ class AbdmTaskBoardSyncService
|
|||
'opd_id' => $opdId,
|
||||
'patient' => trim((string) ($row['P_name'] ?? '')),
|
||||
'abha' => trim((string) ($row['abha_id'] ?? '')),
|
||||
'care_context' => $careContextRef,
|
||||
'care_context' => 'OPD-' . $patientId . '-...',
|
||||
'last_modified' => $lastModified,
|
||||
'remaining_minutes' => $cooling['remaining_minutes'],
|
||||
'auto_link_at' => $cooling['auto_link_at'],
|
||||
|
|
@ -242,42 +217,92 @@ class AbdmTaskBoardSyncService
|
|||
|
||||
$summary['eligible']++;
|
||||
|
||||
if ($dryRun) {
|
||||
$summary['details'][] = [
|
||||
'opd_id' => $opdId,
|
||||
'patient' => trim((string) ($row['P_name'] ?? '')),
|
||||
'abha' => trim((string) ($row['abha_id'] ?? '')),
|
||||
'care_context' => $careContextRef,
|
||||
'status' => 'dry_run_eligible',
|
||||
// Define targets: 1) OPConsultRecord, 2) PrescriptionRecord (if prescribed)
|
||||
$syncTargets = [];
|
||||
|
||||
// Target A: OPConsultRecord
|
||||
if (! empty($opdDocs['OPConsultRecord'])) {
|
||||
$cDoc = $opdDocs['OPConsultRecord'];
|
||||
$sId = (int) ($cDoc['opd_session_id'] ?? 0);
|
||||
$cHr = $hrByOpd[$opdId]['OPConsultRecord'] ?? ($sId > 0 ? ($hrByOpd[$sId]['OPConsultRecord'] ?? null) : null);
|
||||
$derivedRef = 'OPD-' . $patientId . '-S' . ($sId > 0 ? $sId : 0) . '-' . $cleanDate;
|
||||
$ccRef = trim((string) ($cHr['care_context_reference'] ?? ''));
|
||||
if ($ccRef === '') {
|
||||
$ccRef = $derivedRef;
|
||||
}
|
||||
$syncTargets[] = [
|
||||
'doc' => $cDoc,
|
||||
'hr' => $cHr,
|
||||
'ref' => $ccRef,
|
||||
];
|
||||
$processedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perform care context linking and push to ABDM bridge
|
||||
$pushResult = $this->linkAndPushOpdRecord($row, $doc, $careContextRef, $visitDate, $hr);
|
||||
// Target B: PrescriptionRecord (separate context for PHR App Prescriptions tab)
|
||||
$pDoc = $opdDocs['PrescriptionRecord'] ?? ($opdDocs['MedicationRequestBundle'] ?? null);
|
||||
if (! empty($pDoc)) {
|
||||
$sId = (int) ($pDoc['opd_session_id'] ?? 0);
|
||||
$pHr = $hrByOpd[$opdId]['PrescriptionRecord'] ?? ($sId > 0 ? ($hrByOpd[$sId]['PrescriptionRecord'] ?? null) : null);
|
||||
$derivedRef = 'PRESC-' . $patientId . '-S' . ($sId > 0 ? $sId : 0) . '-' . $cleanDate;
|
||||
$ccRef = trim((string) ($pHr['care_context_reference'] ?? ''));
|
||||
if ($ccRef === '') {
|
||||
$ccRef = $derivedRef;
|
||||
}
|
||||
$syncTargets[] = [
|
||||
'doc' => $pDoc,
|
||||
'hr' => $pHr,
|
||||
'ref' => $ccRef,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($syncTargets as $target) {
|
||||
$docToPush = $target['doc'];
|
||||
$hrToUse = $target['hr'];
|
||||
$ccRefToUse = $target['ref'];
|
||||
$pushStatus = strtolower(trim((string) ($hrToUse['push_status'] ?? '')));
|
||||
|
||||
// Already pushed/submitted/linked successfully?
|
||||
if (in_array($pushStatus, ['queued', 'linked', 'pushed'], true)) {
|
||||
$summary['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$summary['details'][] = [
|
||||
'opd_id' => $opdId,
|
||||
'patient' => trim((string) ($row['P_name'] ?? '')),
|
||||
'abha' => trim((string) ($row['abha_id'] ?? '')),
|
||||
'care_context' => $ccRefToUse,
|
||||
'status' => 'dry_run_eligible',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perform care context linking and push to ABDM bridge
|
||||
$pushResult = $this->linkAndPushOpdRecord($row, $docToPush, $ccRefToUse, $visitDate, $hrToUse);
|
||||
|
||||
if ($pushResult['ok'] === 1) {
|
||||
$summary['linked']++;
|
||||
$summary['details'][] = [
|
||||
'opd_id' => $opdId,
|
||||
'patient' => trim((string) ($row['P_name'] ?? '')),
|
||||
'care_context' => $ccRefToUse,
|
||||
'queue_id' => $pushResult['queue_id'] ?? '',
|
||||
'bridge_record_id' => $pushResult['bridge_record_id'] ?? null,
|
||||
'status' => 'linked',
|
||||
];
|
||||
} else {
|
||||
$summary['failed']++;
|
||||
$summary['details'][] = [
|
||||
'opd_id' => $opdId,
|
||||
'patient' => trim((string) ($row['P_name'] ?? '')),
|
||||
'care_context' => $ccRefToUse,
|
||||
'error' => $pushResult['error'] ?? 'Unknown push error',
|
||||
'status' => 'failed',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$processedCount++;
|
||||
|
||||
if ($pushResult['ok'] === 1) {
|
||||
$summary['linked']++;
|
||||
$summary['details'][] = [
|
||||
'opd_id' => $opdId,
|
||||
'patient' => trim((string) ($row['P_name'] ?? '')),
|
||||
'care_context' => $careContextRef,
|
||||
'queue_id' => $pushResult['queue_id'] ?? '',
|
||||
'bridge_record_id' => $pushResult['bridge_record_id'] ?? null,
|
||||
'status' => 'linked',
|
||||
];
|
||||
} else {
|
||||
$summary['failed']++;
|
||||
$summary['details'][] = [
|
||||
'opd_id' => $opdId,
|
||||
'patient' => trim((string) ($row['P_name'] ?? '')),
|
||||
'care_context' => $careContextRef,
|
||||
'error' => $pushResult['error'] ?? 'Unknown push error',
|
||||
'status' => 'failed',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $summary;
|
||||
|
|
|
|||
|
|
@ -184,10 +184,19 @@
|
|||
var html = '';
|
||||
items.forEach(function(consent, idx) {
|
||||
var status = (consent.status || '').toString().toUpperCase();
|
||||
var eraseAt = (consent.erase_at || '').toString().trim();
|
||||
var isExpired = (status === 'EXPIRED');
|
||||
if (!isExpired && eraseAt !== '') {
|
||||
var ed = new Date(eraseAt.indexOf('T') === -1 ? eraseAt.replace(' ', 'T') : eraseAt);
|
||||
if (!isNaN(ed.getTime()) && ed.getTime() <= Date.now()) {
|
||||
isExpired = true;
|
||||
status = 'EXPIRED';
|
||||
}
|
||||
}
|
||||
var hiTypes = (consent.requested_hi_types && consent.requested_hi_types.length) ? consent.requested_hi_types : (consent.granted_hi_types || []);
|
||||
var hiTypesText = hiTypes.length ? hiTypes.join(', ') : '-';
|
||||
var patientId = Number(consent.patient_id || 0);
|
||||
var canFetch = (status === 'GRANTED' || status === 'COMPLETED') && patientId > 0;
|
||||
var canFetch = (status === 'GRANTED' || status === 'COMPLETED') && patientId > 0 && !isExpired;
|
||||
var canOpen = patientId > 0;
|
||||
|
||||
html += '<tr>'
|
||||
|
|
|
|||
|
|
@ -377,9 +377,6 @@ if ($patientPhotoPath === '') {
|
|||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="fw-semibold">Consent Request History</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="<?= base_url('billing/patient/abdm_records_pdf/' . (int) ($patient->id ?? 0)) ?>" target="_blank" class="btn btn-outline-primary btn-sm py-0 px-2 me-3" title="Print all fetched health records for this patient as PDF">
|
||||
<i class="fa fa-print me-1"></i> Print All Records
|
||||
</a>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 me-3" id="btnCheckLiveAbdmStatus">
|
||||
<span class="spinner-border spinner-border-sm d-none" id="abdmLiveStatusSpinner"></span>
|
||||
Check Live Status
|
||||
|
|
@ -397,7 +394,7 @@ if ($patientPhotoPath === '') {
|
|||
<th>Date From</th>
|
||||
<th>Date To</th>
|
||||
<th>View</th>
|
||||
<th class="text-end">Show Data</th>
|
||||
<th class="text-end">Show Data / Print</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -1207,8 +1204,16 @@ $(function() {
|
|||
return '<div class="text-danger small">No consent details available.</div>';
|
||||
}
|
||||
|
||||
var steps = ['REQUESTED', 'GRANTED'];
|
||||
var statusUpper = (consent.status || '').toString().toUpperCase();
|
||||
var eraseAt = (consent.erase_at || '').toString().trim();
|
||||
if (statusUpper !== 'EXPIRED' && eraseAt !== '') {
|
||||
var ed = new Date(eraseAt.indexOf('T') === -1 ? eraseAt.replace(' ', 'T') : eraseAt);
|
||||
if (!isNaN(ed.getTime()) && ed.getTime() <= Date.now()) {
|
||||
statusUpper = 'EXPIRED';
|
||||
}
|
||||
}
|
||||
|
||||
var steps = ['REQUESTED', 'GRANTED'];
|
||||
if (statusUpper === 'REVOKED') { steps.push('REVOKED'); }
|
||||
else if (statusUpper === 'EXPIRED') { steps.push('EXPIRED'); }
|
||||
|
||||
|
|
@ -1224,7 +1229,7 @@ $(function() {
|
|||
+ '<div class="col-md-6">Consent ID: <strong>' + escHtml(consent.consent_id || consent.abdm_consent_artifact_id || consent.consent_request_id || consent.request_id || '-') + '</strong></div>'
|
||||
+ '<div class="col-md-6">ABHA: <strong>' + escHtml(consent.abha_address || '-') + '</strong></div>'
|
||||
+ '<div class="col-md-6">Purpose: <strong>' + escHtml(consent.purpose || '-') + '</strong></div>'
|
||||
+ '<div class="col-md-6">Status: <strong>' + escHtml(consent.status || '-') + '</strong></div>'
|
||||
+ '<div class="col-md-6">Status: <strong>' + escHtml(statusUpper || '-') + '</strong></div>'
|
||||
+ '<div class="col-md-6">Valid From: <strong>' + fmtConsentTs(consent.valid_from) + '</strong></div>'
|
||||
+ '<div class="col-md-6">Valid To: <strong>' + fmtConsentTs(consent.valid_to) + '</strong></div>'
|
||||
+ '<div class="col-md-6">Requested On: <strong>' + fmtConsentTs(consent.requested_on) + '</strong></div>'
|
||||
|
|
@ -1233,14 +1238,15 @@ $(function() {
|
|||
|
||||
var rowsHtml = '';
|
||||
(consent.items || []).forEach(function(item) {
|
||||
var ts = item.status === 'GRANTED' ? item.timestamp
|
||||
: item.status === 'REVOKED' ? item.timestamp
|
||||
: item.status === 'EXPIRED' ? item.timestamp
|
||||
var itemStatus = statusUpper === 'EXPIRED' ? 'EXPIRED' : (item.status || '');
|
||||
var ts = itemStatus === 'GRANTED' ? item.timestamp
|
||||
: itemStatus === 'REVOKED' ? item.timestamp
|
||||
: itemStatus === 'EXPIRED' ? (consent.expired_on || consent.erase_at || item.timestamp)
|
||||
: item.timestamp;
|
||||
rowsHtml += '<tr>'
|
||||
+ '<td>' + escHtml(item.document_name || '') + '</td>'
|
||||
+ '<td>' + escHtml(item.permission || 'VIEW') + '</td>'
|
||||
+ '<td><span class="badge ' + consentItemBadgeClass(item.status) + '">' + escHtml(item.status || '') + '</span></td>'
|
||||
+ '<td><span class="badge ' + consentItemBadgeClass(itemStatus) + '">' + escHtml(itemStatus || '') + '</span></td>'
|
||||
+ '<td>' + fmtConsentTs(ts) + '</td>'
|
||||
+ '</tr>';
|
||||
});
|
||||
|
|
@ -1288,9 +1294,23 @@ $(function() {
|
|||
var html = '';
|
||||
requests.forEach(function(consent, idx) {
|
||||
var status = (consent.status || '').toString().toUpperCase();
|
||||
var canFetch = (status === 'GRANTED' || status === 'COMPLETED');
|
||||
var eraseAt = (consent.erase_at || '').toString().trim();
|
||||
var isExpired = (status === 'EXPIRED');
|
||||
if (!isExpired && eraseAt !== '') {
|
||||
var ed = new Date(eraseAt.indexOf('T') === -1 ? eraseAt.replace(' ', 'T') : eraseAt);
|
||||
if (!isNaN(ed.getTime()) && ed.getTime() <= Date.now()) {
|
||||
isExpired = true;
|
||||
status = 'EXPIRED';
|
||||
}
|
||||
}
|
||||
var canFetch = (status === 'GRANTED' || status === 'COMPLETED') && !isExpired;
|
||||
var rowId = consent.id || (idx + 1);
|
||||
|
||||
var reqId = (consent.consent_request_id || consent.consent_id || consent.request_id || '').toString().trim();
|
||||
var printUrl = abdmPrintPdfUrl + (reqId ? ('?consent_request_id=' + encodeURIComponent(reqId)) : '');
|
||||
var actionHtml = '<button type="button" class="btn btn-link btn-sm p-0 abdm-fetch-request-btn me-2" data-idx="' + idx + '">Show Data</button>'
|
||||
+ '<a href="' + escHtml(printUrl) + '" target="_blank" class="btn btn-link btn-sm p-0 text-decoration-none" title="Print health records for this consent request"><i class="fa fa-print me-1"></i>Print</a>';
|
||||
|
||||
html += '<tr>'
|
||||
+ '<td class="small">' + escHtml(rowId) + '</td>'
|
||||
+ '<td><span class="badge ' + consentStatusBadgeClass(status) + '">' + escHtml(status || 'UNKNOWN') + '</span></td>'
|
||||
|
|
@ -1299,7 +1319,7 @@ $(function() {
|
|||
+ '<td class="small">' + fmtConsentTs(consent.valid_to) + '</td>'
|
||||
+ '<td><button type="button" class="btn btn-link btn-sm p-0 abdm-view-request-btn" data-idx="' + idx + '">View</button></td>'
|
||||
+ '<td class="text-end">'
|
||||
+ (canFetch ? ('<button type="button" class="btn btn-link btn-sm p-0 abdm-fetch-request-btn" data-idx="' + idx + '">Show Data</button>') : '<span class="text-muted small">-</span>')
|
||||
+ (canFetch ? actionHtml : '<span class="text-muted small">-</span>')
|
||||
+ '</td>'
|
||||
+ '</tr>';
|
||||
});
|
||||
|
|
@ -1344,6 +1364,21 @@ $(function() {
|
|||
if (!consent) {
|
||||
return;
|
||||
}
|
||||
|
||||
var status = (consent.status || '').toString().toUpperCase();
|
||||
var eraseAt = (consent.erase_at || '').toString().trim();
|
||||
var isExpired = (status === 'EXPIRED');
|
||||
if (!isExpired && eraseAt !== '') {
|
||||
var ed = new Date(eraseAt.indexOf('T') === -1 ? eraseAt.replace(' ', 'T') : eraseAt);
|
||||
if (!isNaN(ed.getTime()) && ed.getTime() <= Date.now()) {
|
||||
isExpired = true;
|
||||
}
|
||||
}
|
||||
if (isExpired) {
|
||||
setAbdmStatus('This consent request has expired. Under ABDM guidelines, records cannot be fetched or accessed under an expired consent.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchOnlyRunningIdx = idx;
|
||||
currentAbdmFetchModalIdx = idx;
|
||||
applyNewRequestButtonState();
|
||||
|
|
@ -1381,8 +1416,24 @@ $(function() {
|
|||
}
|
||||
|
||||
function loadAbdmFetchResultModalData(consent) {
|
||||
var reqId = (consent && consent.consent_request_id) ? consent.consent_request_id : '';
|
||||
var reqId = (consent && (consent.consent_request_id || consent.consent_id || consent.request_id)) ? (consent.consent_request_id || consent.consent_id || consent.request_id) : '';
|
||||
currentAbdmFetchConsentRequestId = reqId;
|
||||
|
||||
var status = (consent && consent.status ? consent.status : '').toString().toUpperCase();
|
||||
var eraseAt = (consent && consent.erase_at ? consent.erase_at : '').toString().trim();
|
||||
var isExpired = (status === 'EXPIRED');
|
||||
if (!isExpired && eraseAt !== '') {
|
||||
var ed = new Date(eraseAt.indexOf('T') === -1 ? eraseAt.replace(' ', 'T') : eraseAt);
|
||||
if (!isNaN(ed.getTime()) && ed.getTime() <= Date.now()) {
|
||||
isExpired = true;
|
||||
}
|
||||
}
|
||||
if (isExpired) {
|
||||
$('#abdmPrintFetchedRecordsBtn').hide();
|
||||
$('#abdmFetchResultModalBody').html('<div class="text-muted small text-center p-3">This consent request expired on <strong>' + escHtml(fmtConsentTs(eraseAt)) + '</strong>.<br>As per ABDM guidelines, health records are erased and no longer accessible.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
var url = abdmDocumentsUrl + '?limit=200&include_summary=1';
|
||||
if (reqId !== '') {
|
||||
url += '&consent_request_id=' + encodeURIComponent(reqId);
|
||||
|
|
@ -1405,10 +1456,21 @@ $(function() {
|
|||
function renderAbdmFetchResultModal(docs) {
|
||||
if (!docs || !docs.length) {
|
||||
$('#abdmPrintFetchedRecordsBtn').hide();
|
||||
var fetchBtnHtml = currentAbdmFetchModalIdx >= 0
|
||||
var activeConsent = currentAbdmFetchModalIdx >= 0 ? currentAbdmRequests[currentAbdmFetchModalIdx] : null;
|
||||
var isModalConsentExpired = false;
|
||||
if (activeConsent) {
|
||||
var mStat = (activeConsent.status || '').toString().toUpperCase();
|
||||
var mErase = (activeConsent.erase_at || '').toString().trim();
|
||||
if (mStat === 'EXPIRED') { isModalConsentExpired = true; }
|
||||
else if (mErase !== '') {
|
||||
var mEd = new Date(mErase.indexOf('T') === -1 ? mErase.replace(' ', 'T') : mErase);
|
||||
if (!isNaN(mEd.getTime()) && mEd.getTime() <= Date.now()) { isModalConsentExpired = true; }
|
||||
}
|
||||
}
|
||||
var fetchBtnHtml = (currentAbdmFetchModalIdx >= 0 && !isModalConsentExpired)
|
||||
? '<div class="text-center mt-2"><button type="button" class="btn btn-sm btn-primary abdm-fetch-again-btn" data-idx="' + currentAbdmFetchModalIdx + '">Fetch Data</button></div>'
|
||||
: '';
|
||||
$('#abdmFetchResultModalBody').html('<div class="text-muted small text-center">No fetched records found for this consent request yet.</div>' + fetchBtnHtml);
|
||||
$('#abdmFetchResultModalBody').html('<div class="text-muted small text-center">No fetched records found for this consent request.</div>' + fetchBtnHtml);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user