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:
DevSoftTech Server 2026-09-28 18:29:50 +05:30
parent ed57228b98
commit 61206784f3
8 changed files with 337 additions and 58 deletions

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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,
]);
}

View File

@ -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,

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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; ?>