diff --git a/app/Controllers/AbdmTaskBoard.php b/app/Controllers/AbdmTaskBoard.php index a48f499..545bc02 100644 --- a/app/Controllers/AbdmTaskBoard.php +++ b/app/Controllers/AbdmTaskBoard.php @@ -44,6 +44,8 @@ class AbdmTaskBoard extends BaseController [$dateFrom, $dateTo] = [$dateTo, $dateFrom]; } + $filterAbha = trim((string) ($this->request->getGet('abha_address') ?? $this->request->getGet('abha') ?? '')); + return view('abdm/task_board', [ 'tasks' => $tasks, 'task_status' => $taskStatus, @@ -54,6 +56,8 @@ class AbdmTaskBoard extends BaseController 'opd_book_rows' => $this->getOpdBookRows(), 'opd_consult_rows' => $this->getOpdConsultPublishRows(), 'invoice_rows' => $this->getInvoiceRows(), + 'abha_patients' => $this->getAbhaPatientsList(), + 'filter_abha_address' => $filterAbha, ]); } @@ -112,6 +116,64 @@ class AbdmTaskBoard extends BaseController return []; } + $db = $this->db ?? db_connect(); + + // 0. Batch-resolve patient_master for ABHA addresses, ABHA numbers, p_code, and phone + $patientIds = []; + foreach ($tasks as $task) { + $pId = (int) ($task['patient_id'] ?? 0); + if ($pId > 0) { + $patientIds[] = $pId; + } + } + $patientIds = array_values(array_filter(array_unique($patientIds))); + $patientMap = []; + if (! empty($patientIds) && $db->tableExists('patient_master')) { + $pFields = $db->getFieldNames('patient_master') ?? []; + $pSel = ['id', 'p_code', 'p_fname', 'gender', 'dob']; + if (in_array('p_lname', $pFields, true)) { + $pSel[] = 'p_lname'; + } + if (in_array('mphone1', $pFields, true)) { + $pSel[] = 'mphone1'; + } + if (in_array('abha_address', $pFields, true)) { + $pSel[] = 'abha_address'; + } + if (in_array('abha_id', $pFields, true)) { + $pSel[] = 'abha_id'; + } + if (in_array('abha_no', $pFields, true)) { + $pSel[] = 'abha_no'; + } + + $pRows = $db->table('patient_master')->select(implode(', ', $pSel))->whereIn('id', $patientIds)->get()->getResultArray(); + foreach ($pRows as $pr) { + $rawAddr = trim((string) ($pr['abha_address'] ?? '')); + $rawId = trim((string) ($pr['abha_id'] ?? $pr['abha_no'] ?? '')); + $addr = ''; + $num = ''; + if (str_contains($rawAddr, '@')) { + $addr = $rawAddr; + } elseif (str_contains($rawId, '@')) { + $addr = $rawId; + } + $d1 = preg_replace('/\D/', '', $rawId); + $d2 = preg_replace('/\D/', '', $rawAddr); + if (is_string($d1) && strlen($d1) === 14) { + $num = $d1; + } elseif (is_string($d2) && strlen($d2) === 14) { + $num = $d2; + } + $patientMap[(int) $pr['id']] = [ + 'p_code' => (string) ($pr['p_code'] ?? ''), + 'abha_address' => $addr, + 'abha_number' => $num, + 'phone' => (string) ($pr['mphone1'] ?? ''), + ]; + } + } + // 1. Extract invoice metadata and resolve invoice_code $invoiceIds = []; foreach ($tasks as &$task) { @@ -127,8 +189,6 @@ class AbdmTaskBoard extends BaseController } unset($task); - $db = $this->db ?? db_connect(); - if (! empty($invoiceIds) && $db->tableExists('invoice_master')) { $invRows = $db->table('invoice_master') ->select('id, invoice_code') @@ -260,6 +320,24 @@ class AbdmTaskBoard extends BaseController } } + $pData = $patientMap[$pId] ?? null; + $taskAbha = trim((string) ($task['abha_id'] ?? '')); + $taskAbhaAddr = (string) ($pData['abha_address'] ?? ''); + $taskAbhaNum = (string) ($pData['abha_number'] ?? ''); + + if (str_contains($taskAbha, '@') && $taskAbhaAddr === '') { + $taskAbhaAddr = $taskAbha; + } + $cleanAbha = preg_replace('/\D/', '', $taskAbha); + if (strlen($cleanAbha) === 14 && $taskAbhaNum === '') { + $taskAbhaNum = $cleanAbha; + } + + $task['patient_p_code'] = (string) ($pData['p_code'] ?? ''); + $task['patient_abha_address'] = $taskAbhaAddr; + $task['patient_abha_number'] = $taskAbhaNum; + $task['patient_phone'] = (string) ($pData['phone'] ?? ''); + $task['bridge_care_context_reference'] = $careContext; $task['bridge_submitted'] = in_array($pushStatus, ['queued', 'pushed', 'linked'], true) ? 1 : 0; } @@ -881,7 +959,7 @@ class AbdmTaskBoard extends BaseController if ($this->db->tableExists('opd_master')) { $builder = $this->db->table('opd_master o') - ->select("o.opd_id AS bill_id, o.opd_code AS bill_code, o.p_id AS patient_id, COALESCE(NULLIF(o.P_name, ''), p.p_fname, '') AS patient_name, o.apointment_date AS bill_date, o.opd_fee_amount AS amount, COALESCE(NULLIF(p.abha_address, ''), NULLIF(p.abha_id, ''), '') AS abha_id", false); + ->select("o.opd_id AS bill_id, o.opd_code AS bill_code, o.p_id AS patient_id, COALESCE(NULLIF(o.P_name, ''), p.p_fname, '') AS patient_name, o.apointment_date AS bill_date, o.opd_fee_amount AS amount, COALESCE(p.p_code, '') AS p_code, COALESCE(p.abha_address, '') AS patient_abha_address, COALESCE(p.abha_id, '') AS patient_abha_number, COALESCE(NULLIF(p.abha_address, ''), NULLIF(p.abha_id, ''), '') AS abha_id", false); if ($this->db->tableExists('patient_master')) { $builder->join('patient_master p', 'p.id = o.p_id', 'left'); } @@ -896,7 +974,7 @@ class AbdmTaskBoard extends BaseController if ($this->db->tableExists('invoice_master')) { $builder = $this->db->table('invoice_master i') - ->select("i.id AS bill_id, i.invoice_code AS bill_code, i.attach_id AS patient_id, COALESCE(NULLIF(i.inv_name, ''), p.p_fname, '') AS patient_name, i.inv_date AS bill_date, i.net_amount AS amount, COALESCE(NULLIF(p.abha_address, ''), NULLIF(p.abha_id, ''), '') AS abha_id", false); + ->select("i.id AS bill_id, i.invoice_code AS bill_code, i.attach_id AS patient_id, COALESCE(NULLIF(i.inv_name, ''), p.p_fname, '') AS patient_name, i.inv_date AS bill_date, i.net_amount AS amount, COALESCE(p.p_code, '') AS p_code, COALESCE(p.abha_address, '') AS patient_abha_address, COALESCE(p.abha_id, '') AS patient_abha_number, COALESCE(NULLIF(p.abha_address, ''), NULLIF(p.abha_id, ''), '') AS abha_id", false); if ($this->db->tableExists('patient_master')) { $builder->join('patient_master p', 'p.id = i.attach_id AND i.attach_type = 0', 'left'); } @@ -911,7 +989,7 @@ class AbdmTaskBoard extends BaseController if ($this->db->tableExists('ipd_master')) { $builder = $this->db->table('ipd_master i') - ->select("i.id AS bill_id, i.ipd_code AS bill_code, i.p_id AS patient_id, COALESCE(NULLIF(NULLIF(TRIM(i.P_name), ''), '0'), NULLIF(TRIM(p.p_fname), ''), '') AS patient_name, COALESCE(i.discharge_date, i.register_date) AS bill_date, i.net_amount AS amount, COALESCE(NULLIF(p.abha_address, ''), NULLIF(p.abha_id, ''), '') AS abha_id", false); + ->select("i.id AS bill_id, i.ipd_code AS bill_code, i.p_id AS patient_id, COALESCE(NULLIF(NULLIF(TRIM(i.P_name), ''), '0'), NULLIF(TRIM(p.p_fname), ''), '') AS patient_name, COALESCE(i.discharge_date, i.register_date) AS bill_date, i.net_amount AS amount, COALESCE(p.p_code, '') AS p_code, COALESCE(p.abha_address, '') AS patient_abha_address, COALESCE(p.abha_id, '') AS patient_abha_number, COALESCE(NULLIF(p.abha_address, ''), NULLIF(p.abha_id, ''), '') AS abha_id", false); if ($this->db->tableExists('patient_master')) { $builder->join('patient_master p', 'p.id = i.p_id', 'left'); } @@ -969,6 +1047,27 @@ class AbdmTaskBoard extends BaseController } foreach ($rows as &$row) { + $rawAddress = trim((string) ($row['patient_abha_address'] ?? '')); + $rawNumber = trim((string) ($row['patient_abha_number'] ?? '')); + $addr = ''; + $num = ''; + if (str_contains($rawAddress, '@')) { + $addr = $rawAddress; + } elseif (str_contains($rawNumber, '@')) { + $addr = $rawNumber; + } + $d1 = preg_replace('/\D/', '', $rawNumber); + $d2 = preg_replace('/\D/', '', $rawAddress); + if (is_string($d1) && strlen($d1) === 14) { + $num = $d1; + } elseif (is_string($d2) && strlen($d2) === 14) { + $num = $d2; + } + $row['abha_address'] = $addr; + $row['abha_number'] = $num; + $row['abha_id'] = $addr !== '' ? $addr : $num; + $row['p_code'] = (string) ($row['p_code'] ?? ''); + $billId = (string) ($row['bill_id'] ?? ''); $keys = [(string) $row['source_key'] . ':' . $billId]; if ($row['source_key'] === 'charges_invoice') { @@ -1120,19 +1219,157 @@ class AbdmTaskBoard extends BaseController * * @return array> */ + /** + * Distinct patients with an ABHA address or ABHA number for the board selector & datalist. + * + * @return array> + */ + private function getAbhaPatientsList(): array + { + if (! $this->db->tableExists('patient_master')) { + return []; + } + + $fields = $this->db->getFieldNames('patient_master') ?? []; + $hasAbhaAddress = in_array('abha_address', $fields, true); + $hasAbhaId = in_array('abha_id', $fields, true); + $hasAbhaNo = in_array('abha_no', $fields, true); + + if (! $hasAbhaAddress && ! $hasAbhaId && ! $hasAbhaNo) { + return []; + } + + $select = ['id', 'p_code', 'p_fname', 'gender', 'dob']; + if (in_array('p_lname', $fields, true)) { + $select[] = 'p_lname'; + } + if (in_array('mphone1', $fields, true)) { + $select[] = 'mphone1'; + } + if ($hasAbhaAddress) { + $select[] = 'abha_address'; + } + if ($hasAbhaId) { + $select[] = 'abha_id'; + } + if ($hasAbhaNo) { + $select[] = 'abha_no'; + } + + $builder = $this->db->table('patient_master')->select(implode(', ', $select)); + + $whereOr = []; + if ($hasAbhaAddress) { + $whereOr[] = "NULLIF(TRIM(abha_address), '') IS NOT NULL"; + } + if ($hasAbhaId) { + $whereOr[] = "NULLIF(TRIM(abha_id), '') IS NOT NULL"; + } + if ($hasAbhaNo) { + $whereOr[] = "NULLIF(TRIM(abha_no), '') IS NOT NULL"; + } + + if ($whereOr !== []) { + $builder->where('(' . implode(' OR ', $whereOr) . ')', null, false); + } + + $rows = $builder->orderBy('id', 'DESC')->limit(500)->get()->getResultArray(); + $patients = []; + + foreach ($rows as $row) { + $rawAddress = trim((string) ($row['abha_address'] ?? '')); + $rawId = trim((string) ($row['abha_id'] ?? $row['abha_no'] ?? '')); + + $addr = ''; + $num = ''; + + if (str_contains($rawAddress, '@')) { + $addr = $rawAddress; + } elseif (str_contains($rawId, '@')) { + $addr = $rawId; + } + + $d1 = preg_replace('/\D/', '', $rawId); + $d2 = preg_replace('/\D/', '', $rawAddress); + if (is_string($d1) && strlen($d1) === 14) { + $num = $d1; + } elseif (is_string($d2) && strlen($d2) === 14) { + $num = $d2; + } + + if ($addr === '' && $num === '') { + continue; + } + + $fname = trim((string) ($row['p_fname'] ?? '')); + $lname = trim((string) ($row['p_lname'] ?? '')); + $fullName = trim($fname . ' ' . $lname); + if ($fullName === '') { + $fullName = 'Patient #' . ($row['id'] ?? ''); + } + + $genderInt = (int) ($row['gender'] ?? 0); + $genderStr = ($genderInt === 1 ? 'M' : ($genderInt === 2 ? 'F' : 'O')); + + $dob = trim((string) ($row['dob'] ?? '')); + $yob = 0; + if ($dob !== '' && ! str_starts_with($dob, '0000')) { + $ts = strtotime($dob); + if ($ts !== false) { + $yob = (int) date('Y', $ts); + } + } + + $patients[] = [ + 'id' => (int) ($row['id'] ?? 0), + 'p_code' => (string) ($row['p_code'] ?? ''), + 'name' => $fullName, + 'abha_address' => $addr, + 'abha_number' => $num, + 'phone' => (string) ($row['mphone1'] ?? ''), + 'gender' => $genderStr, + 'dob' => $dob, + 'yob' => $yob, + ]; + } + + return $patients; + } + private function getOpdBookRows(): array { if (! $this->db->tableExists('abdm_opd_tokens')) { return []; } - return $this->db->table('abdm_opd_tokens') - ->where('queue_date >=', date('Y-m-d', strtotime('-7 days'))) - ->orderBy('queue_date', 'DESC') - ->orderBy('gateway_token_id', 'DESC') + $builder = $this->db->table('abdm_opd_tokens t'); + if ($this->db->tableExists('patient_master')) { + $builder->select('t.*, p.p_code, p.mphone1 as patient_phone') + ->join('patient_master p', 'p.id = t.patient_id', 'left'); + } else { + $builder->select('t.*'); + } + + $rows = $builder->where('t.queue_date >=', date('Y-m-d', strtotime('-7 days'))) + ->orderBy('t.queue_date', 'DESC') + ->orderBy('t.gateway_token_id', 'DESC') ->limit(200) ->get() ->getResultArray(); + + foreach ($rows as &$r) { + $rawAddress = trim((string) ($r['abha_address'] ?? '')); + $rawNumber = trim((string) ($r['abha_number'] ?? '')); + if (str_contains($rawNumber, '@') && $rawAddress === '') { + $rawAddress = $rawNumber; + } + $r['abha_address'] = $rawAddress; + $r['abha_number'] = preg_replace('/\D/', '', $rawNumber); + $r['p_code'] = (string) ($r['p_code'] ?? ''); + } + unset($r); + + return $rows; } /** @@ -1147,23 +1384,77 @@ class AbdmTaskBoard extends BaseController } $patientFields = $this->db->getFieldNames('patient_master') ?? []; - $abhaCol = $this->resolveExistingColumn($patientFields, ['abha_id', 'abha_no', 'abha_address', 'abha']); - if ($abhaCol === null) { - return []; + $selectCols = [ + 'o.opd_id', 'o.p_id', 'o.P_name', 'o.apointment_date', 'o.opd_status', 'o.doc_name', + ]; + if (in_array('p_code', $patientFields, true)) { + $selectCols[] = 'p.p_code'; + } + if (in_array('abha_address', $patientFields, true)) { + $selectCols[] = 'p.abha_address'; + } + if (in_array('abha_id', $patientFields, true)) { + $selectCols[] = 'p.abha_id'; + } + if (in_array('abha_no', $patientFields, true)) { + $selectCols[] = 'p.abha_no'; + } + if (in_array('mphone1', $patientFields, true)) { + $selectCols[] = 'p.mphone1 as patient_phone'; } - $rows = $this->db->table('opd_master o') - ->select('o.opd_id, o.p_id, o.P_name, o.apointment_date, o.opd_status, o.doc_name, p.' . $abhaCol . ' as abha_id', false) + $builder = $this->db->table('opd_master o') + ->select(implode(', ', $selectCols), false) ->join('patient_master p', 'p.id = o.p_id', 'left') ->where('o.opd_status', 2) - ->where('DATE(o.apointment_date) >=', date('Y-m-d', strtotime('-30 days')), false) - ->where('p.' . $abhaCol . ' !=', '') - ->orderBy('o.opd_id', 'DESC') - ->limit(300) - ->get() - ->getResultArray(); + ->where('DATE(o.apointment_date) >=', date('Y-m-d', strtotime('-30 days')), false); + + $whereAbhaOr = []; + if (in_array('abha_address', $patientFields, true)) { + $whereAbhaOr[] = "NULLIF(TRIM(p.abha_address), '') IS NOT NULL"; + } + if (in_array('abha_id', $patientFields, true)) { + $whereAbhaOr[] = "NULLIF(TRIM(p.abha_id), '') IS NOT NULL"; + } + if (in_array('abha_no', $patientFields, true)) { + $whereAbhaOr[] = "NULLIF(TRIM(p.abha_no), '') IS NOT NULL"; + } + if ($whereAbhaOr !== []) { + $builder->where('(' . implode(' OR ', $whereAbhaOr) . ')', null, false); + } + + $rawRows = $builder->orderBy('o.opd_id', 'DESC')->limit(300)->get()->getResultArray(); + + $rows = []; + foreach ($rawRows as $r) { + $rawAddress = trim((string) ($r['abha_address'] ?? '')); + $rawId = trim((string) ($r['abha_id'] ?? $r['abha_no'] ?? '')); + $addr = ''; + $num = ''; + if (str_contains($rawAddress, '@')) { + $addr = $rawAddress; + } elseif (str_contains($rawId, '@')) { + $addr = $rawId; + } + $d1 = preg_replace('/\D/', '', $rawId); + $d2 = preg_replace('/\D/', '', $rawAddress); + if (is_string($d1) && strlen($d1) === 14) { + $num = $d1; + } elseif (is_string($d2) && strlen($d2) === 14) { + $num = $d2; + } + + if ($addr === '' && $num === '') { + continue; + } + + $r['abha_address'] = $addr; + $r['abha_number'] = $num; + $r['abha_id'] = $addr !== '' ? $addr : $num; + $r['p_code'] = (string) ($r['p_code'] ?? ''); + $rows[] = $r; + } - $rows = array_values(array_filter($rows, static fn ($r) => preg_match('/^\d{14}$/', trim((string) ($r['abha_id'] ?? ''))) === 1)); if (empty($rows)) { return []; } @@ -1398,7 +1689,7 @@ class AbdmTaskBoard extends BaseController $patientId = (int) ($row['p_id'] ?? 0); $docId = (int) ($row['id'] ?? 0); $abhaId = trim((string) ($row['abha_id'] ?? '')); - if ($patientId <= 0 || $docId <= 0 || preg_match('/^\d{14}$/', $abhaId) !== 1) { + if ($patientId <= 0 || $docId <= 0 || ! $this->isValidAbhaNumber($abhaId)) { continue; } @@ -1447,7 +1738,7 @@ class AbdmTaskBoard extends BaseController $patientId = (int) ($fRow['pid'] ?? 0); $fileId = (int) ($fRow['id'] ?? 0); $abhaId = trim((string) ($fRow['abha_id'] ?? '')); - if ($patientId <= 0 || $fileId <= 0 || preg_match('/^\d{14}$/', $abhaId) !== 1) { + if ($patientId <= 0 || $fileId <= 0 || ! $this->isValidAbhaNumber($abhaId)) { continue; } @@ -1524,7 +1815,7 @@ class AbdmTaskBoard extends BaseController $rxId = (int) ($row['id'] ?? 0); $opdId = (int) ($row['opd_id'] ?? 0); $abhaId = trim((string) ($row['abha_id'] ?? '')); - if ($patientId <= 0 || $rxId <= 0 || preg_match('/^\d{14}$/', $abhaId) !== 1) { + if ($patientId <= 0 || $rxId <= 0 || ! $this->isValidAbhaNumber($abhaId)) { continue; } diff --git a/app/Views/abdm/task_board.php b/app/Views/abdm/task_board.php index 4aebfab..50a6383 100644 --- a/app/Views/abdm/task_board.php +++ b/app/Views/abdm/task_board.php @@ -11,6 +11,10 @@ .container-wrap { width: 100%; margin: 0; padding: 20px 24px; box-sizing: border-box; } .table td, .table th { vertical-align: middle; } .status-pill { text-transform: uppercase; font-size: 11px; letter-spacing: .4px; } + .clickable-abha-filter { cursor: pointer; transition: all 0.15s ease-in-out; } + .clickable-abha-filter:hover { opacity: 0.85; transform: scale(1.02); filter: drop-shadow(0 1px 2px rgba(0,0,0,0.15)); } + .quick-abha-pill { transition: all 0.15s ease-in-out; } + .quick-abha-pill:hover { background-color: #0d6efd !important; color: #fff !important; } @@ -44,6 +48,176 @@ + +
+
+
+
+ + + +
+ ABHA Filter & FHIR Inspector + Filter board & inspect patient care contexts +
+
+
+
+ + + + +
+ + + + + + +
+ +
+ Quick: + + + +
+ +
+
+
+ Active ABHA Filter + Filtering all queues for: - + Scanning records... +
+
+ +
+
+
+
+ + +
+
+
+ + Patient Demographics & Context Status + M + YOB: - + UHID: - +
+
+ + + +
+
+
+ +
+
+
ABHA Address
+
-
+
+
+
ABHA Number
+
-
+
+
+
Phone Number
+
-
+
+
+
Gateway Link Verification
+
Unchecked
+
+
+ + +
+
+
+
Total Care Contexts
+
0
+
+
+
+
+
Linked Contexts
+
0
+
+
+
+
+
Unlinked Contexts
+
0
+
+
+
+
+
FHIR Ready Bundles
+
0
+
+
+
+ + +
+ + + + + + + + + + + + + + +
Care Context RefVisit / Record DescriptionHI TypeFHIR Bundle StatusContext Link StatusActions
Loading care contexts and FHIR status...
+
+
+
+
@@ -171,6 +345,11 @@ $bridgeCareContext, (string) ($t['last_action_result'] ?? ''), ]))); + $taskAbhaAddr = trim((string) ($t['patient_abha_address'] ?? '')); + $taskAbhaNum = trim((string) ($t['patient_abha_number'] ?? '')); + $taskPCode = trim((string) ($t['patient_p_code'] ?? '')); + $taskPhone = trim((string) ($t['patient_phone'] ?? '')); + $effectiveAbha = $taskAbhaAddr !== '' ? $taskAbhaAddr : ($taskAbhaNum !== '' ? $taskAbhaNum : $abhaId); ?> @@ -196,13 +379,27 @@
-
#
+
+ # + + () + +
-
+
+ + + +
- +
+ + + + +
@@ -393,10 +590,23 @@ - + + - + + + + + @@ -447,11 +657,31 @@ if ($statusTone === 'warning') { $statusBadgeClass .= ' text-dark'; } + $consultAbha = trim((string) (($r['abha_address'] ?? '') ?: ($r['abha_number'] ?? '') ?: ($r['abha_id'] ?? ''))); ?> - + # - - + +
+ +
+ + + + + + + @@ -530,22 +760,37 @@ $pushStatus = strtolower(trim((string) ($invoice['push_status'] ?? ''))); $linkStatus = strtolower(trim((string) ($invoice['link_status'] ?? ''))); $alreadySubmitted = in_array($pushStatus, ['queued', 'pushed', 'linked'], true) || $linkStatus === 'linked'; + $invAbha = trim((string) (($invoice['abha_address'] ?? '') ?: ($invoice['abha_number'] ?? '') ?: ($invoice['abha_id'] ?? ''))); + $invPCode = trim((string) ($invoice['p_code'] ?? '')); ?>
- # - -
+ + # + + () + + + +
+ + + +
@@ -729,6 +974,7 @@ var currentCategoryFilter = 'dashboard'; var currentStatusFilter = 'all'; var currentSearchQuery = ''; + var currentAbhaFilter = ''.trim(); function updateTaskTableDisplay() { var rows = document.querySelectorAll('#taskTable tbody tr:not(#taskTableEmptyRow)'); @@ -776,7 +1022,29 @@ // Check search match var searchMatch = !q || searchData.indexOf(q) !== -1; - if (statusMatch && searchMatch) { + // Check ABHA filter match + var abhaMatch = true; + if (currentAbhaFilter !== '') { + var patAbhaAddr = (row.getAttribute('data-patient-abha-address') || '').toLowerCase(); + var patAbhaNum = (row.getAttribute('data-patient-abha-number') || '').toLowerCase(); + var rowAbhaId = (row.getAttribute('data-abha-id') || '').toLowerCase(); + var patPCode = (row.getAttribute('data-patient-p-code') || '').toLowerCase(); + var patId = (row.getAttribute('data-patient-id') || '').toLowerCase(); + var patName = (row.getAttribute('data-patient-name') || '').toLowerCase(); + var sTerm = currentAbhaFilter.toLowerCase(); + var sDigits = sTerm.replace(/\D/g, ''); + + abhaMatch = (patAbhaAddr !== '' && patAbhaAddr.indexOf(sTerm) !== -1) + || (rowAbhaId !== '' && rowAbhaId.indexOf(sTerm) !== -1) + || (sDigits.length >= 6 && patAbhaNum !== '' && patAbhaNum.indexOf(sDigits) !== -1) + || (sDigits.length >= 6 && rowAbhaId.replace(/\D/g, '').indexOf(sDigits) !== -1) + || (patPCode !== '' && patPCode.indexOf(sTerm) !== -1) + || (patId !== '' && patId === sTerm) + || (patName !== '' && patName.indexOf(sTerm) !== -1) + || (searchData.indexOf(sTerm) !== -1); + } + + if (statusMatch && searchMatch && abhaMatch) { row.style.display = ''; visibleCount++; } else { @@ -799,6 +1067,294 @@ } } + function filterOpdConsultTable() { + var rows = document.querySelectorAll('#opdConsultCard tbody tr'); + var sTerm = currentAbhaFilter.toLowerCase(); + var sDigits = sTerm.replace(/\D/g, ''); + var matchCount = 0; + + rows.forEach(function (row) { + if (sTerm === '') { + row.style.display = ''; + matchCount++; + return; + } + var abhaAddr = (row.getAttribute('data-abha-address') || '').toLowerCase(); + var abhaNum = (row.getAttribute('data-abha-number') || '').toLowerCase(); + var abhaId = (row.getAttribute('data-abha-id') || '').toLowerCase(); + var pCode = (row.getAttribute('data-patient-p-code') || '').toLowerCase(); + var pName = (row.getAttribute('data-patient-name') || '').toLowerCase(); + var pId = (row.getAttribute('data-patient-id') || '').toLowerCase(); + var cContext = (row.getAttribute('data-care-context') || '').toLowerCase(); + + var matches = (abhaAddr !== '' && abhaAddr.indexOf(sTerm) !== -1) + || (abhaId !== '' && abhaId.indexOf(sTerm) !== -1) + || (sDigits.length >= 6 && abhaNum !== '' && abhaNum.indexOf(sDigits) !== -1) + || (sDigits.length >= 6 && abhaId.replace(/\D/g, '').indexOf(sDigits) !== -1) + || (pCode !== '' && pCode.indexOf(sTerm) !== -1) + || (pName !== '' && pName.indexOf(sTerm) !== -1) + || (pId !== '' && pId === sTerm) + || (cContext !== '' && cContext.indexOf(sTerm) !== -1) + || (row.textContent || '').toLowerCase().indexOf(sTerm) !== -1; + + if (matches) { + row.style.display = ''; + matchCount++; + } else { + row.style.display = 'none'; + } + }); + + return matchCount; + } + + function filterInvoiceTable() { + var rows = document.querySelectorAll('#invoiceCard tbody tr'); + var sTerm = currentAbhaFilter.toLowerCase(); + var sDigits = sTerm.replace(/\D/g, ''); + var matchCount = 0; + + rows.forEach(function (row) { + if (sTerm === '') { + row.style.display = ''; + matchCount++; + return; + } + var abhaAddr = (row.getAttribute('data-abha-address') || '').toLowerCase(); + var abhaNum = (row.getAttribute('data-abha-number') || '').toLowerCase(); + var abhaId = (row.getAttribute('data-abha-id') || '').toLowerCase(); + var pCode = (row.getAttribute('data-patient-p-code') || '').toLowerCase(); + var pName = (row.getAttribute('data-patient-name') || '').toLowerCase(); + var pId = (row.getAttribute('data-patient-id') || '').toLowerCase(); + var cContext = (row.getAttribute('data-care-context') || '').toLowerCase(); + + var matches = (abhaAddr !== '' && abhaAddr.indexOf(sTerm) !== -1) + || (abhaId !== '' && abhaId.indexOf(sTerm) !== -1) + || (sDigits.length >= 6 && abhaNum !== '' && abhaNum.indexOf(sDigits) !== -1) + || (sDigits.length >= 6 && abhaId.replace(/\D/g, '').indexOf(sDigits) !== -1) + || (pCode !== '' && pCode.indexOf(sTerm) !== -1) + || (pName !== '' && pName.indexOf(sTerm) !== -1) + || (pId !== '' && pId === sTerm) + || (cContext !== '' && cContext.indexOf(sTerm) !== -1) + || (row.textContent || '').toLowerCase().indexOf(sTerm) !== -1; + + if (matches) { + row.style.display = ''; + matchCount++; + } else { + row.style.display = 'none'; + } + }); + + return matchCount; + } + + function filterOpdBookTable() { + var rows = document.querySelectorAll('#opdBookCard tbody tr'); + var sTerm = currentAbhaFilter.toLowerCase(); + var sDigits = sTerm.replace(/\D/g, ''); + var matchCount = 0; + + rows.forEach(function (row) { + if (sTerm === '') { + row.style.display = ''; + matchCount++; + return; + } + var abhaAddr = (row.getAttribute('data-abha-address') || '').toLowerCase(); + var abhaNum = (row.getAttribute('data-abha-number') || '').toLowerCase(); + var pCode = (row.getAttribute('data-patient-p-code') || '').toLowerCase(); + var pName = (row.getAttribute('data-patient-name') || '').toLowerCase(); + var pId = (row.getAttribute('data-patient-id') || '').toLowerCase(); + + var matches = (abhaAddr !== '' && abhaAddr.indexOf(sTerm) !== -1) + || (sDigits.length >= 6 && abhaNum !== '' && abhaNum.indexOf(sDigits) !== -1) + || (pCode !== '' && pCode.indexOf(sTerm) !== -1) + || (pName !== '' && pName.indexOf(sTerm) !== -1) + || (pId !== '' && pId === sTerm) + || (row.textContent || '').toLowerCase().indexOf(sTerm) !== -1; + + if (matches) { + row.style.display = ''; + matchCount++; + } else { + row.style.display = 'none'; + } + }); + + return matchCount; + } + + function updateAllTablesForAbhaFilter() { + updateTaskTableDisplay(); + var consultMatches = filterOpdConsultTable(); + var invoiceMatches = filterInvoiceTable(); + var opdBookMatches = filterOpdBookTable(); + + var qRows = document.querySelectorAll('#taskTable tbody tr:not(#taskTableEmptyRow)'); + var qVisible = 0; + qRows.forEach(function (r) { if (r.style.display !== 'none') qVisible++; }); + + var total = qVisible + consultMatches + invoiceMatches + opdBookMatches; + var statsEl = document.getElementById('activeAbhaMatchStats'); + if (statsEl) { + statsEl.textContent = total + ' matches found (' + qVisible + ' queue, ' + consultMatches + ' consult, ' + invoiceMatches + ' invoice, ' + opdBookMatches + ' tokens)'; + } + } + + function applyGlobalAbhaFilter(val) { + val = (val || '').trim(); + currentAbhaFilter = val; + var inputEl = document.getElementById('globalAbhaFilterInput'); + if (inputEl) { + inputEl.value = val; + } + + var noticeEl = document.getElementById('activeAbhaFilterNotice'); + var textEl = document.getElementById('activeAbhaFilterText'); + var inspectorCard = document.getElementById('patientAbhaInspectorCard'); + + if (val !== '') { + if (noticeEl) noticeEl.classList.remove('d-none'); + if (textEl) textEl.textContent = val; + if (currentCategoryFilter === 'dashboard') { + applyFilter('all'); + } + loadPatientAbhaInspector(val); + } else { + if (noticeEl) noticeEl.classList.add('d-none'); + if (inspectorCard) inspectorCard.classList.add('d-none'); + } + + updateAllTablesForAbhaFilter(); + } + + function clearGlobalAbhaFilter() { + applyGlobalAbhaFilter(''); + } + + function loadPatientAbhaInspector(abhaAddress) { + var card = document.getElementById('patientAbhaInspectorCard'); + var tbody = document.getElementById('inspCareContextTbody'); + if (!card || !tbody) { + return; + } + + card.classList.remove('d-none'); + tbody.innerHTML = '
Loading patient care contexts & FHIR readiness...'; + + var url = '?abha_address=' + encodeURIComponent(abhaAddress); + + fetch(url, { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + } + }).then(function (r) { + return r.json(); + }).then(function (res) { + if (!res || res.ok !== 1 || !res.patient) { + tbody.innerHTML = 'No patient demographics or active care contexts found for ' + hesc(abhaAddress) + '. You can initiate linking via the HIP Link button.'; + document.getElementById('inspPatientName').textContent = 'Patient Not Linked Yet'; + document.getElementById('inspAbhaAddress').textContent = abhaAddress; + document.getElementById('inspAbhaNumber').textContent = '-'; + document.getElementById('inspPhone').textContent = '-'; + document.getElementById('inspGatewayStatus').innerHTML = 'Unregistered'; + document.getElementById('inspTotalContexts').textContent = '0'; + document.getElementById('inspLinkedContexts').textContent = '0'; + document.getElementById('inspUnlinkedContexts').textContent = '0'; + document.getElementById('inspFhirCount').textContent = '0'; + return; + } + + var pat = res.patient; + var contexts = res.care_contexts || []; + var linkedRefs = res.linked_refs || []; + + document.getElementById('inspPatientName').textContent = pat.name || 'Patient'; + document.getElementById('inspPatientGender').textContent = pat.gender || 'O'; + document.getElementById('inspPatientYob').textContent = 'YOB: ' + (pat.year_of_birth || '-'); + document.getElementById('inspPatientUhid').textContent = 'UHID: ' + (pat.patient_ref || ('P-' + pat.id)); + document.getElementById('inspAbhaAddress').textContent = pat.abha_address || abhaAddress; + document.getElementById('inspAbhaNumber').textContent = pat.abha_number || '-'; + document.getElementById('inspPhone').textContent = pat.phone || '-'; + + var linkedCount = 0; + var fhirCount = 0; + contexts.forEach(function (c) { + if (c.is_linked) linkedCount++; + if (c.is_fhir_ready) fhirCount++; + }); + + document.getElementById('inspTotalContexts').textContent = contexts.length; + document.getElementById('inspLinkedContexts').textContent = linkedCount; + document.getElementById('inspUnlinkedContexts').textContent = contexts.length - linkedCount; + document.getElementById('inspFhirCount').textContent = fhirCount; + + if (linkedRefs.length > 0) { + document.getElementById('inspGatewayStatus').innerHTML = 'Gateway Verified (' + linkedRefs.length + ' links)'; + } else if (linkedCount > 0) { + document.getElementById('inspGatewayStatus').innerHTML = 'Local Linked'; + } else { + document.getElementById('inspGatewayStatus').innerHTML = 'Unlinked'; + } + + if (contexts.length === 0) { + tbody.innerHTML = 'No care contexts registered for this patient yet.'; + return; + } + + var html = ''; + contexts.forEach(function (c) { + var ref = c.ref || ''; + var isLinked = !!c.is_linked; + var isFhir = !!c.is_fhir_ready; + var hiType = c.hi_type || 'OPConsultRecord'; + + var linkBadge = isLinked + ? 'LINKED' + : 'UNLINKED'; + + var fhirBadge = isFhir + ? 'Ready' + : 'Pending'; + + // Resolve preview button + var fhirPreviewBtn = ''; + var opdMatch = ref.match(/^OPD-(\d+)/i); + if (opdMatch) { + var opdId = opdMatch[1]; + var sMatch = ref.match(/-S(\d+)-/i); + var sId = sMatch ? sMatch[1] : 0; + var previewUrl = sId > 0 + ? '/' + opdId + '/' + sId + : '/' + opdId; + fhirPreviewBtn = ''; + } + + var actionBtn = ''; + if (isLinked) { + actionBtn = 'Active'; + } else { + actionBtn = ''; + } + + html += ''; + html += '' + hesc(ref) + ''; + html += '' + hesc(c.display || ref) + ''; + html += '' + hesc(hiType) + ''; + html += '' + fhirBadge + ' ' + fhirPreviewBtn + ''; + html += '' + linkBadge + ''; + html += '' + actionBtn + ''; + html += ''; + }); + + tbody.innerHTML = html; + }).catch(function (err) { + tbody.innerHTML = 'Failed to load contexts: ' + hesc(err.message || 'Network error') + ''; + }); + } + function applyFilter(filter) { currentCategoryFilter = filter; @@ -814,12 +1370,15 @@ } else if (filter === 'opd_book') { if (taskTableCard) taskTableCard.classList.add('d-none'); if (opdBookCard) opdBookCard.classList.remove('d-none'); + filterOpdBookTable(); } else if (filter === 'opd_consult_publish') { if (taskTableCard) taskTableCard.classList.add('d-none'); if (opdConsultCard) opdConsultCard.classList.remove('d-none'); + filterOpdConsultTable(); } else if (filter === 'invoice') { if (taskTableCard) taskTableCard.classList.add('d-none'); if (invoiceCard) invoiceCard.classList.remove('d-none'); + filterInvoiceTable(); } else { if (taskTableCard) taskTableCard.classList.remove('d-none'); var titleEl = document.getElementById('taskTableTitle'); @@ -1468,7 +2027,165 @@ }); }); - applyFilter('dashboard'); + // Wire ABHA Filter Bar Buttons + var btnApplyAbha = document.getElementById('btnApplyAbhaFilter'); + if (btnApplyAbha) { + btnApplyAbha.addEventListener('click', function () { + var input = document.getElementById('globalAbhaFilterInput'); + applyGlobalAbhaFilter(input ? input.value : ''); + }); + } + + var inputAbhaFilter = document.getElementById('globalAbhaFilterInput'); + if (inputAbhaFilter) { + inputAbhaFilter.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + applyGlobalAbhaFilter(inputAbhaFilter.value); + } + }); + } + + var btnClearAbha = document.getElementById('btnClearAbhaFilter'); + if (btnClearAbha) { + btnClearAbha.addEventListener('click', function () { + clearGlobalAbhaFilter(); + }); + } + + var btnNoticeClear = document.getElementById('btnNoticeClearFilter'); + if (btnNoticeClear) { + btnNoticeClear.addEventListener('click', function () { + clearGlobalAbhaFilter(); + }); + } + + document.querySelectorAll('.quick-abha-pill').forEach(function (pill) { + pill.addEventListener('click', function () { + var abhaVal = pill.getAttribute('data-abha') || ''; + applyGlobalAbhaFilter(abhaVal); + }); + }); + + // Wire Inspector Actions + var btnCloseInsp = document.getElementById('btnCloseInspector'); + if (btnCloseInsp) { + btnCloseInsp.addEventListener('click', function () { + var card = document.getElementById('patientAbhaInspectorCard'); + if (card) card.classList.add('d-none'); + }); + } + + var btnRefreshInsp = document.getElementById('btnRefreshInspector'); + if (btnRefreshInsp) { + btnRefreshInsp.addEventListener('click', function () { + if (currentAbhaFilter !== '') { + loadPatientAbhaInspector(currentAbhaFilter); + } + }); + } + + var btnVerifyGateway = document.getElementById('btnVerifyAbhaGateway'); + if (btnVerifyGateway) { + btnVerifyGateway.addEventListener('click', function () { + if (!currentAbhaFilter) { + setStatus('Please specify an ABHA address to query the ABDM Gateway.', true); + return; + } + var statusEl = document.getElementById('inspGatewayStatus'); + if (statusEl) { + statusEl.innerHTML = 'Verifying on Gateway...'; + } + var gwUrl = '?abha_address=' + encodeURIComponent(currentAbhaFilter); + fetch(gwUrl, { + headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } + }).then(function (r) { return r.json(); }) + .then(function (res) { + if (res && res.ok === 1 && res.links && res.links.length > 0) { + if (statusEl) { + statusEl.innerHTML = 'Gateway Verified (' + res.links.length + ' links)'; + } + setStatus('ABDM Gateway verified ' + res.links.length + ' linked care context(s) for patient.'); + } else if (res && res.ok === 1) { + if (statusEl) { + statusEl.innerHTML = 'No Gateway Links Found'; + } + setStatus('No active links recorded on Gateway for this ABHA address yet.'); + } else { + if (statusEl) { + statusEl.innerHTML = 'Check Failed'; + } + setStatus('Gateway check returned error: ' + (res ? (res.error_text || res.message || 'Unknown') : 'Network error'), true); + } + }).catch(function (err) { + if (statusEl) { + statusEl.innerHTML = 'Error'; + } + setStatus('Gateway verification failed: ' + err.message, true); + }); + }); + } + + // Delegated click handler for inspector actions and table filter triggers + document.addEventListener('click', function (e) { + // Clickable ABHA badge on any table + var abhaFilterTarget = e.target.closest('.clickable-abha-filter'); + if (abhaFilterTarget) { + e.preventDefault(); + var abhaVal = (abhaFilterTarget.getAttribute('data-patient-abha-address') + || abhaFilterTarget.getAttribute('data-patient-abha-number') + || abhaFilterTarget.getAttribute('data-abha-id') + || abhaFilterTarget.textContent || '').trim(); + if (abhaVal) { + applyGlobalAbhaFilter(abhaVal); + } + return; + } + + // Preview FHIR from within inspector table + var previewFhirTarget = e.target.closest('.insp-preview-fhir'); + if (previewFhirTarget) { + e.preventDefault(); + var previewUrl = previewFhirTarget.getAttribute('data-url') || ''; + var previewTitle = previewFhirTarget.getAttribute('data-title') || 'FHIR Preview'; + if (previewUrl) { + showInlineFhirPreview(previewUrl, previewTitle); + } + return; + } + + // Link context from within inspector table + var linkTarget = e.target.closest('.btn-insp-link'); + if (linkTarget) { + e.preventDefault(); + var patId = parseInt(linkTarget.getAttribute('data-patient-id') || '0', 10); + var patName = linkTarget.getAttribute('data-patient-name') || ''; + var abhaAddr = linkTarget.getAttribute('data-abha') || currentAbhaFilter; + var cRef = linkTarget.getAttribute('data-ref') || ''; + var cType = linkTarget.getAttribute('data-type') || 'OPConsultRecord'; + + if (typeof window.openAbdmHipLinkModal === 'function') { + window.openAbdmHipLinkModal(patId, abhaAddr, { + patient_name: patName, + care_context_reference: cRef, + hi_type: cType, + onLinked: function () { + if (currentAbhaFilter !== '') { + loadPatientAbhaInspector(currentAbhaFilter); + } + updateAllTablesForAbhaFilter(); + } + }); + } + return; + } + }); + + if (currentAbhaFilter !== '') { + applyGlobalAbhaFilter(currentAbhaFilter); + } else { + applyFilter('dashboard'); + } modalConfirmBtn.addEventListener('click', function () { if (!selectedRow || !selectedAction) { diff --git a/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php b/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php index 5f0081a..2d4d469 100644 --- a/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php +++ b/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php @@ -130,4 +130,128 @@ final class AbdmTaskBoardBackfillTest extends CIUnitTestCase $this->assertSame('status', $params[0]->getName()); $this->assertSame('all', $params[0]->getDefaultValue()); } + + public function testGetAbhaPatientsListReturnsNormalizedArray(): void + { + $db = \Config\Database::connect(); + $db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('patient_master') . " ( + id INTEGER PRIMARY KEY, + p_code TEXT, + p_fname TEXT, + p_lname TEXT, + dob TEXT, + age TEXT, + gender INTEGER, + abha_id TEXT, + abha_address TEXT, + abha_no TEXT, + mphone1 TEXT + )"); + + $testPatientId = 998876; + $db->table('patient_master')->where('id', $testPatientId)->delete(); + $db->table('patient_master')->insert([ + 'id' => $testPatientId, + 'p_code' => 'P-TEST-998876', + 'p_fname' => 'Janvi', + 'p_lname' => 'Bisht', + 'mphone1' => '9876543212', + 'gender' => 2, + 'abha_address' => 'janvibisht2506@sbx', + 'abha_id' => '91747451787144', + ]); + + try { + $controller = (new ReflectionClass(AbdmTaskBoard::class))->newInstanceWithoutConstructor(); + $dbProp = new ReflectionProperty(AbdmTaskBoard::class, 'db'); + $dbProp->setAccessible(true); + $dbProp->setValue($controller, $db); + + $method = new ReflectionMethod(AbdmTaskBoard::class, 'getAbhaPatientsList'); + $method->setAccessible(true); + + $patients = $method->invoke($controller); + $this->assertIsArray($patients); + $this->assertNotEmpty($patients); + + $found = false; + foreach ($patients as $p) { + if ($p['id'] === $testPatientId) { + $this->assertSame('janvibisht2506@sbx', $p['abha_address']); + $this->assertSame('91747451787144', $p['abha_number']); + $this->assertSame('P-TEST-998876', $p['p_code']); + $this->assertSame('Janvi Bisht', $p['name']); + $this->assertSame('F', $p['gender']); + $found = true; + break; + } + } + $this->assertTrue($found, 'Inserted test patient was not found in getAbhaPatientsList'); + } finally { + $db->table('patient_master')->where('id', $testPatientId)->delete(); + } + } + + public function testEnrichTasksWithHealthRecordStateAttachesPatientAbha(): void + { + $db = \Config\Database::connect(); + $db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('patient_master') . " ( + id INTEGER PRIMARY KEY, + p_code TEXT, + p_fname TEXT, + p_lname TEXT, + dob TEXT, + age TEXT, + gender INTEGER, + abha_id TEXT, + abha_address TEXT, + abha_no TEXT, + mphone1 TEXT + )"); + + $testPatientId = 998877; + $db->table('patient_master')->where('id', $testPatientId)->delete(); + $db->table('patient_master')->insert([ + 'id' => $testPatientId, + 'p_code' => 'P-TEST-998877', + 'p_fname' => 'Test', + 'p_lname' => 'AbhaPatient', + 'mphone1' => '9876543210', + 'gender' => 1, + 'abha_address' => 'testpatient998877@sbx', + 'abha_id' => '91747451787143', + ]); + + try { + $controller = (new ReflectionClass(AbdmTaskBoard::class))->newInstanceWithoutConstructor(); + $dbProp = new ReflectionProperty(AbdmTaskBoard::class, 'db'); + $dbProp->setAccessible(true); + $dbProp->setValue($controller, $db); + + $method = new ReflectionMethod(AbdmTaskBoard::class, 'enrichTasksWithHealthRecordState'); + $method->setAccessible(true); + + $mockTasks = [ + [ + 'id' => 1, + 'task_code' => 'ABDM-TEST-ENRICH-1', + 'task_type' => 'opd_prescription_publish', + 'patient_id' => $testPatientId, + 'entity_id' => '100', + 'status' => 'pending', + 'abha_id' => '', + ] + ]; + + $enriched = $method->invoke($controller, $mockTasks); + $this->assertCount(1, $enriched); + $this->assertSame('testpatient998877@sbx', $enriched[0]['patient_abha_address']); + $this->assertSame('91747451787143', $enriched[0]['patient_abha_number']); + $this->assertSame('P-TEST-998877', $enriched[0]['patient_p_code']); + $this->assertSame('9876543210', $enriched[0]['patient_phone']); + } finally { + $db->table('patient_master')->where('id', $testPatientId)->delete(); + } + } } + diff --git a/tests/unit/Abdm/TestHipDemographicsCareContext.php b/tests/unit/Abdm/TestHipDemographicsCareContext.php index 4dea317..79936b8 100644 --- a/tests/unit/Abdm/TestHipDemographicsCareContext.php +++ b/tests/unit/Abdm/TestHipDemographicsCareContext.php @@ -73,7 +73,9 @@ final class TestHipDemographicsCareContext extends CIUnitTestCase ]); $this->gateway = new AbdmGateway(); - $req = \Config\Services::request(); + \Config\Services::resetSingle('request'); + \Config\Services::resetSingle('response'); + $req = \Config\Services::request(null, false); $resp = \Config\Services::response(); $logger = \Config\Services::logger(); $this->gateway->initController($req, $resp, $logger);