feat(abdm): implement care-context linking cooling window and timer reset on record modification
- Add cooling period configuration (60 min for OPD/Lab/Radiology/others, 24 hr for IPD Discharge) in AbdmConnector - Add calculateCooling() logic in AbdmTaskBoardSyncService and skip automated linking during cooling window - Touch and refresh work task timestamps on prescription save and IPD discharge summary save/print - Update opd_fhir_documents on prescription edit rather than inserting duplicate bundles - Enrich ABDM Task Board table and OPD consult list with active cooling badges, remaining minutes, and auto-link schedule - Preserve manual 'Link Context' action on task board to allow immediate override anytime - Update CLI commands (abdm:taskboard-sync, abdm:push-sync) with cooling counters and status logging
This commit is contained in:
parent
ed57228b98
commit
61206784f3
|
|
@ -52,7 +52,7 @@ class AbdmPushSync extends BaseCommand
|
|||
$tbService = new AbdmTaskBoardSyncService();
|
||||
$tbSummary = $tbService->syncAll($limit);
|
||||
|
||||
CLI::write('OPD Consults -> Eligible: ' . ($tbSummary['opd']['eligible'] ?? 0) . ' | Linked: ' . ($tbSummary['opd']['linked'] ?? 0) . ' | Failed: ' . ($tbSummary['opd']['failed'] ?? 0), 'green');
|
||||
CLI::write('Work Tasks -> Eligible: ' . ($tbSummary['tasks']['eligible'] ?? 0) . ' | Linked: ' . ($tbSummary['tasks']['linked'] ?? 0) . ' | Failed: ' . ($tbSummary['tasks']['failed'] ?? 0), 'green');
|
||||
CLI::write('OPD Consults -> Eligible: ' . ($tbSummary['opd']['eligible'] ?? 0) . ' | Linked: ' . ($tbSummary['opd']['linked'] ?? 0) . ' | Cooling: ' . ($tbSummary['opd']['cooling'] ?? 0) . ' | Failed: ' . ($tbSummary['opd']['failed'] ?? 0), 'green');
|
||||
CLI::write('Work Tasks -> Eligible: ' . ($tbSummary['tasks']['eligible'] ?? 0) . ' | Linked: ' . ($tbSummary['tasks']['linked'] ?? 0) . ' | Cooling: ' . ($tbSummary['tasks']['cooling'] ?? 0) . ' | Failed: ' . ($tbSummary['tasks']['failed'] ?? 0), 'green');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,19 +48,24 @@ class AbdmTaskBoardSync extends BaseCommand
|
|||
|
||||
CLI::write('Eligible : ' . $opdSummary['eligible']);
|
||||
CLI::write('Linked : ' . $opdSummary['linked'], 'green');
|
||||
CLI::write('Cooling : ' . ($opdSummary['cooling'] ?? 0), 'cyan');
|
||||
CLI::write('Failed : ' . $opdSummary['failed'], $opdSummary['failed'] > 0 ? 'red' : 'green');
|
||||
CLI::write('Skipped : ' . $opdSummary['skipped']);
|
||||
|
||||
foreach ($opdSummary['details'] as $item) {
|
||||
$statusColor = match ($item['status'] ?? '') {
|
||||
'linked' => 'green',
|
||||
'failed' => 'red',
|
||||
default => 'yellow',
|
||||
'linked' => 'green',
|
||||
'failed' => 'red',
|
||||
'cooling' => 'cyan',
|
||||
default => 'yellow',
|
||||
};
|
||||
$coolingNote = (isset($item['status']) && $item['status'] === 'cooling')
|
||||
? ' [Cooling: ' . ($item['remaining_minutes'] ?? 0) . 'm left, auto-link at ' . ($item['auto_link_at'] ?? '') . ']'
|
||||
: '';
|
||||
$extra = isset($item['queue_id']) && $item['queue_id'] !== ''
|
||||
? ' [Queue: ' . $item['queue_id'] . (isset($item['bridge_record_id']) ? ' Bridge: #' . $item['bridge_record_id'] : '') . ']'
|
||||
: (isset($item['error']) ? ' [Error: ' . $item['error'] . ']' : '');
|
||||
CLI::write(' OPD #' . ($item['opd_id'] ?? 0) . ' (' . ($item['patient'] ?? '') . ') -> ' . ($item['status'] ?? '') . $extra, $statusColor);
|
||||
CLI::write(' OPD #' . ($item['opd_id'] ?? 0) . ' (' . ($item['patient'] ?? '') . ') -> ' . ($item['status'] ?? '') . $coolingNote . $extra, $statusColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,19 +77,24 @@ class AbdmTaskBoardSync extends BaseCommand
|
|||
|
||||
CLI::write('Eligible : ' . $taskSummary['eligible']);
|
||||
CLI::write('Linked : ' . $taskSummary['linked'], 'green');
|
||||
CLI::write('Cooling : ' . ($taskSummary['cooling'] ?? 0), 'cyan');
|
||||
CLI::write('Failed : ' . $taskSummary['failed'], $taskSummary['failed'] > 0 ? 'red' : 'green');
|
||||
CLI::write('Skipped : ' . $taskSummary['skipped']);
|
||||
|
||||
foreach ($taskSummary['details'] as $item) {
|
||||
$statusColor = match ($item['status'] ?? '') {
|
||||
'linked' => 'green',
|
||||
'failed' => 'red',
|
||||
default => 'yellow',
|
||||
'linked' => 'green',
|
||||
'failed' => 'red',
|
||||
'cooling' => 'cyan',
|
||||
default => 'yellow',
|
||||
};
|
||||
$coolingNote = (isset($item['status']) && $item['status'] === 'cooling')
|
||||
? ' [Cooling: ' . ($item['remaining_minutes'] ?? 0) . 'm left, auto-link at ' . ($item['auto_link_at'] ?? '') . ']'
|
||||
: '';
|
||||
$extra = isset($item['queue_id']) && $item['queue_id'] !== ''
|
||||
? ' [Queue: ' . $item['queue_id'] . ']'
|
||||
: (isset($item['error']) ? ' [Error: ' . $item['error'] . ']' : '');
|
||||
CLI::write(' Task #' . ($item['task_id'] ?? 0) . ' (' . ($item['task_type'] ?? '') . ') -> ' . ($item['status'] ?? '') . $extra, $statusColor);
|
||||
CLI::write(' Task #' . ($item['task_id'] ?? 0) . ' (' . ($item['task_type'] ?? '') . ') -> ' . ($item['status'] ?? '') . $coolingNote . $extra, $statusColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -144,4 +144,19 @@ class AbdmConnector extends BaseConfig
|
|||
// REQUIRED on production; falls back to a derived key in development.
|
||||
// ------------------------------------------------------------------
|
||||
// ABDM_FHIR_ENCRYPTION_KEY =
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Automated Care-Context Linking Grace / Cooling Period
|
||||
// Prevents premature data push while staff/doctors may still modify
|
||||
// prescriptions, lab reports, radiology reports, or discharge summaries.
|
||||
// Manual push from ABDM Task Board always bypasses this cooling period.
|
||||
// ------------------------------------------------------------------
|
||||
public bool $autoLinkEnabled = true;
|
||||
|
||||
/** Default cooling period for OPD, Lab, Radiology, and all other HI types (in minutes). */
|
||||
public int $autoLinkDelayMinutes = 60;
|
||||
|
||||
/** Cooling period for IPD Discharge Summary (in hours). */
|
||||
public int $autoLinkDelayDischargeHours = 24;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace App\Controllers;
|
|||
|
||||
use App\Libraries\AbdmWorkTaskService;
|
||||
use App\Libraries\BridgeSyncService;
|
||||
use App\Libraries\Abdm\Sync\AbdmTaskBoardSyncService;
|
||||
|
||||
class AbdmTaskBoard extends BaseController
|
||||
{
|
||||
|
|
@ -21,7 +22,7 @@ class AbdmTaskBoard extends BaseController
|
|||
$this->backfillImmunizationTasks();
|
||||
$this->backfillHealthDocumentTasks();
|
||||
$this->backfillWellnessTasks();
|
||||
$tasks = $this->enrichImmunizationPushState($this->taskService->getOpenTasks(300));
|
||||
$tasks = $this->enrichTasksWithCoolingState($this->enrichImmunizationPushState($this->taskService->getOpenTasks(300)));
|
||||
|
||||
$dateFrom = trim((string) ($this->request->getGet('date_from') ?? date('Y-m-d')));
|
||||
$dateTo = trim((string) ($this->request->getGet('date_to') ?? date('Y-m-d')));
|
||||
|
|
@ -56,7 +57,7 @@ class AbdmTaskBoard extends BaseController
|
|||
$this->backfillWellnessTasks();
|
||||
return $this->response->setJSON([
|
||||
'ok' => 1,
|
||||
'tasks' => $this->enrichImmunizationPushState($this->taskService->getOpenTasks(300)),
|
||||
'tasks' => $this->enrichTasksWithCoolingState($this->enrichImmunizationPushState($this->taskService->getOpenTasks(300))),
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
|
|
@ -137,6 +138,49 @@ class AbdmTaskBoard extends BaseController
|
|||
return $tasks;
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $tasks */
|
||||
private function enrichTasksWithCoolingState(array $tasks): array
|
||||
{
|
||||
if (empty($tasks)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($tasks as &$task) {
|
||||
$taskType = (string) ($task['task_type'] ?? '');
|
||||
$lastModified = ! empty($task['updated_at'])
|
||||
? (string) $task['updated_at']
|
||||
: (! empty($task['created_at']) ? (string) $task['created_at'] : null);
|
||||
|
||||
$cooling = AbdmTaskBoardSyncService::calculateCooling($taskType, $lastModified);
|
||||
|
||||
$task['cooling_active'] = ! empty($cooling['is_cooling_active']);
|
||||
$task['cooling_remaining_minutes'] = (int) ($cooling['remaining_minutes'] ?? 0);
|
||||
$task['cooling_remaining_seconds'] = (int) ($cooling['remaining_seconds'] ?? 0);
|
||||
$task['auto_link_at'] = $cooling['auto_link_at'] ?? null;
|
||||
$task['ready_for_autolink'] = false;
|
||||
|
||||
if ($task['cooling_active']) {
|
||||
$remMin = $task['cooling_remaining_minutes'];
|
||||
if ($remMin >= 60) {
|
||||
$hours = intdiv($remMin, 60);
|
||||
$mins = $remMin % 60;
|
||||
$timeStr = $mins > 0 ? "{$hours}h {$mins}m" : "{$hours}h";
|
||||
} else {
|
||||
$timeStr = "{$remMin}m";
|
||||
}
|
||||
$task['cooling_label'] = "Cooling ({$timeStr} left)";
|
||||
$task['cooling_tooltip'] = "Cooling active until {$task['auto_link_at']} to allow clinician edits. Modifications reset the timer. Click Link to bypass.";
|
||||
} elseif (! empty($task['auto_link_at']) && $task['auto_link_at'] !== 'disabled') {
|
||||
$task['ready_for_autolink'] = true;
|
||||
$task['cooling_label'] = "Ready for Link";
|
||||
$task['cooling_tooltip'] = "Cooling period elapsed at {$task['auto_link_at']}. Queued for automated linking.";
|
||||
}
|
||||
}
|
||||
unset($task);
|
||||
|
||||
return $tasks;
|
||||
}
|
||||
|
||||
public function performAction()
|
||||
{
|
||||
if (! $this->request->isAJAX()) {
|
||||
|
|
@ -920,7 +964,7 @@ class AbdmTaskBoard extends BaseController
|
|||
$latestDocByOpd = [];
|
||||
if (! empty($opdIds) && $this->db->tableExists('opd_fhir_documents')) {
|
||||
$docRows = $this->db->table('opd_fhir_documents')
|
||||
->select('id, opd_id, opd_session_id, generated_at, created_at')
|
||||
->select('id, opd_id, opd_session_id, generated_at, created_at, updated_at')
|
||||
->whereIn('opd_id', $opdIds)
|
||||
->whereIn('bundle_type', ['OPConsultRecord', 'MedicationRequestBundle', 'PrescriptionRecord'])
|
||||
->orderBy('id', 'DESC')
|
||||
|
|
@ -1019,12 +1063,25 @@ class AbdmTaskBoard extends BaseController
|
|||
$statusTone = 'secondary';
|
||||
$statusNote = 'FHIR not registered in health_records yet.';
|
||||
|
||||
$docModified = null;
|
||||
if ($doc !== null) {
|
||||
$statusLabel = 'FHIR Generated';
|
||||
$statusTone = 'info';
|
||||
$statusNote = 'FHIR bundle generated; awaiting registration.';
|
||||
|
||||
$docTimestamps = array_filter([
|
||||
! empty($doc['updated_at']) ? (string) $doc['updated_at'] : null,
|
||||
! empty($doc['generated_at']) ? (string) $doc['generated_at'] : null,
|
||||
! empty($doc['created_at']) ? (string) $doc['created_at'] : null,
|
||||
]);
|
||||
$docModified = ! empty($docTimestamps) ? max($docTimestamps) : null;
|
||||
}
|
||||
|
||||
$cooling = AbdmTaskBoardSyncService::calculateCooling('opd_prescription_publish', $docModified);
|
||||
$isCoolingActive = ! empty($cooling['is_cooling_active']);
|
||||
$coolingRemainingMinutes = (int) ($cooling['remaining_minutes'] ?? 0);
|
||||
$autoLinkAt = $cooling['auto_link_at'] ?? null;
|
||||
|
||||
if ($pushStatus !== '') {
|
||||
if ($pushStatus === 'local_discovery_ready') {
|
||||
$statusLabel = 'Discovery Ready';
|
||||
|
|
@ -1061,6 +1118,28 @@ class AbdmTaskBoard extends BaseController
|
|||
$statusNote = 'Link callback reported failure.';
|
||||
}
|
||||
|
||||
// If not yet linked or submitted to gateway, reflect cooling window state
|
||||
$isAlreadyLinkedOrQueued = in_array($linkStatus, ['linked', 'registered'], true) || in_array($pushStatus, ['linked', 'pushed', 'queued'], true);
|
||||
if ($doc !== null && ! $isAlreadyLinkedOrQueued) {
|
||||
if ($isCoolingActive) {
|
||||
$remMin = $coolingRemainingMinutes;
|
||||
if ($remMin >= 60) {
|
||||
$hours = intdiv($remMin, 60);
|
||||
$mins = $remMin % 60;
|
||||
$timeStr = $mins > 0 ? "{$hours}h {$mins}m" : "{$hours}h";
|
||||
} else {
|
||||
$timeStr = "{$remMin}m";
|
||||
}
|
||||
$statusLabel = 'Cooling (' . $timeStr . ' left)';
|
||||
$statusTone = 'warning';
|
||||
$statusNote = 'Cooling active until ' . $autoLinkAt . ' to allow clinician prescription edits before auto-link.';
|
||||
} elseif (! empty($autoLinkAt) && $autoLinkAt !== 'disabled') {
|
||||
$statusLabel = 'Ready for Auto-Link';
|
||||
$statusTone = 'primary';
|
||||
$statusNote = 'Cooling period elapsed. Ready for automated link push.';
|
||||
}
|
||||
}
|
||||
|
||||
$queueId = trim((string) ($hr['abdm_txn_id'] ?? ''));
|
||||
$bridgeRecordId = (int) ($hr['bridge_record_id'] ?? 0);
|
||||
|
||||
|
|
@ -1075,6 +1154,9 @@ class AbdmTaskBoard extends BaseController
|
|||
'queue_id' => $queueId,
|
||||
'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null,
|
||||
'has_fhir' => $doc !== null ? 1 : 0,
|
||||
'cooling_active' => $isCoolingActive ? 1 : 0,
|
||||
'cooling_remaining_minutes' => $coolingRemainingMinutes,
|
||||
'auto_link_at' => $autoLinkAt,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1640,33 +1640,44 @@ class Ipd_discharge extends BaseController
|
|||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
$saved = false;
|
||||
if (! empty($existing['id'])) {
|
||||
return (bool) $builder
|
||||
$saved = (bool) $builder
|
||||
->where('id', (int) $existing['id'])
|
||||
->update(['content' => $content]);
|
||||
} else {
|
||||
$insert = [
|
||||
'ipd_id' => $ipdId,
|
||||
'content' => $content,
|
||||
];
|
||||
|
||||
// Legacy schema has NOT NULL columns without defaults.
|
||||
$userLabel = substr($this->currentUserLabel(), 0, 50);
|
||||
if ($this->db->fieldExists('created_by', 'ipd_discharge')) {
|
||||
$insert['created_by'] = $userLabel;
|
||||
}
|
||||
if ($this->db->fieldExists('checked_by', 'ipd_discharge')) {
|
||||
$insert['checked_by'] = $userLabel;
|
||||
}
|
||||
if ($this->db->fieldExists('created_datetime', 'ipd_discharge')) {
|
||||
$insert['created_datetime'] = date('Y-m-d H:i:s');
|
||||
}
|
||||
if ($this->db->fieldExists('ipd_discharge_print', 'ipd_discharge')) {
|
||||
$insert['ipd_discharge_print'] = 0;
|
||||
}
|
||||
|
||||
$saved = (bool) $builder->insert($insert);
|
||||
}
|
||||
|
||||
$insert = [
|
||||
'ipd_id' => $ipdId,
|
||||
'content' => $content,
|
||||
];
|
||||
|
||||
// Legacy schema has NOT NULL columns without defaults.
|
||||
$userLabel = substr($this->currentUserLabel(), 0, 50);
|
||||
if ($this->db->fieldExists('created_by', 'ipd_discharge')) {
|
||||
$insert['created_by'] = $userLabel;
|
||||
}
|
||||
if ($this->db->fieldExists('checked_by', 'ipd_discharge')) {
|
||||
$insert['checked_by'] = $userLabel;
|
||||
}
|
||||
if ($this->db->fieldExists('created_datetime', 'ipd_discharge')) {
|
||||
$insert['created_datetime'] = date('Y-m-d H:i:s');
|
||||
}
|
||||
if ($this->db->fieldExists('ipd_discharge_print', 'ipd_discharge')) {
|
||||
$insert['ipd_discharge_print'] = 0;
|
||||
if ($saved && $this->db->tableExists('abdm_work_tasks')) {
|
||||
$this->db->table('abdm_work_tasks')
|
||||
->where('task_type', 'ipd_discharge_publish')
|
||||
->where('entity_id', (string) $ipdId)
|
||||
->whereIn('status', ['pending', 'in_progress'])
|
||||
->update(['updated_at' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
|
||||
return (bool) $builder->insert($insert);
|
||||
return $saved;
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'Discharge content save failed for IPD {ipd}: {msg}', [
|
||||
'ipd' => $ipdId,
|
||||
|
|
|
|||
|
|
@ -703,6 +703,7 @@ class Opd_prescription extends BaseController
|
|||
$fhirStored = false;
|
||||
try {
|
||||
$fhirStored = $this->storePrescriptionFhirBundle((int) $opdId, (int) $recordId, $patientRow, (array) $opdRow);
|
||||
$this->createOpdPrescriptionWorkTask((int) $opdId, (int) $recordId);
|
||||
} catch (\Throwable $fhirEx) {
|
||||
log_message('error', 'FHIR bundle store failed during opd save: ' . $fhirEx->getMessage() . ' in ' . $fhirEx->getFile() . ':' . $fhirEx->getLine());
|
||||
}
|
||||
|
|
@ -11790,14 +11791,36 @@ class Opd_prescription extends BaseController
|
|||
}
|
||||
$generatedBy = (string) ($userId ?? session('user_id') ?? 'system');
|
||||
|
||||
$inserted = (bool) $this->db->table('opd_fhir_documents')->insert([
|
||||
'opd_id' => $opdId,
|
||||
'opd_session_id' => $sessionId,
|
||||
'bundle_type' => 'OPConsultRecord',
|
||||
'bundle_json' => $bundleJson,
|
||||
'generated_by' => $generatedBy,
|
||||
'generated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
||||
]);
|
||||
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
||||
$existingDoc = $this->db->table('opd_fhir_documents')
|
||||
->where('opd_id', $opdId)
|
||||
->where('opd_session_id', $sessionId)
|
||||
->where('bundle_type', 'OPConsultRecord')
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (! empty($existingDoc['id'])) {
|
||||
$inserted = (bool) $this->db->table('opd_fhir_documents')
|
||||
->where('id', (int) $existingDoc['id'])
|
||||
->update([
|
||||
'bundle_json' => $bundleJson,
|
||||
'generated_by' => $generatedBy,
|
||||
'generated_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
} else {
|
||||
$inserted = (bool) $this->db->table('opd_fhir_documents')->insert([
|
||||
'opd_id' => $opdId,
|
||||
'opd_session_id' => $sessionId,
|
||||
'bundle_type' => 'OPConsultRecord',
|
||||
'bundle_json' => $bundleJson,
|
||||
'generated_by' => $generatedBy,
|
||||
'generated_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $inserted) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ class AbdmTaskBoardSyncService
|
|||
$summary = [
|
||||
'eligible' => 0,
|
||||
'linked' => 0,
|
||||
'cooling' => 0,
|
||||
'failed' => 0,
|
||||
'skipped' => 0,
|
||||
'details' => [],
|
||||
|
|
@ -200,8 +201,6 @@ class AbdmTaskBoardSyncService
|
|||
continue;
|
||||
}
|
||||
|
||||
$summary['eligible']++;
|
||||
|
||||
$consultDate = (string) ($row['apointment_date'] ?? '');
|
||||
$visitDate = $consultDate !== '' ? date('Y-m-d', strtotime($consultDate)) : date('Y-m-d');
|
||||
$derivedCcRef = 'OPD-' . $opdId . '-S' . ($sessionId > 0 ? $sessionId : 0) . '-' . $visitDate;
|
||||
|
|
@ -211,6 +210,33 @@ class AbdmTaskBoardSyncService
|
|||
$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'));
|
||||
|
||||
$cooling = self::calculateCooling('opd_prescription_publish', $lastModified);
|
||||
if ($cooling['is_cooling_active']) {
|
||||
$summary['cooling']++;
|
||||
$summary['details'][] = [
|
||||
'opd_id' => $opdId,
|
||||
'patient' => trim((string) ($row['P_name'] ?? '')),
|
||||
'abha' => trim((string) ($row['abha_id'] ?? '')),
|
||||
'care_context' => $careContextRef,
|
||||
'last_modified' => $lastModified,
|
||||
'remaining_minutes' => $cooling['remaining_minutes'],
|
||||
'auto_link_at' => $cooling['auto_link_at'],
|
||||
'status' => 'cooling',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$summary['eligible']++;
|
||||
|
||||
if ($dryRun) {
|
||||
$summary['details'][] = [
|
||||
'opd_id' => $opdId,
|
||||
|
|
@ -521,6 +547,7 @@ class AbdmTaskBoardSyncService
|
|||
$summary = [
|
||||
'eligible' => 0,
|
||||
'linked' => 0,
|
||||
'cooling' => 0,
|
||||
'failed' => 0,
|
||||
'skipped' => 0,
|
||||
'details' => [],
|
||||
|
|
@ -552,21 +579,6 @@ class AbdmTaskBoardSyncService
|
|||
return $summary;
|
||||
}
|
||||
|
||||
$summary['eligible'] = count($tasks);
|
||||
|
||||
if ($dryRun) {
|
||||
foreach ($tasks as $t) {
|
||||
$summary['details'][] = [
|
||||
'task_id' => (int) $t['id'],
|
||||
'task_type' => $t['task_type'],
|
||||
'entity_id' => $t['entity_id'],
|
||||
'patient' => $t['patient_name'] ?? '',
|
||||
'status' => 'dry_run_eligible',
|
||||
];
|
||||
}
|
||||
return $summary;
|
||||
}
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$taskId = (int) $task['id'];
|
||||
$taskType = (string) $task['task_type'];
|
||||
|
|
@ -574,6 +586,38 @@ class AbdmTaskBoardSyncService
|
|||
$patientId = (int) ($task['patient_id'] ?? 0);
|
||||
$abhaId = trim((string) ($task['abha_id'] ?? ''));
|
||||
|
||||
// Check cooling period from the latest modification/refresh timestamp
|
||||
$lastModified = ! empty($task['updated_at']) ? $task['updated_at'] : ($task['created_at'] ?? date('Y-m-d H:i:s'));
|
||||
$cooling = self::calculateCooling($taskType, $lastModified);
|
||||
|
||||
if ($cooling['is_cooling_active']) {
|
||||
$summary['cooling']++;
|
||||
$summary['details'][] = [
|
||||
'task_id' => $taskId,
|
||||
'task_type' => $taskType,
|
||||
'entity_id' => $entityId,
|
||||
'patient' => $task['patient_name'] ?? '',
|
||||
'last_modified' => $lastModified,
|
||||
'remaining_minutes' => $cooling['remaining_minutes'],
|
||||
'auto_link_at' => $cooling['auto_link_at'],
|
||||
'status' => 'cooling',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$summary['eligible']++;
|
||||
$summary['details'][] = [
|
||||
'task_id' => $taskId,
|
||||
'task_type' => $taskType,
|
||||
'entity_id' => $entityId,
|
||||
'patient' => $task['patient_name'] ?? '',
|
||||
'status' => 'dry_run_eligible',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$summary['eligible']++;
|
||||
$result = $this->processIndividualWorkTask($task);
|
||||
|
||||
if ($result['ok'] === 1) {
|
||||
|
|
@ -811,4 +855,90 @@ class AbdmTaskBoardSyncService
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cooling period state for any task or document.
|
||||
*
|
||||
* Rules:
|
||||
* - IPD Discharge Summary: 24 hours from last modified/printed time.
|
||||
* - All other HI types (OPD Consult/Prescription, Lab, Radiology, Wellness, Immunization, etc.):
|
||||
* 60 minutes from last modified/updated/printed time.
|
||||
* - Any modification/update in HMS refreshes the timestamp, resetting the cooling period.
|
||||
*
|
||||
* @param string $taskType Task type, HI type, or entity name
|
||||
* @param string|null $lastModified DateTime string of the last update/generation
|
||||
* @return array{
|
||||
* is_cooling_active: bool,
|
||||
* remaining_seconds: int,
|
||||
* remaining_minutes: int,
|
||||
* required_seconds: int,
|
||||
* last_modified: string,
|
||||
* auto_link_at: string
|
||||
* }
|
||||
*/
|
||||
public static function calculateCooling(string $taskType, ?string $lastModified): array
|
||||
{
|
||||
$cfg = config('AbdmConnector');
|
||||
$autoLinkEnabled = (bool) ($cfg->autoLinkEnabled ?? true);
|
||||
if (! $autoLinkEnabled) {
|
||||
return [
|
||||
'is_cooling_active' => true,
|
||||
'cooling_active' => true,
|
||||
'remaining_seconds' => PHP_INT_MAX,
|
||||
'remaining_minutes' => PHP_INT_MAX,
|
||||
'required_seconds' => PHP_INT_MAX,
|
||||
'last_modified' => (string) $lastModified,
|
||||
'auto_link_at' => 'disabled',
|
||||
];
|
||||
}
|
||||
|
||||
$typeLower = strtolower(trim($taskType));
|
||||
if (in_array($typeLower, ['ipd_discharge_publish', 'ipd_discharge', 'dischargesummaryrecord', 'ipd'], true)) {
|
||||
$hours = max(1, (int) ($cfg->autoLinkDelayDischargeHours ?? 24));
|
||||
$requiredSeconds = $hours * 3600;
|
||||
} else {
|
||||
$minutes = max(1, (int) ($cfg->autoLinkDelayMinutes ?? 60));
|
||||
$requiredSeconds = $minutes * 60;
|
||||
}
|
||||
|
||||
$lastTimestamp = ! empty($lastModified) ? strtotime($lastModified) : 0;
|
||||
if ($lastTimestamp <= 0) {
|
||||
$lastTimestamp = time();
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$elapsedSeconds = max(0, $now - $lastTimestamp);
|
||||
$remainingSeconds = max(0, $requiredSeconds - $elapsedSeconds);
|
||||
$isCoolingActive = ($remainingSeconds > 0);
|
||||
$remainingMinutes = (int) ceil($remainingSeconds / 60);
|
||||
$autoLinkAt = date('Y-m-d H:i:s', $lastTimestamp + $requiredSeconds);
|
||||
|
||||
return [
|
||||
'is_cooling_active' => $isCoolingActive,
|
||||
'cooling_active' => $isCoolingActive,
|
||||
'remaining_seconds' => $remainingSeconds,
|
||||
'remaining_minutes' => $remainingMinutes,
|
||||
'required_seconds' => $requiredSeconds,
|
||||
'last_modified' => (string) $lastModified,
|
||||
'auto_link_at' => $autoLinkAt,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance wrapper for calculateCooling.
|
||||
*
|
||||
* @return array{
|
||||
* is_cooling_active: bool,
|
||||
* remaining_seconds: int,
|
||||
* remaining_minutes: int,
|
||||
* required_seconds: int,
|
||||
* last_modified: string,
|
||||
* auto_link_at: string
|
||||
* }
|
||||
*/
|
||||
public function resolveCoolingState(string $taskType, ?string $lastModified): array
|
||||
{
|
||||
return self::calculateCooling($taskType, $lastModified);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ABDM Work Task Board</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background: #f5f7fb; }
|
||||
.container-wrap { width: 100%; margin: 0; padding: 20px 24px; box-sizing: border-box; }
|
||||
|
|
@ -168,6 +169,13 @@
|
|||
<?php elseif ($bridgeSubmitted): ?>
|
||||
<span class="badge bg-info status-pill">BRIDGE SUBMITTED</span>
|
||||
<?php if ($bridgeCareContext !== ''): ?><div class="small"><code><?= esc($bridgeCareContext) ?></code></div><?php endif; ?>
|
||||
<?php elseif (! empty($t['cooling_active'])): ?>
|
||||
<span class="badge bg-warning text-dark status-pill" title="<?= esc((string) ($t['cooling_tooltip'] ?? '')) ?>"><i class="bi bi-clock-history"></i> <?= esc((string) ($t['cooling_label'] ?? 'COOLING')) ?></span>
|
||||
<?php if (! empty($t['auto_link_at']) && $t['auto_link_at'] !== 'disabled'): ?>
|
||||
<div class="small text-muted" style="font-size:10px;">Auto-link: <?= esc(substr((string) $t['auto_link_at'], 11, 5)) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php elseif (! empty($t['ready_for_autolink'])): ?>
|
||||
<span class="badge bg-primary status-pill" title="<?= esc((string) ($t['cooling_tooltip'] ?? '')) ?>"><i class="bi bi-lightning-charge"></i> READY TO LINK</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary status-pill"><?= esc((string) ($t['status'] ?? 'pending')) ?></span>
|
||||
<?php endif; ?>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user