feat(abdm): add task status filtering, search, and completed task visibility on task board

- Add getTasks() method with status filtering (all, open, completed, failed) to AbdmWorkTaskService
- Enhance AbdmTaskBoard to enrich all task types with health_records, invoice_code, and care contexts
- Add status toggle filter buttons (All, Open, Completed) with dynamic count badges to Task Board UI
- Add real-time fuzzy/keyword search input for instant filtering by invoice ID, patient name, ABHA, or task code
- Render completed/linked tasks with green LINKED badge, gateway reference, and FHIR Preview button
- Add unit tests in AbdmTaskBoardBackfillTest
This commit is contained in:
DevSoftTech Server 2026-10-01 19:40:32 +05:30
parent 2569f694c1
commit e43bfbad71
4 changed files with 372 additions and 62 deletions

View File

@ -13,6 +13,7 @@ class AbdmTaskBoard extends BaseController
public function __construct()
{
$this->taskService = new AbdmWorkTaskService();
$this->db = db_connect();
}
public function index()
@ -22,7 +23,14 @@ class AbdmTaskBoard extends BaseController
$this->backfillImmunizationTasks();
$this->backfillHealthDocumentTasks();
$this->backfillWellnessTasks();
$tasks = $this->enrichTasksWithCoolingState($this->enrichImmunizationPushState($this->taskService->getOpenTasks(300)));
$taskStatus = strtolower(trim((string) ($this->request->getGet('task_status') ?? 'all')));
if (! in_array($taskStatus, ['all', 'open', 'completed', 'failed'], true)) {
$taskStatus = 'all';
}
$rawTasks = $this->taskService->getTasks($taskStatus, 600);
$tasks = $this->enrichTasksWithCoolingState($this->enrichTasksWithHealthRecordState($rawTasks));
$dateFrom = trim((string) ($this->request->getGet('date_from') ?? date('Y-m-d')));
$dateTo = trim((string) ($this->request->getGet('date_to') ?? date('Y-m-d')));
@ -38,6 +46,7 @@ class AbdmTaskBoard extends BaseController
return view('abdm/task_board', [
'tasks' => $tasks,
'task_status' => $taskStatus,
'dashboard_metrics' => $this->getDashboardMetrics($dateFrom, $dateTo),
'dashboard_date_from' => $dateFrom,
'dashboard_date_to' => $dateTo,
@ -55,9 +64,17 @@ class AbdmTaskBoard extends BaseController
$this->backfillImmunizationTasks();
$this->backfillHealthDocumentTasks();
$this->backfillWellnessTasks();
$taskStatus = strtolower(trim((string) ($this->request->getGet('task_status') ?? 'all')));
if (! in_array($taskStatus, ['all', 'open', 'completed', 'failed'], true)) {
$taskStatus = 'all';
}
$rawTasks = $this->taskService->getTasks($taskStatus, 600);
return $this->response->setJSON([
'ok' => 1,
'tasks' => $this->enrichTasksWithCoolingState($this->enrichImmunizationPushState($this->taskService->getOpenTasks(300))),
'task_status' => $taskStatus,
'tasks' => $this->enrichTasksWithCoolingState($this->enrichTasksWithHealthRecordState($rawTasks)),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
@ -89,48 +106,91 @@ class AbdmTaskBoard extends BaseController
}
/** @param array<int,array<string,mixed>> $tasks */
private function enrichImmunizationPushState(array $tasks): array
private function enrichTasksWithHealthRecordState(array $tasks): array
{
if ($tasks === [] || ! $this->db->tableExists('health_records')) {
return $tasks;
if ($tasks === []) {
return [];
}
$entityIds = [];
foreach ($tasks as $task) {
if (($task['task_type'] ?? '') === 'immunization_record_publish') {
$entityIds[] = (string) ($task['entity_id'] ?? '');
// 1. Extract invoice metadata and resolve invoice_code
$invoiceIds = [];
foreach ($tasks as &$task) {
$payload = json_decode((string) ($task['payload_json'] ?? ''), true);
$meta = (is_array($payload) && isset($payload['meta']) && is_array($payload['meta'])) ? $payload['meta'] : [];
if (! empty($meta['invoice_id'])) {
$task['invoice_id'] = (int) $meta['invoice_id'];
$invoiceIds[] = (int) $meta['invoice_id'];
}
if (! empty($meta['invoice_code'])) {
$task['invoice_code'] = (string) $meta['invoice_code'];
}
}
$entityIds = array_values(array_filter(array_unique($entityIds), static fn (string $id): bool => $id !== ''));
if ($entityIds === []) {
return $tasks;
unset($task);
$db = $this->db ?? db_connect();
if (! empty($invoiceIds) && $db->tableExists('invoice_master')) {
$invRows = $db->table('invoice_master')
->select('id, invoice_code')
->whereIn('id', array_unique($invoiceIds))
->get()
->getResultArray();
$codeMap = [];
foreach ($invRows as $ir) {
$codeMap[(int) $ir['id']] = (string) ($ir['invoice_code'] ?? '');
}
foreach ($tasks as &$task) {
if (! empty($task['invoice_id']) && empty($task['invoice_code']) && isset($codeMap[(int) $task['invoice_id']])) {
$task['invoice_code'] = $codeMap[(int) $task['invoice_id']];
}
}
unset($task);
}
$latest = [];
$rows = $this->db->table('health_records')
->select('id, entity_id, push_status, care_context_reference, linked_at')
->where('hi_type', 'ImmunizationRecord')
->where('entity_type', 'immunization')
->whereIn('entity_id', $entityIds)
->orderBy('id', 'DESC')
->get()
->getResultArray();
foreach ($rows as $row) {
$entityId = (string) ($row['entity_id'] ?? '');
if ($entityId !== '' && ! isset($latest[$entityId])) {
$latest[$entityId] = $row;
// 2. Lookup health_records by entity_id
$latestHr = [];
if ($db->tableExists('health_records')) {
$entityIds = [];
foreach ($tasks as $task) {
$entityId = trim((string) ($task['entity_id'] ?? ''));
if ($entityId !== '') {
$entityIds[] = $entityId;
}
}
$entityIds = array_values(array_filter(array_unique($entityIds)));
if ($entityIds !== []) {
$rows = $db->table('health_records')
->select('id, entity_type, entity_id, push_status, care_context_reference, linked_at, abdm_txn_id')
->whereIn('entity_id', $entityIds)
->orderBy('id', 'DESC')
->get()
->getResultArray();
foreach ($rows as $row) {
$eid = (string) ($row['entity_id'] ?? '');
if ($eid !== '' && ! isset($latestHr[$eid])) {
$latestHr[$eid] = $row;
}
}
}
}
foreach ($tasks as &$task) {
if (($task['task_type'] ?? '') !== 'immunization_record_publish') {
continue;
}
$healthRecord = $latest[(string) ($task['entity_id'] ?? '')] ?? null;
$pushStatus = strtolower(trim((string) ($healthRecord['push_status'] ?? '')));
$task['bridge_health_record_id'] = (int) ($healthRecord['id'] ?? 0);
$eid = (string) ($task['entity_id'] ?? '');
$hr = $latestHr[$eid] ?? null;
$pushStatus = strtolower(trim((string) ($hr['push_status'] ?? '')));
$task['bridge_health_record_id'] = (int) ($hr['id'] ?? 0);
$task['bridge_push_status'] = $pushStatus;
$task['bridge_care_context_reference'] = trim((string) ($healthRecord['care_context_reference'] ?? ''));
$careContext = trim((string) ($hr['care_context_reference'] ?? ''));
if ($careContext === '') {
$resultText = (string) ($task['last_action_result'] ?? '');
if (preg_match('/(RAD-\d+-\d+|LAB-\d+-\d+|OPD-\d+-\d+|INVOICE-\w+-\d+-\d+|IMM-\d+-\d+|DIS-\d+-\d+)/i', $resultText, $ccm)) {
$careContext = $ccm[1];
}
}
$task['bridge_care_context_reference'] = $careContext;
$task['bridge_submitted'] = in_array($pushStatus, ['queued', 'pushed', 'linked'], true) ? 1 : 0;
}
unset($task);
@ -138,6 +198,12 @@ class AbdmTaskBoard extends BaseController
return $tasks;
}
/** @param array<int,array<string,mixed>> $tasks */
private function enrichImmunizationPushState(array $tasks): array
{
return $this->enrichTasksWithHealthRecordState($tasks);
}
/** @param array<int,array<string,mixed>> $tasks */
private function enrichTasksWithCoolingState(array $tasks): array
{

View File

@ -115,24 +115,51 @@ class AbdmWorkTaskService
}
/**
* @param string $status 'all' (default), 'open', 'completed', or 'failed'
* @param int $limit Max rows to fetch
* @param string|null $taskType Optional task type filter
* @return array<int, array<string, mixed>>
*/
public function getOpenTasks(int $limit = 200): array
public function getTasks(string $status = 'all', int $limit = 500, ?string $taskType = null): array
{
if (! $this->db->tableExists('abdm_work_tasks')) {
return [];
}
$this->syncPatientAbhaBacklog(max(200, $limit));
$status = strtolower(trim($status));
if (in_array($status, ['open', 'all'], true)) {
$this->syncPatientAbhaBacklog(max(200, min(500, $limit)));
}
return $this->db->table('abdm_work_tasks')
->whereIn('status', ['pending', 'in_progress'])
$builder = $this->db->table('abdm_work_tasks');
if ($status === 'open') {
$builder->whereIn('status', ['pending', 'in_progress']);
} elseif ($status === 'completed') {
$builder->whereIn('status', ['completed', 'linked']);
} elseif ($status === 'failed') {
$builder->whereIn('status', ['failed', 'cancelled']);
}
if (! empty($taskType)) {
$builder->where('task_type', trim($taskType));
}
return $builder
->orderBy('id', 'DESC')
->limit(max(1, $limit))
->get()
->getResultArray();
}
/**
* @return array<int, array<string, mixed>>
*/
public function getOpenTasks(int $limit = 200): array
{
return $this->getTasks('open', $limit);
}
public function markTaskStatus(int $taskId, string $status, string $resultText = ''): bool
{
if (! $this->db->tableExists('abdm_work_tasks') || $taskId <= 0) {

View File

@ -104,6 +104,20 @@
</div>
<div class="card shadow-sm" id="taskTableCard">
<div class="card-header py-2 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<strong id="taskTableTitle">Work Queue</strong>
<span class="badge bg-primary" id="taskTableCount"><?= count($tasks ?? []) ?></span>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<input type="search" class="form-control form-control-sm" id="taskTableSearch" placeholder="Search invoice, patient, code..." style="width: 230px;">
<div class="btn-group btn-group-sm" role="group" id="statusFilterGroup">
<button type="button" class="btn btn-sm btn-primary status-filter-btn active" data-status="all">All <span class="badge bg-light text-dark ms-1" id="countAll">0</span></button>
<button type="button" class="btn btn-sm btn-outline-primary status-filter-btn" data-status="open">Open <span class="badge bg-light text-dark ms-1" id="countOpen">0</span></button>
<button type="button" class="btn btn-sm btn-outline-primary status-filter-btn" data-status="completed">Completed <span class="badge bg-light text-dark ms-1" id="countCompleted">0</span></button>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-striped mb-0" id="taskTable">
@ -132,15 +146,37 @@
$meta = (isset($payload['meta']) && is_array($payload['meta'])) ? $payload['meta'] : [];
$opdId = (int) ($meta['opd_id'] ?? 0);
$opdSessionId = (int) ($meta['opd_session_id'] ?? 0);
$invoiceId = (int) ($t['invoice_id'] ?? ($meta['invoice_id'] ?? 0));
$invoiceCode = trim((string) ($t['invoice_code'] ?? ($meta['invoice_code'] ?? '')));
$invoiceDisplay = $invoiceCode !== '' ? $invoiceCode : ($invoiceId > 0 ? ('#' . $invoiceId) : '');
$showSandbox = ($type === 'opd_prescription_publish' && $opdId > 0);
$showPreview = ($type === 'opd_prescription_publish' && $opdId > 0)
|| in_array($type, ['lab_report_publish', 'radiology_report_publish', 'ipd_discharge_publish', 'immunization_record_publish', 'health_document_publish', 'wellness_record_publish'], true);
$bridgeSubmitted = (int) ($t['bridge_submitted'] ?? 0) === 1;
$bridgeCareContext = trim((string) ($t['bridge_care_context_reference'] ?? ''));
$rawTaskStatus = strtolower(trim((string) ($t['status'] ?? 'pending')));
$isTaskLinked = in_array($rawTaskStatus, ['completed', 'linked'], true) || ($bridgeSubmitted && strtolower((string) ($t['bridge_push_status'] ?? '')) === 'linked');
$searchText = strtolower(implode(' ', array_filter([
(string) ($t['task_code'] ?? ''),
(string) ($t['task_type'] ?? ''),
(string) ($t['patient_name'] ?? ''),
(string) ($t['patient_id'] ?? ''),
(string) ($t['abha_id'] ?? ''),
(string) ($t['entity_id'] ?? ''),
$invoiceCode,
$invoiceId > 0 ? (string) $invoiceId : '',
$bridgeCareContext,
(string) ($t['last_action_result'] ?? ''),
])));
?>
<tr
data-task-id="<?= (int) ($t['id'] ?? 0) ?>"
data-task-type="<?= esc((string) ($t['task_type'] ?? '')) ?>"
data-task-status="<?= esc($rawTaskStatus) ?>"
data-is-linked="<?= $isTaskLinked ? '1' : '0' ?>"
data-entity-id="<?= esc((string) ($t['entity_id'] ?? '')) ?>"
data-patient-id="<?= (int) ($t['patient_id'] ?? 0) ?>"
data-abha-id="<?= esc($abhaId) ?>"
@ -148,32 +184,44 @@
data-opd-session-id="<?= $opdSessionId ?>"
data-sandbox-eligible="<?= $showSandbox ? '1' : '0' ?>"
data-bridge-submitted="<?= $bridgeSubmitted ? '1' : '0' ?>"
data-search="<?= esc($searchText) ?>"
>
<td><?= (int) ($t['id'] ?? 0) ?></td>
<td>
<div><strong><?= esc((string) ($t['task_code'] ?? '')) ?></strong></div>
<div class="text-muted small"><?= esc((string) ($t['task_type'] ?? '')) ?></div>
<?php if ($invoiceDisplay !== ''): ?>
<div class="small text-primary fw-semibold"><i class="bi bi-receipt me-1"></i>Inv: <?= esc($invoiceDisplay) ?></div>
<?php endif; ?>
</td>
<td>
<div><?= esc((string) ($t['patient_name'] ?? '')) ?></div>
<div class="text-muted small">#<?= (int) ($t['patient_id'] ?? 0) ?></div>
</td>
<td><input type="text" class="form-control form-control-sm abha-input" value="<?= esc((string) ($t['abha_id'] ?? '')) ?>" placeholder="14-digit ABHA"></td>
<td>
<?php if ($isTaskLinked): ?>
<div class="small"><span class="badge bg-light text-dark border"><i class="bi bi-person-check text-success me-1"></i><?= esc((string) ($t['abha_id'] ?? '')) ?></span></div>
<?php else: ?>
<input type="text" class="form-control form-control-sm abha-input" value="<?= esc((string) ($t['abha_id'] ?? '')) ?>" placeholder="14-digit ABHA">
<?php endif; ?>
</td>
<td>
<div><?= esc((string) ($t['entity_type'] ?? '')) ?></div>
<div class="text-muted small"><?= esc((string) ($t['entity_id'] ?? '')) ?></div>
</td>
<td>
<?php
$rawTaskStatus = strtolower(trim((string) ($t['status'] ?? 'pending')));
$isTaskLinked = in_array($rawTaskStatus, ['completed', 'linked'], true) || ($bridgeSubmitted && strtolower((string) ($t['bridge_push_status'] ?? '')) === 'linked');
?>
<?php if ($isTaskLinked): ?>
<span class="badge bg-success status-pill">LINKED</span>
<?php if ($bridgeCareContext !== ''): ?><div class="small"><code><?= esc($bridgeCareContext) ?></code></div><?php endif; ?>
<span class="badge bg-success status-pill"><i class="bi bi-check-circle me-1"></i>LINKED</span>
<?php if ($bridgeCareContext !== ''): ?>
<div class="small mt-1"><code><?= esc($bridgeCareContext) ?></code></div>
<?php elseif (! empty($t['last_action_result'])): ?>
<div class="small text-muted mt-1" style="font-size:10px; max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<?= esc((string) $t['last_action_result']) ?>">
<?= esc((string) $t['last_action_result']) ?>
</div>
<?php endif; ?>
<?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 if ($bridgeCareContext !== ''): ?><div class="small mt-1"><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'): ?>
@ -186,24 +234,37 @@
<?php endif; ?>
</td>
<td>
<div class="d-flex gap-1 flex-wrap">
<?php if ($type === 'patient_abha_create' || $type === 'patient_abha_link'): ?>
<button type="button" class="btn btn-sm btn-outline-success action-btn" data-action="create_abha">Create ABHA</button>
<?php elseif ($type === 'patient_abha_update'): ?>
<button type="button" class="btn btn-sm btn-outline-primary action-btn" data-action="update_abha">Update ABHA</button>
<div class="d-flex gap-1 flex-wrap align-items-center">
<?php if ($isTaskLinked): ?>
<span class="badge bg-success-subtle text-success border border-success-subtle py-1 px-2"><i class="bi bi-check2-all me-1"></i>Done</span>
<?php if ($showPreview): ?>
<button type="button" class="btn btn-sm btn-outline-primary preview-fhir-btn"><i class="bi bi-eye me-1"></i>Preview FHIR</button>
<?php endif; ?>
<?php else: ?>
<?php if ($type === 'patient_abha_create' || $type === 'patient_abha_link'): ?>
<button type="button" class="btn btn-sm btn-outline-success action-btn" data-action="create_abha">Create ABHA</button>
<?php elseif ($type === 'patient_abha_update'): ?>
<button type="button" class="btn btn-sm btn-outline-primary action-btn" data-action="update_abha">Update ABHA</button>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-success btn-row-hip-link" title="HIP-Initiated Care Context Linking"><i class="bi bi-link-45deg"></i> Link Care Context</button>
<?php if ($showPreview): ?>
<button type="button" class="btn btn-sm btn-outline-primary preview-fhir-btn">Preview FHIR</button>
<?php endif; ?>
<?php if ($showSandbox): ?>
<button type="button" class="btn btn-sm btn-outline-info sandbox-btn">Sandbox</button>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-dark close-btn">Close</button>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-success btn-row-hip-link" title="HIP-Initiated Care Context Linking"><i class="bi bi-link-45deg"></i> Link Care Context</button>
<?php if ($showPreview): ?>
<button type="button" class="btn btn-sm btn-outline-primary preview-fhir-btn">Preview FHIR</button>
<?php endif; ?>
<?php if ($showSandbox): ?>
<button type="button" class="btn btn-sm btn-outline-info sandbox-btn">Sandbox</button>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-dark close-btn">Close</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<tr id="taskTableEmptyRow" class="d-none">
<td colspan="7" class="text-center py-4 text-muted">
<i class="bi bi-inbox fs-4 d-block mb-1"></i>
<span>No tasks match the selected filter.</span>
</td>
</tr>
</tbody>
</table>
</div>
@ -665,7 +726,82 @@
var taskTableCard = document.getElementById('taskTableCard');
var dashboardCard = document.getElementById('dashboardCard');
var currentCategoryFilter = 'dashboard';
var currentStatusFilter = 'all';
var currentSearchQuery = '';
function updateTaskTableDisplay() {
var rows = document.querySelectorAll('#taskTable tbody tr:not(#taskTableEmptyRow)');
var emptyRow = document.getElementById('taskTableEmptyRow');
var q = currentSearchQuery.trim().toLowerCase();
var visibleCount = 0;
var countAll = 0;
var countOpen = 0;
var countCompleted = 0;
rows.forEach(function (row) {
var type = (row.getAttribute('data-task-type') || '').toLowerCase();
var status = (row.getAttribute('data-task-status') || '').toLowerCase();
var isLinked = row.getAttribute('data-is-linked') === '1' || status === 'completed' || status === 'linked';
var searchData = (row.getAttribute('data-search') || '').toLowerCase();
// Check if matches current category
var categoryMatch = (currentCategoryFilter === 'all' || type === currentCategoryFilter.toLowerCase());
if (!categoryMatch) {
row.style.display = 'none';
return;
}
// Tally status counts for this category
countAll++;
if (isLinked) {
countCompleted++;
} else {
countOpen++;
}
// Check status match
var statusMatch = false;
if (currentStatusFilter === 'all') {
statusMatch = true;
} else if (currentStatusFilter === 'completed') {
statusMatch = isLinked;
} else if (currentStatusFilter === 'open') {
statusMatch = !isLinked;
} else if (currentStatusFilter === status) {
statusMatch = true;
}
// Check search match
var searchMatch = !q || searchData.indexOf(q) !== -1;
if (statusMatch && searchMatch) {
row.style.display = '';
visibleCount++;
} else {
row.style.display = 'none';
}
});
// Update counts in UI
var elCountAll = document.getElementById('countAll');
var elCountOpen = document.getElementById('countOpen');
var elCountCompleted = document.getElementById('countCompleted');
var elTableCount = document.getElementById('taskTableCount');
if (elCountAll) elCountAll.textContent = countAll;
if (elCountOpen) elCountOpen.textContent = countOpen;
if (elCountCompleted) elCountCompleted.textContent = countCompleted;
if (elTableCount) elTableCount.textContent = visibleCount;
if (emptyRow) {
emptyRow.classList.toggle('d-none', visibleCount > 0);
}
}
function applyFilter(filter) {
currentCategoryFilter = filter;
// Hide dedicated section cards
if (opdBookCard) opdBookCard.classList.add('d-none');
if (opdConsultCard) opdConsultCard.classList.add('d-none');
@ -686,11 +822,12 @@
if (invoiceCard) invoiceCard.classList.remove('d-none');
} else {
if (taskTableCard) taskTableCard.classList.remove('d-none');
var rows = document.querySelectorAll('#taskTable tbody tr');
rows.forEach(function (row) {
var type = (row.getAttribute('data-task-type') || '').toLowerCase();
row.style.display = (type === filter.toLowerCase()) ? '' : 'none';
});
var titleEl = document.getElementById('taskTableTitle');
if (titleEl) {
var btn = document.querySelector('.filter-btn[data-filter="' + filter + '"]');
titleEl.textContent = btn ? btn.textContent.trim() : 'Work Queue';
}
updateTaskTableDisplay();
}
document.querySelectorAll('.filter-btn').forEach(function (btn) {
@ -1270,6 +1407,27 @@
});
});
document.querySelectorAll('.status-filter-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
document.querySelectorAll('.status-filter-btn').forEach(function (b) {
b.classList.remove('btn-primary', 'active');
b.classList.add('btn-outline-primary');
});
btn.classList.remove('btn-outline-primary');
btn.classList.add('btn-primary', 'active');
currentStatusFilter = (btn.getAttribute('data-status') || 'all').toLowerCase();
updateTaskTableDisplay();
});
});
var searchInput = document.getElementById('taskTableSearch');
if (searchInput) {
searchInput.addEventListener('input', function () {
currentSearchQuery = searchInput.value || '';
updateTaskTableDisplay();
});
}
document.querySelectorAll('.sandbox-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
openSandboxForRow(btn.closest('tr'));

View File

@ -25,4 +25,63 @@ final class AbdmTaskBoardBackfillTest extends CIUnitTestCase
$this->assertFalse($method->invoke($controller, '12345'));
$this->assertFalse($method->invoke($controller, 'invalid_address'));
}
public function testEnrichTasksWithHealthRecordStateExtractsInvoiceAndCareContext(): void
{
$controller = (new ReflectionClass(AbdmTaskBoard::class))->newInstanceWithoutConstructor();
$method = new ReflectionMethod(AbdmTaskBoard::class, 'enrichTasksWithHealthRecordState');
$method->setAccessible(true);
$mockTasks = [
[
'id' => 686,
'task_code' => 'ABDM-20261001151127-C8C055',
'task_type' => 'radiology_report_publish',
'entity_id' => '51450',
'status' => 'completed',
'payload_json' => json_encode([
'meta' => [
'lab_type' => 3,
'invoice_id' => 40291,
'invoice_code' => 'N26100040291',
],
]),
'last_action_result' => 'Linked by cron: REQ-20261001151202-7447b1c4',
],
[
'id' => 679,
'task_code' => 'ABDM-20261001142918-25422B',
'task_type' => 'radiology_report_publish',
'entity_id' => '51450',
'status' => 'completed',
'payload_json' => json_encode([
'meta' => [
'invoice_id' => 40291,
],
]),
'last_action_result' => 'Care context already linked in ABDM: RAD-51450-20261001',
],
];
$enriched = $method->invoke($controller, $mockTasks);
$this->assertCount(2, $enriched);
$this->assertSame(40291, $enriched[0]['invoice_id']);
$this->assertSame('N26100040291', $enriched[0]['invoice_code']);
$this->assertSame(40291, $enriched[1]['invoice_id']);
// From last_action_result regex fallback
$this->assertSame('RAD-51450-20261001', $enriched[1]['bridge_care_context_reference']);
}
public function testWorkTaskServiceHasGetTasksMethod(): void
{
$serviceClass = new ReflectionClass(\App\Libraries\AbdmWorkTaskService::class);
$this->assertTrue($serviceClass->hasMethod('getTasks'));
$this->assertTrue($serviceClass->hasMethod('getOpenTasks'));
$getTasksMethod = $serviceClass->getMethod('getTasks');
$params = $getTasksMethod->getParameters();
$this->assertSame('status', $params[0]->getName());
$this->assertSame('all', $params[0]->getDefaultValue());
}
}