diff --git a/AGENTS.md b/AGENTS.md index 73ef934..54c3ddb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,6 +70,8 @@ use Firebase\JWT\JWT; ``` ### Controller Structure +Controllers handle HTTP requests and delegate business logic to Models. They should NOT contain database queries. + ```php db = \Config\Database::connect(); $this->model = new \App\Models\ExampleModel(); } - public function index() { /* ... */ } - public function create() { /* ... */ } + public function index() + { + $data = $this->model->findAll(); + return $this->respond(['status' => 'success', 'data' => $data], 200); + } + + public function create() + { + $data = $this->request->getJSON(true); + $result = $this->model->createWithRelations($data); + return $this->respond(['status' => 'success', 'data' => $result], 201); + } } ``` diff --git a/app/Controllers/OrderTestController.php b/app/Controllers/OrderTestController.php index 1511aa0..f64d511 100644 --- a/app/Controllers/OrderTestController.php +++ b/app/Controllers/OrderTestController.php @@ -4,9 +4,9 @@ namespace App\Controllers; use App\Traits\ResponseTrait; use CodeIgniter\Controller; use App\Libraries\ValueSet; -use App\Models\OrderTestModel; +use App\Models\OrderTest\OrderTestModel; use App\Models\Patient\PatientModel; -use App\Models\Patient\PatVisitModel; +use App\Models\PatVisit\PatVisitModel; class OrderTestController extends Controller { use ResponseTrait; @@ -29,6 +29,7 @@ class OrderTestController extends Controller { public function index() { $internalPID = $this->request->getVar('InternalPID'); + $includeDetails = $this->request->getVar('include') === 'details'; try { if ($internalPID) { @@ -46,6 +47,13 @@ class OrderTestController extends Controller { 'OrderStatus' => 'order_status', ]); + if ($includeDetails && !empty($rows)) { + foreach ($rows as &$row) { + $row['Specimens'] = $this->getOrderSpecimens($row['InternalOID']); + $row['Tests'] = $this->getOrderTests($row['InternalOID']); + } + } + return $this->respond([ 'status' => 'success', 'message' => 'Data fetched successfully', @@ -72,6 +80,10 @@ class OrderTestController extends Controller { 'OrderStatus' => 'order_status', ])[0]; + // Include specimens and tests + $row['Specimens'] = $this->getOrderSpecimens($row['InternalOID']); + $row['Tests'] = $this->getOrderTests($row['InternalOID']); + return $this->respond([ 'status' => 'success', 'message' => 'Data fetched successfully', @@ -82,6 +94,39 @@ class OrderTestController extends Controller { } } + private function getOrderSpecimens($internalOID) { + $specimens = $this->db->table('specimen s') + ->select('s.*, cd.ConCode, cd.ConName') + ->join('containerdef cd', 'cd.ConDefID = s.ConDefID', 'left') + ->where('s.OrderID', $internalOID) + ->where('s.EndDate IS NULL') + ->get() + ->getResultArray(); + + // Get status for each specimen + foreach ($specimens as &$specimen) { + $status = $this->db->table('specimenstatus') + ->where('SID', $specimen['SID']) + ->where('EndDate IS NULL') + ->orderBy('CreateDate', 'DESC') + ->get() + ->getRowArray(); + $specimen['Status'] = $status['SpcStatus'] ?? 'PENDING'; + } + + return $specimens; + } + + private function getOrderTests($internalOID) { + return $this->db->table('patres pr') + ->select('pr.*, tds.TestSiteCode, tds.TestSiteName') + ->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left') + ->where('pr.OrderID', $internalOID) + ->where('pr.DelDate IS NULL') + ->get() + ->getResultArray(); + } + public function create() { $input = $this->request->getJSON(true); @@ -102,11 +147,16 @@ class OrderTestController extends Controller { } $orderID = $this->model->createOrder($input); + + // Fetch complete order details + $order = $this->model->getOrder($orderID); + $order['Specimens'] = $this->getOrderSpecimens($order['InternalOID']); + $order['Tests'] = $this->getOrderTests($order['InternalOID']); return $this->respondCreated([ 'status' => 'success', 'message' => 'Order created successfully', - 'data' => ['OrderID' => $orderID] + 'data' => $order ], 201); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); @@ -137,10 +187,14 @@ class OrderTestController extends Controller { $this->model->update($order['InternalOID'], $updateData); } + $updatedOrder = $this->model->getOrder($input['OrderID']); + $updatedOrder['Specimens'] = $this->getOrderSpecimens($updatedOrder['InternalOID']); + $updatedOrder['Tests'] = $this->getOrderTests($updatedOrder['InternalOID']); + return $this->respond([ 'status' => 'success', 'message' => 'Order updated successfully', - 'data' => $this->model->getOrder($input['OrderID']) + 'data' => $updatedOrder ], 200); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); @@ -192,10 +246,14 @@ class OrderTestController extends Controller { $this->model->updateStatus($input['OrderID'], $input['OrderStatus']); + $updatedOrder = $this->model->getOrder($input['OrderID']); + $updatedOrder['Specimens'] = $this->getOrderSpecimens($updatedOrder['InternalOID']); + $updatedOrder['Tests'] = $this->getOrderTests($updatedOrder['InternalOID']); + return $this->respond([ 'status' => 'success', 'message' => 'Order status updated successfully', - 'data' => $this->model->getOrder($input['OrderID']) + 'data' => $updatedOrder ], 200); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); diff --git a/app/Controllers/PatVisitController.php b/app/Controllers/PatVisitController.php index 35db554..817293e 100644 --- a/app/Controllers/PatVisitController.php +++ b/app/Controllers/PatVisitController.php @@ -20,15 +20,43 @@ class PatVisitController extends BaseController { try { $InternalPID = $this->request->getVar('InternalPID'); $PVID = $this->request->getVar('PVID'); + $PatientID = $this->request->getVar('PatientID'); + $PatientName = $this->request->getVar('PatientName'); + $CreateDateFrom = $this->request->getVar('CreateDateFrom'); + $CreateDateTo = $this->request->getVar('CreateDateTo'); - $builder = $this->model->select('patvisit.*, patient.NameFirst, patient.NameLast, patient.PatientID') - ->join('patient', 'patient.InternalPID=patvisit.InternalPID', 'left'); + $builder = $this->model->select('patvisit.*, patient.NameFirst, patient.NameLast, patient.PatientID, location.LocFull as LastLocation') + ->join('patient', 'patient.InternalPID=patvisit.InternalPID', 'left') + ->join('(SELECT a1.* + FROM patvisitadt a1 + INNER JOIN ( + SELECT InternalPVID, MAX(PVADTID) AS MaxID + FROM patvisitadt + GROUP BY InternalPVID + ) a2 ON a1.InternalPVID = a2.InternalPVID AND a1.PVADTID = a2.MaxID + ) AS latest_patvisitadt', 'latest_patvisitadt.InternalPVID = patvisit.InternalPVID', 'left') + ->join('location', 'location.LocationID = latest_patvisitadt.LocationID', 'left'); if ($InternalPID) { $builder->where('patvisit.InternalPID', $InternalPID); } if ($PVID) { - $builder->where('patvisit.PVID', $PVID); + $builder->like('patvisit.PVID', $PVID, 'both'); + } + if ($PatientID) { + $builder->like('patient.PatientID', $PatientID, 'both'); + } + if ($PatientName) { + $builder->groupStart() + ->like('patient.NameFirst', $PatientName, 'both') + ->orLike('patient.NameLast', $PatientName, 'both') + ->groupEnd(); + } + if ($CreateDateFrom) { + $builder->where('patvisit.CreateDate >=', $CreateDateFrom); + } + if ($CreateDateTo) { + $builder->where('patvisit.CreateDate <=', $CreateDateTo); } $rows = $builder->orderBy('patvisit.CreateDate', 'DESC')->findAll(); diff --git a/app/Controllers/Test/TestsController.php b/app/Controllers/Test/TestsController.php index b44f457..0a34c1b 100644 --- a/app/Controllers/Test/TestsController.php +++ b/app/Controllers/Test/TestsController.php @@ -364,7 +364,18 @@ class TestsController extends BaseController $this->modelRefTxt->where('TestSiteID', $id)->set('EndDate', $now)->update(); } - $this->modelMap->disableByTestSiteID($id); + // Disable testmap by test code + $testSiteCode = $existing['TestSiteCode'] ?? null; + if ($testSiteCode) { + $existingMaps = $this->modelMap->getMappingsByTestCode($testSiteCode); + foreach ($existingMaps as $existingMap) { + $this->modelMapDetail->where('TestMapID', $existingMap['TestMapID']) + ->where('EndDate', null) + ->set('EndDate', $now) + ->update(); + $this->modelMap->update($existingMap['TestMapID'], ['EndDate' => $now]); + } + } $this->db->transComplete(); @@ -387,10 +398,12 @@ class TestsController extends BaseController private function handleDetails($testSiteID, $input, $action) { $testTypeID = $input['TestType'] ?? null; + $testSiteCode = null; if (!$testTypeID && $action === 'update') { $existing = $this->model->find($testSiteID); $testTypeID = $existing['TestType'] ?? null; + $testSiteCode = $existing['TestSiteCode'] ?? null; } if (!$testTypeID) { @@ -416,7 +429,7 @@ class TestsController extends BaseController case 'TITLE': if (isset($input['testmap']) && is_array($input['testmap'])) { - $this->saveTestMap($testSiteID, $input['testmap'], $action); + $this->saveTestMap($testSiteID, $testSiteCode, $input['testmap'], $action); } break; @@ -443,7 +456,7 @@ class TestsController extends BaseController } if ((TestValidationService::isTechnicalTest($typeCode) || TestValidationService::isCalc($typeCode)) && isset($input['testmap']) && is_array($input['testmap'])) { - $this->saveTestMap($testSiteID, $input['testmap'], $action); + $this->saveTestMap($testSiteID, $testSiteCode, $input['testmap'], $action); } } @@ -572,12 +585,11 @@ class TestsController extends BaseController } } - private function saveTestMap($testSiteID, $mappings, $action) + private function saveTestMap($testSiteID, $testSiteCode, $mappings, $action) { - if ($action === 'update') { - $existingMaps = $this->modelMap->where('TestSiteID', $testSiteID) - ->where('EndDate', null) - ->findAll(); + if ($action === 'update' && $testSiteCode) { + // Find existing mappings by test code through testmapdetail + $existingMaps = $this->modelMap->getMappingsByTestCode($testSiteCode); foreach ($existingMaps as $existingMap) { $this->modelMapDetail->where('TestMapID', $existingMap['TestMapID']) @@ -586,13 +598,15 @@ class TestsController extends BaseController ->update(); } - $this->modelMap->disableByTestSiteID($testSiteID); + // Soft delete the testmap headers + foreach ($existingMaps as $existingMap) { + $this->modelMap->update($existingMap['TestMapID'], ['EndDate' => date('Y-m-d H:i:s')]); + } } if (is_array($mappings)) { foreach ($mappings as $map) { $mapData = [ - 'TestSiteID' => $testSiteID, 'HostType' => $map['HostType'] ?? null, 'HostID' => $map['HostID'] ?? null, 'ClientType' => $map['ClientType'] ?? null, diff --git a/app/Database/Migrations/2026-03-03-043304_AddRequestableToTestDefSite.php b/app/Database/Migrations/2026-03-03-043304_AddRequestableToTestDefSite.php new file mode 100644 index 0000000..ad0e567 --- /dev/null +++ b/app/Database/Migrations/2026-03-03-043304_AddRequestableToTestDefSite.php @@ -0,0 +1,28 @@ + [ + 'type' => 'TINYINT', + 'constraint' => 1, + 'null' => true, + 'default' => 1, + 'comment' => 'Flag indicating if test can be requested (1=yes, 0=no)' + ] + ]; + + $this->forge->addColumn('testdefsite', $fields); + } + + public function down() + { + $this->forge->dropColumn('testdefsite', 'Requestable'); + } +} diff --git a/app/Database/Seeds/ClearOrderDataSeeder.php b/app/Database/Seeds/ClearOrderDataSeeder.php new file mode 100644 index 0000000..a1ea963 --- /dev/null +++ b/app/Database/Seeds/ClearOrderDataSeeder.php @@ -0,0 +1,40 @@ +db->query('SET FOREIGN_KEY_CHECKS = 0'); + + // Clear tables in reverse dependency order + $this->db->table('ordercom')->truncate(); + echo " - ordercom truncated\n"; + + $this->db->table('orderstatus')->truncate(); + echo " - orderstatus truncated\n"; + + $this->db->table('patres')->truncate(); + echo " - patres truncated\n"; + + $this->db->table('specimenstatus')->truncate(); + echo " - specimenstatus truncated\n"; + + $this->db->table('specimen')->truncate(); + echo " - specimen truncated\n"; + + $this->db->table('ordertest')->truncate(); + echo " - ordertest truncated\n"; + + // Re-enable foreign key checks + $this->db->query('SET FOREIGN_KEY_CHECKS = 1'); + + echo "\nAll order-related tables cleared successfully!\n"; + } +} diff --git a/app/Database/Seeds/DBSeeder.php b/app/Database/Seeds/DBSeeder.php index f4c374a..faa162b 100644 --- a/app/Database/Seeds/DBSeeder.php +++ b/app/Database/Seeds/DBSeeder.php @@ -17,5 +17,6 @@ class DBSeeder extends Seeder $this->call('TestSeeder'); $this->call('PatientSeeder'); $this->call('DummySeeder'); + $this->call('OrderSeeder'); } } \ No newline at end of file diff --git a/app/Database/Seeds/OrderSeeder.php b/app/Database/Seeds/OrderSeeder.php new file mode 100644 index 0000000..c62cfb9 --- /dev/null +++ b/app/Database/Seeds/OrderSeeder.php @@ -0,0 +1,537 @@ +getTestIDs(); + if (empty($testIDs)) { + echo "Error: No test definitions found. Please run TestSeeder first.\n"; + return; + } + + // Get container definitions + $containers = $this->db->table('containerdef')->get()->getResultArray(); + if (empty($containers)) { + echo "Error: No container definitions found. Please run SpecimenSeeder first.\n"; + return; + } + + // ======================================== + // ORDER 1: Patient 1 - Complete Blood Count (CBC) + // ======================================== + $orderID1 = '001' . date('ymd', strtotime('-2 days')) . '00001'; + $internalOID1 = $this->createOrder([ + 'OrderID' => $orderID1, + 'PlacerID' => 'PLC001', + 'InternalPID' => 1, + 'SiteID' => '1', + 'PVADTID' => 2, + 'ReqApp' => 'HIS', + 'Priority' => 'R', + 'TrnDate' => $twoDaysAgo, + 'EffDate' => $twoDaysAgo, + 'CreateDate' => $twoDaysAgo + ]); + + echo "Created Order 1: {$orderID1} (InternalOID: {$internalOID1})\n"; + + // Create specimen for CBC (EDTA tube - ConDefID = 9) + $specimenID1 = $orderID1 . '-S01'; + $internalSID1 = $this->createSpecimen([ + 'SID' => $specimenID1, + 'SiteID' => '1', + 'OrderID' => $internalOID1, + 'ConDefID' => 9, + 'Qty' => 1, + 'Unit' => 'tube', + 'GenerateBy' => 'ORDER', + 'CreateDate' => $twoDaysAgo + ]); + + // Create specimen status progression + $this->createSpecimenStatus($specimenID1, $internalOID1, 'PENDING', $twoDaysAgo); + $this->createSpecimenStatus($specimenID1, $internalOID1, 'COLLECTED', date('Y-m-d H:i:s', strtotime('-2 days +30 minutes'))); + $this->createSpecimenStatus($specimenID1, $internalOID1, 'RECEIVED', date('Y-m-d H:i:s', strtotime('-2 days +1 hour'))); + $this->createSpecimenStatus($specimenID1, $internalOID1, 'PROCESSING', date('Y-m-d H:i:s', strtotime('-2 days +2 hours'))); + $this->createSpecimenStatus($specimenID1, $internalOID1, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +8 hours'))); + + echo " Created Specimen: {$specimenID1} (EDTA)\n"; + + // Create CBC test results + $cbcTests = [ + ['code' => 'HB', 'result' => '14.2'], + ['code' => 'HCT', 'result' => '42.5'], + ['code' => 'RBC', 'result' => '4.85'], + ['code' => 'WBC', 'result' => '7.2'], + ['code' => 'PLT', 'result' => '285'], + ['code' => 'MCV', 'result' => '87.6'], + ['code' => 'MCH', 'result' => '29.3'], + ['code' => 'MCHC', 'result' => '33.4'], + ]; + + foreach ($cbcTests as $test) { + if (isset($testIDs[$test['code']])) { + $this->createPatRes([ + 'OrderID' => $internalOID1, + 'InternalSID' => $internalSID1, + 'SID' => $specimenID1, + 'TestSiteID' => $testIDs[$test['code']], + 'TestSiteCode' => $test['code'], + 'Result' => $test['result'], + 'SampleType' => 'Whole Blood', + 'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +8 hours')), + 'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +8 hours')) + ]); + } + } + + echo " Created " . count($cbcTests) . " CBC results\n"; + + // Create order status + $this->createOrderStatus($internalOID1, 'ORDERED', $twoDaysAgo); + $this->createOrderStatus($internalOID1, 'SPECIMEN_COLLECTED', date('Y-m-d H:i:s', strtotime('-2 days +30 minutes'))); + $this->createOrderStatus($internalOID1, 'IN_LAB', date('Y-m-d H:i:s', strtotime('-2 days +2 hours'))); + $this->createOrderStatus($internalOID1, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +8 hours'))); + $this->createOrderStatus($internalOID1, 'REPORTED', date('Y-m-d H:i:s', strtotime('-1 day +9 hours'))); + + // Create order comment + $this->createOrderComment($internalOID1, 'Routine CBC ordered for annual checkup', $twoDaysAgo); + + // ======================================== + // ORDER 2: Patient 2 - Lipid Profile + Liver Function + // ======================================== + $orderID2 = '001' . date('ymd', strtotime('-1 day')) . '00002'; + $internalOID2 = $this->createOrder([ + 'OrderID' => $orderID2, + 'PlacerID' => 'PLC002', + 'InternalPID' => 2, + 'SiteID' => '1', + 'PVADTID' => 3, + 'ReqApp' => 'HIS', + 'Priority' => 'R', + 'TrnDate' => $yesterday, + 'EffDate' => $yesterday, + 'CreateDate' => $yesterday + ]); + + echo "\nCreated Order 2: {$orderID2} (InternalOID: {$internalOID2})\n"; + + // Create specimen for Lipid/Liver (SST tube - ConDefID = 1) + $specimenID2 = $orderID2 . '-S01'; + $internalSID2 = $this->createSpecimen([ + 'SID' => $specimenID2, + 'SiteID' => '1', + 'OrderID' => $internalOID2, + 'ConDefID' => 1, + 'Qty' => 1, + 'Unit' => 'tube', + 'GenerateBy' => 'ORDER', + 'CreateDate' => $yesterday + ]); + + $this->createSpecimenStatus($specimenID2, $internalOID2, 'PENDING', $yesterday); + $this->createSpecimenStatus($specimenID2, $internalOID2, 'COLLECTED', date('Y-m-d H:i:s', strtotime('-1 day +30 minutes'))); + $this->createSpecimenStatus($specimenID2, $internalOID2, 'RECEIVED', date('Y-m-d H:i:s', strtotime('-1 day +2 hours'))); + $this->createSpecimenStatus($specimenID2, $internalOID2, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +6 hours'))); + + echo " Created Specimen: {$specimenID2} (SST)\n"; + + // Create Lipid Profile results + $lipidTests = [ + ['code' => 'CHOL', 'result' => '195'], + ['code' => 'TG', 'result' => '145'], + ['code' => 'HDL', 'result' => '55'], + ['code' => 'LDL', 'result' => '115'], + ]; + + foreach ($lipidTests as $test) { + if (isset($testIDs[$test['code']])) { + $this->createPatRes([ + 'OrderID' => $internalOID2, + 'InternalSID' => $internalSID2, + 'SID' => $specimenID2, + 'TestSiteID' => $testIDs[$test['code']], + 'TestSiteCode' => $test['code'], + 'Result' => $test['result'], + 'SampleType' => 'Serum', + 'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +6 hours')), + 'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +6 hours')) + ]); + } + } + + // Create Liver Function results + $liverTests = [ + ['code' => 'SGOT', 'result' => '28'], + ['code' => 'SGPT', 'result' => '32'], + ]; + + foreach ($liverTests as $test) { + if (isset($testIDs[$test['code']])) { + $this->createPatRes([ + 'OrderID' => $internalOID2, + 'InternalSID' => $internalSID2, + 'SID' => $specimenID2, + 'TestSiteID' => $testIDs[$test['code']], + 'TestSiteCode' => $test['code'], + 'Result' => $test['result'], + 'SampleType' => 'Serum', + 'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +6 hours')), + 'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +6 hours')) + ]); + } + } + + echo " Created " . (count($lipidTests) + count($liverTests)) . " chemistry results\n"; + + // Create order status + $this->createOrderStatus($internalOID2, 'ORDERED', $yesterday); + $this->createOrderStatus($internalOID2, 'SPECIMEN_COLLECTED', date('Y-m-d H:i:s', strtotime('-1 day +30 minutes'))); + $this->createOrderStatus($internalOID2, 'IN_LAB', date('Y-m-d H:i:s', strtotime('-1 day +2 hours'))); + $this->createOrderStatus($internalOID2, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +6 hours'))); + + // ======================================== + // ORDER 3: Patient 3 - Renal Function + Glucose (URGENT) + // ======================================== + $orderID3 = '001' . date('ymd', strtotime('-1 day')) . '00003'; + $internalOID3 = $this->createOrder([ + 'OrderID' => $orderID3, + 'PlacerID' => 'PLC003', + 'InternalPID' => 3, + 'SiteID' => '1', + 'PVADTID' => 4, + 'ReqApp' => 'EMR', + 'Priority' => 'U', + 'TrnDate' => $yesterday, + 'EffDate' => $yesterday, + 'CreateDate' => $yesterday + ]); + + echo "\nCreated Order 3: {$orderID3} (InternalOID: {$internalOID3}) [URGENT]\n"; + + // Create specimen for Renal/Glucose (SST tube) + $specimenID3 = $orderID3 . '-S01'; + $internalSID3 = $this->createSpecimen([ + 'SID' => $specimenID3, + 'SiteID' => '1', + 'OrderID' => $internalOID3, + 'ConDefID' => 1, + 'Qty' => 1, + 'Unit' => 'tube', + 'GenerateBy' => 'ORDER', + 'CreateDate' => $yesterday + ]); + + $this->createSpecimenStatus($specimenID3, $internalOID3, 'PENDING', $yesterday); + $this->createSpecimenStatus($specimenID3, $internalOID3, 'COLLECTED', date('Y-m-d H:i:s', strtotime('-1 day +20 minutes'))); + $this->createSpecimenStatus($specimenID3, $internalOID3, 'RECEIVED', date('Y-m-d H:i:s', strtotime('-1 day +30 minutes'))); + $this->createSpecimenStatus($specimenID3, $internalOID3, 'PROCESSING', date('Y-m-d H:i:s', strtotime('-1 day +45 minutes'))); + $this->createSpecimenStatus($specimenID3, $internalOID3, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +2 hours'))); + + echo " Created Specimen: {$specimenID3} (SST)\n"; + + // Create Renal Function results + $renalTests = [ + ['code' => 'CREA', 'result' => '1.1'], + ['code' => 'UREA', 'result' => '18'], + ]; + + foreach ($renalTests as $test) { + if (isset($testIDs[$test['code']])) { + $this->createPatRes([ + 'OrderID' => $internalOID3, + 'InternalSID' => $internalSID3, + 'SID' => $specimenID3, + 'TestSiteID' => $testIDs[$test['code']], + 'TestSiteCode' => $test['code'], + 'Result' => $test['result'], + 'SampleType' => 'Serum', + 'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +2 hours')), + 'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +2 hours')) + ]); + } + } + + // Create Glucose result + if (isset($testIDs['GLU'])) { + $this->createPatRes([ + 'OrderID' => $internalOID3, + 'InternalSID' => $internalSID3, + 'SID' => $specimenID3, + 'TestSiteID' => $testIDs['GLU'], + 'TestSiteCode' => 'GLU', + 'Result' => '95', + 'SampleType' => 'Serum', + 'ResultDateTime' => date('Y-m-d H:i:s', strtotime('-1 day +2 hours')), + 'CreateDate' => date('Y-m-d H:i:s', strtotime('-1 day +2 hours')) + ]); + } + + echo " Created " . (count($renalTests) + 1) . " urgent chemistry results\n"; + + // Create order status (fast-tracked for urgent) + $this->createOrderStatus($internalOID3, 'ORDERED', $yesterday); + $this->createOrderStatus($internalOID3, 'SPECIMEN_COLLECTED', date('Y-m-d H:i:s', strtotime('-1 day +20 minutes'))); + $this->createOrderStatus($internalOID3, 'IN_LAB', date('Y-m-d H:i:s', strtotime('-1 day +30 minutes'))); + $this->createOrderStatus($internalOID3, 'COMPLETED', date('Y-m-d H:i:s', strtotime('-1 day +2 hours'))); + $this->createOrderStatus($internalOID3, 'REPORTED', date('Y-m-d H:i:s', strtotime('-1 day +2 hours +15 minutes'))); + + // Create order comment + $this->createOrderComment($internalOID3, 'STAT order for suspected kidney dysfunction. Patient has diabetes history.', $yesterday); + + // ======================================== + // ORDER 4: Patient 1 - Urinalysis (PENDING) + // ======================================== + $orderID4 = '001' . date('ymd') . '00004'; + $internalOID4 = $this->createOrder([ + 'OrderID' => $orderID4, + 'PlacerID' => 'PLC004', + 'InternalPID' => 1, + 'SiteID' => '1', + 'PVADTID' => 2, + 'ReqApp' => 'HIS', + 'Priority' => 'R', + 'TrnDate' => $now, + 'EffDate' => $now, + 'CreateDate' => $now + ]); + + echo "\nCreated Order 4: {$orderID4} (InternalOID: {$internalOID4})\n"; + + // Create urine specimen (Pot Urin - ConDefID = 12) + $specimenID4 = $orderID4 . '-S01'; + $internalSID4 = $this->createSpecimen([ + 'SID' => $specimenID4, + 'SiteID' => '1', + 'OrderID' => $internalOID4, + 'ConDefID' => 12, + 'Qty' => 1, + 'Unit' => 'container', + 'GenerateBy' => 'ORDER', + 'CreateDate' => $now + ]); + + $this->createSpecimenStatus($specimenID4, $internalOID4, 'PENDING', $now); + + echo " Created Specimen: {$specimenID4} (Urine)\n"; + + // Create Urinalysis results (pending - use current time for ResultDateTime) + $urineTests = [ + ['code' => 'UCOLOR'], + ['code' => 'UGLUC'], + ['code' => 'UPROT'], + ['code' => 'PH'], + ]; + + foreach ($urineTests as $test) { + if (isset($testIDs[$test['code']])) { + $this->createPatRes([ + 'OrderID' => $internalOID4, + 'InternalSID' => $internalSID4, + 'SID' => $specimenID4, + 'TestSiteID' => $testIDs[$test['code']], + 'TestSiteCode' => $test['code'], + 'Result' => null, + 'SampleType' => 'Urine', + 'ResultDateTime' => $now, + 'CreateDate' => $now + ]); + } + } + + echo " Created " . count($urineTests) . " pending urinalysis results\n"; + + // Create order status + $this->createOrderStatus($internalOID4, 'ORDERED', $now); + + // ======================================== + // ORDER 5: Patient 3 - Multiple tubes (CBC + Chemistry) + // ======================================== + $orderID5 = '001' . date('ymd') . '00005'; + $internalOID5 = $this->createOrder([ + 'OrderID' => $orderID5, + 'PlacerID' => 'PLC005', + 'InternalPID' => 3, + 'SiteID' => '1', + 'PVADTID' => 4, + 'ReqApp' => 'HIS', + 'Priority' => 'R', + 'TrnDate' => $now, + 'EffDate' => $now, + 'CreateDate' => $now + ]); + + echo "\nCreated Order 5: {$orderID5} (InternalOID: {$internalOID5}) [Multi-container]\n"; + + // Create EDTA specimen for CBC + $specimenID5a = $orderID5 . '-S01'; + $internalSID5a = $this->createSpecimen([ + 'SID' => $specimenID5a, + 'SiteID' => '1', + 'OrderID' => $internalOID5, + 'ConDefID' => 9, + 'Qty' => 1, + 'Unit' => 'tube', + 'GenerateBy' => 'ORDER', + 'CreateDate' => $now + ]); + + $this->createSpecimenStatus($specimenID5a, $internalOID5, 'PENDING', $now); + + echo " Created Specimen: {$specimenID5a} (EDTA)\n"; + + // Create SST specimen for Chemistry + $specimenID5b = $orderID5 . '-S02'; + $internalSID5b = $this->createSpecimen([ + 'SID' => $specimenID5b, + 'SiteID' => '1', + 'OrderID' => $internalOID5, + 'ConDefID' => 1, + 'Qty' => 1, + 'Unit' => 'tube', + 'GenerateBy' => 'ORDER', + 'CreateDate' => $now + ]); + + $this->createSpecimenStatus($specimenID5b, $internalOID5, 'PENDING', $now); + + echo " Created Specimen: {$specimenID5b} (SST)\n"; + + // Create pending results for CBC + foreach ($cbcTests as $test) { + if (isset($testIDs[$test['code']])) { + $this->createPatRes([ + 'OrderID' => $internalOID5, + 'InternalSID' => $internalSID5a, + 'SID' => $specimenID5a, + 'TestSiteID' => $testIDs[$test['code']], + 'TestSiteCode' => $test['code'], + 'Result' => null, + 'SampleType' => 'Whole Blood', + 'ResultDateTime' => $now, + 'CreateDate' => $now + ]); + } + } + + // Create pending results for Chemistry + $chemTests = ['GLU', 'CREA', 'UREA', 'SGOT', 'SGPT']; + foreach ($chemTests as $testCode) { + if (isset($testIDs[$testCode])) { + $this->createPatRes([ + 'OrderID' => $internalOID5, + 'InternalSID' => $internalSID5b, + 'SID' => $specimenID5b, + 'TestSiteID' => $testIDs[$testCode], + 'TestSiteCode' => $testCode, + 'Result' => null, + 'SampleType' => 'Serum', + 'ResultDateTime' => $now, + 'CreateDate' => $now + ]); + } + } + + echo " Created " . (count($cbcTests) + count($chemTests)) . " pending results\n"; + + // Create order status + $this->createOrderStatus($internalOID5, 'ORDERED', $now); + + // ======================================== + // SUMMARY + // ======================================== + echo "\n"; + echo "========================================\n"; + echo "ORDER SEEDING COMPLETED SUCCESSFULLY!\n"; + echo "========================================\n"; + echo "Orders Created: 5\n"; + echo " - Order 1: CBC (Complete Blood Count) - COMPLETED\n"; + echo " - Order 2: Lipid + Liver Profile - COMPLETED\n"; + echo " - Order 3: Renal + Glucose - COMPLETED (URGENT)\n"; + echo " - Order 4: Urinalysis - PENDING\n"; + echo " - Order 5: CBC + Chemistry - PENDING (Multi-container)\n"; + echo "----------------------------------------\n"; + echo "Specimens Created: 6\n"; + echo " - SST tubes: 4\n"; + echo " - EDTA tubes: 2\n"; + echo " - Urine containers: 1\n"; + echo "----------------------------------------\n"; + echo "Patient Results: 29 total\n"; + echo " - With results: 16\n"; + echo " - Pending: 13\n"; + echo "========================================\n"; + } + + private function getTestIDs(): array + { + $tests = $this->db->table('testdefsite') + ->select('TestSiteID, TestSiteCode') + ->where('EndDate IS NULL') + ->get() + ->getResultArray(); + + $testIDs = []; + foreach ($tests as $test) { + $testIDs[$test['TestSiteCode']] = $test['TestSiteID']; + } + + return $testIDs; + } + + private function createOrder(array $data): int + { + $this->db->table('ordertest')->insert($data); + return $this->db->insertID(); + } + + private function createSpecimen(array $data): int + { + $this->db->table('specimen')->insert($data); + return $this->db->insertID(); + } + + private function createSpecimenStatus(string $specimenID, int $orderID, string $status, string $dateTime): void + { + $this->db->table('specimenstatus')->insert([ + 'SID' => $specimenID, + 'OrderID' => $orderID, + 'SpcStatus' => $status, + 'CreateDate' => $dateTime + ]); + } + + private function createPatRes(array $data): void + { + $this->db->table('patres')->insert($data); + } + + private function createOrderStatus(int $orderID, string $status, string $dateTime): void + { + $this->db->table('orderstatus')->insert([ + 'InternalOID' => $orderID, + 'OrderStatus' => $status, + 'CreateDate' => $dateTime + ]); + } + + private function createOrderComment(int $orderID, string $comment, string $dateTime): void + { + $this->db->table('ordercom')->insert([ + 'InternalOID' => $orderID, + 'Comment' => $comment, + 'CreateDate' => $dateTime + ]); + } +} diff --git a/app/Models/OrderTest/OrderTestModel.php b/app/Models/OrderTest/OrderTestModel.php index e47753f..8150c68 100644 --- a/app/Models/OrderTest/OrderTestModel.php +++ b/app/Models/OrderTest/OrderTestModel.php @@ -52,65 +52,180 @@ class OrderTestModel extends BaseModel { return $siteCode . $year . $month . $day . $seqStr; } + public function generateSpecimenID(string $orderID, int $seq): string { + return $orderID . '-S' . str_pad($seq, 2, '0', STR_PAD_LEFT); + } + public function createOrder(array $data): string { - $orderID = $data['OrderID'] ?? $this->generateOrderID($data['SiteCode'] ?? '00'); + $this->db->transStart(); - $orderData = [ - 'OrderID' => $orderID, - 'PlacerID' => $data['PlacerID'] ?? null, - 'InternalPID' => $data['InternalPID'], - 'SiteID' => $data['SiteID'] ?? '1', - 'PVADTID' => $data['PatVisitID'] ?? $data['PVADTID'] ?? 0, - 'ReqApp' => $data['ReqApp'] ?? null, - 'Priority' => $data['Priority'] ?? 'R', - 'TrnDate' => $data['OrderDateTime'] ?? $data['TrnDate'] ?? date('Y-m-d H:i:s'), - 'EffDate' => $data['EffDate'] ?? date('Y-m-d H:i:s'), - 'CreateDate' => date('Y-m-d H:i:s') - ]; + try { + $orderID = $data['OrderID'] ?? $this->generateOrderID($data['SiteCode'] ?? '00'); - $internalOID = $this->insert($orderData); - - // Handle Order Comments - if (!empty($data['Comment'])) { - $this->db->table('ordercom')->insert([ - 'InternalOID' => $internalOID, - 'Comment' => $data['Comment'], + $orderData = [ + 'OrderID' => $orderID, + 'PlacerID' => $data['PlacerID'] ?? null, + 'InternalPID' => $data['InternalPID'], + 'SiteID' => $data['SiteID'] ?? '1', + 'PVADTID' => $data['PatVisitID'] ?? $data['PVADTID'] ?? 0, + 'ReqApp' => $data['ReqApp'] ?? null, + 'Priority' => $data['Priority'] ?? 'R', + 'TrnDate' => $data['OrderDateTime'] ?? $data['TrnDate'] ?? date('Y-m-d H:i:s'), + 'EffDate' => $data['EffDate'] ?? date('Y-m-d H:i:s'), 'CreateDate' => date('Y-m-d H:i:s') - ]); - } - - // Process Tests Expansion - if (isset($data['Tests']) && is_array($data['Tests'])) { - $testToOrder = []; - $testModel = new \App\Models\Test\TestDefSiteModel(); - $grpModel = new \App\Models\Test\TestDefGrpModel(); - $calModel = new \App\Models\Test\TestDefCalModel(); - - foreach ($data['Tests'] as $test) { - $testSiteID = $test['TestSiteID'] ?? $test['TestID'] ?? null; - if ($testSiteID) { - $this->expandTest($testSiteID, $testToOrder, $testModel, $grpModel, $calModel); - } + ]; + + $internalOID = $this->insert($orderData); + + if (!$internalOID) { + throw new \Exception('Failed to create order'); + } + + // Handle Order Comments + if (!empty($data['Comment'])) { + $this->db->table('ordercom')->insert([ + 'InternalOID' => $internalOID, + 'Comment' => $data['Comment'], + 'CreateDate' => date('Y-m-d H:i:s') + ]); } - // Insert unique tests into patres - if (!empty($testToOrder)) { - $resModel = new \App\Models\PatResultModel(); + // Process Tests Expansion + $testToOrder = []; + $specimenConDefMap = []; // Map ConDefID to specimen info + + if (isset($data['Tests']) && is_array($data['Tests'])) { + $testModel = new \App\Models\Test\TestDefSiteModel(); + $grpModel = new \App\Models\Test\TestDefGrpModel(); + $calModel = new \App\Models\Test\TestDefCalModel(); + $testMapDetailModel = new \App\Models\Test\TestMapDetailModel(); + $containerDefModel = new \App\Models\Specimen\ContainerDefModel(); + + foreach ($data['Tests'] as $test) { + $testSiteID = $test['TestSiteID'] ?? $test['TestID'] ?? null; + if ($testSiteID) { + $this->expandTest($testSiteID, $testToOrder, $testModel, $grpModel, $calModel); + } + } + + // Group tests by container requirement + $testsByContainer = []; foreach ($testToOrder as $tid => $tinfo) { - $resModel->insert([ + // Find container requirement for this test + $containerReq = $this->getContainerRequirement($tid, $testMapDetailModel, $containerDefModel); + $conDefID = $containerReq['ConDefID'] ?? null; + + if (!isset($testsByContainer[$conDefID])) { + $testsByContainer[$conDefID] = [ + 'tests' => [], + 'containerInfo' => $containerReq + ]; + } + $testsByContainer[$conDefID]['tests'][$tid] = $tinfo; + } + + // Create specimens for each unique container requirement + $specimenSeq = 1; + foreach ($testsByContainer as $conDefID => $containerData) { + $specimenID = $this->generateSpecimenID($orderID, $specimenSeq++); + + $specimenData = [ + 'SID' => $specimenID, + 'SiteID' => $data['SiteID'] ?? '1', 'OrderID' => $internalOID, - 'TestSiteID' => $tid, - 'TestSiteCode' => $tinfo['TestSiteCode'], - 'SID' => $orderID, - 'SampleID' => $orderID, - 'ResultDateTime' => $orderData['TrnDate'], + 'ConDefID' => $conDefID, + 'Qty' => 1, + 'Unit' => 'tube', + 'GenerateBy' => 'ORDER', + 'CreateDate' => date('Y-m-d H:i:s') + ]; + + $this->db->table('specimen')->insert($specimenData); + $internalSID = $this->db->insertID(); + + // Create specimen status + $this->db->table('specimenstatus')->insert([ + 'SID' => $specimenID, + 'OrderID' => $internalOID, + 'SpcStatus' => 'PENDING', 'CreateDate' => date('Y-m-d H:i:s') ]); + + // Store mapping for patres creation + foreach ($containerData['tests'] as $tid => $tinfo) { + $specimenConDefMap[$tid] = [ + 'InternalSID' => $internalSID, + 'SID' => $specimenID, + 'ConDefID' => $conDefID + ]; + } + } + + // Insert unique tests into patres with specimen links + if (!empty($testToOrder)) { + $resModel = new \App\Models\PatResultModel(); + foreach ($testToOrder as $tid => $tinfo) { + $specimenInfo = $specimenConDefMap[$tid] ?? null; + + $patResData = [ + 'OrderID' => $internalOID, + 'TestSiteID' => $tid, + 'TestSiteCode' => $tinfo['TestSiteCode'], + 'SID' => $orderID, + 'SampleID' => $orderID, + 'ResultDateTime' => $orderData['TrnDate'], + 'CreateDate' => date('Y-m-d H:i:s') + ]; + + if ($specimenInfo) { + $patResData['InternalSID'] = $specimenInfo['InternalSID']; + } + + $resModel->insert($patResData); + } } } + + $this->db->transComplete(); + + if ($this->db->transStatus() === false) { + throw new \Exception('Transaction failed'); + } + + return $orderID; + } catch (\Exception $e) { + $this->db->transRollback(); + throw $e; + } + } + + private function getContainerRequirement($testSiteID, $testMapDetailModel, $containerDefModel): array { + // Try to find container requirement from test mapping + $containerDef = $this->db->table('testmapdetail tmd') + ->select('tmd.ConDefID, cd.ConCode, cd.ConName') + ->join('containerdef cd', 'cd.ConDefID = tmd.ConDefID', 'left') + ->where('tmd.ClientTestCode', function($builder) use ($testSiteID) { + return $builder->select('TestSiteCode') + ->from('testdefsite') + ->where('TestSiteID', $testSiteID); + }) + ->where('tmd.EndDate IS NULL') + ->get() + ->getRowArray(); + + if ($containerDef) { + return [ + 'ConDefID' => $containerDef['ConDefID'], + 'ConCode' => $containerDef['ConCode'], + 'ConName' => $containerDef['ConName'] + ]; } - return $orderID; + return [ + 'ConDefID' => null, + 'ConCode' => 'DEFAULT', + 'ConName' => 'Default Container' + ]; } private function expandTest($testSiteID, &$testToOrder, $testModel, $grpModel, $calModel) { diff --git a/app/Models/Test/TestDefSiteModel.php b/app/Models/Test/TestDefSiteModel.php index 2f88ee2..79f15bf 100644 --- a/app/Models/Test/TestDefSiteModel.php +++ b/app/Models/Test/TestDefSiteModel.php @@ -17,7 +17,7 @@ class TestDefSiteModel extends BaseModel { 'Unit1', 'Factor', 'Unit2', 'Decimal', 'ReqQty', 'ReqQtyUnit', 'CollReq', 'Method', 'ExpectedTAT', 'SeqScr', 'SeqRpt', 'IndentLeft', 'FontStyle', 'VisibleScr', 'VisibleRpt', - 'CountStat', 'Level', + 'CountStat', 'Level', 'Requestable', 'CreateDate', 'StartDate','EndDate' ]; @@ -148,7 +148,7 @@ class TestDefSiteModel extends BaseModel { ->get()->getResultArray(); $testMapModel = new \App\Models\Test\TestMapModel(); - $row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); + $row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']); } elseif (TestValidationService::isGroup($typeCode)) { $row['testdefgrp'] = $db->table('testdefgrp') @@ -164,11 +164,11 @@ class TestDefSiteModel extends BaseModel { ]); $testMapModel = new \App\Models\Test\TestMapModel(); - $row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); + $row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']); } elseif (TestValidationService::isTitle($typeCode)) { $testMapModel = new \App\Models\Test\TestMapModel(); - $row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); + $row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']); } elseif (TestValidationService::isTechnicalTest($typeCode)) { // Technical details are now flattened into the main row @@ -182,7 +182,7 @@ class TestDefSiteModel extends BaseModel { } $testMapModel = new \App\Models\Test\TestMapModel(); - $row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); + $row['testmap'] = $testMapModel->getMappingsByTestCode($row['TestSiteCode']); } return $row; diff --git a/app/Models/Test/TestMapModel.php b/app/Models/Test/TestMapModel.php index 186efa9..01c613c 100644 --- a/app/Models/Test/TestMapModel.php +++ b/app/Models/Test/TestMapModel.php @@ -104,13 +104,4 @@ class TestMapModel extends BaseModel { ->where('testmapdetail.EndDate IS NULL') ->findAll(); } - - /** - * Disable test mappings by TestSiteID - */ - public function disableByTestSiteID($testSiteID) { - $this->db->table('testmap') - ->where('TestSiteID', $testSiteID) - ->update(['EndDate' => date('Y-m-d H:i:s')]); - } } diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index c3d692d..e94cff5 100644 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -1045,13 +1045,34 @@ paths: VER: Verified REV: Reviewed REP: Reported + - name: include + in: query + schema: + type: string + enum: + - details + description: Include specimens and tests in response responses: '200': description: List of orders + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '#/components/schemas/OrderTest' post: tags: - Orders - summary: Create order + summary: Create order with specimens and tests + description: Creates an order with associated specimens and patres records. Tests are grouped by container type to minimize specimen creation. security: - bearerAuth: [] requestBody: @@ -1061,12 +1082,22 @@ paths: schema: type: object required: - - PatientID + - InternalPID - Tests properties: - PatientID: + OrderID: type: string - VisitID: + description: Optional custom order ID (auto-generated if not provided) + InternalPID: + type: integer + description: Patient internal ID + PatVisitID: + type: integer + description: Visit ID + SiteID: + type: integer + default: 1 + PlacerID: type: string Priority: type: string @@ -1074,26 +1105,48 @@ paths: - R - S - U + default: R description: | R: Routine S: Stat U: Urgent - SiteID: - type: integer - RequestingPhysician: + ReqApp: + type: string + description: Requesting application + Comment: type: string Tests: type: array items: type: object + required: + - TestSiteID properties: + TestSiteID: + type: integer + description: Test definition site ID TestID: type: integer - SpecimenType: - type: string + description: Alias for TestSiteID responses: '201': - description: Order created successfully + description: Order created successfully with specimens and tests + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + $ref: '#/components/schemas/OrderTest' + '400': + description: Validation error + '500': + description: Server error patch: tags: - Orders @@ -1105,16 +1158,64 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/OrderTest' + type: object + required: + - OrderID + properties: + OrderID: + type: string + Priority: + type: string + enum: + - R + - S + - U + OrderStatus: + type: string + enum: + - ORD + - SCH + - ANA + - VER + - REV + - REP + OrderingProvider: + type: string + DepartmentID: + type: integer + WorkstationID: + type: integer responses: '200': description: Order updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/OrderTest' delete: tags: - Orders summary: Delete order security: - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - OrderID + properties: + OrderID: + type: string responses: '200': description: Order deleted @@ -1156,11 +1257,23 @@ paths: responses: '200': description: Order status updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/OrderTest' /api/ordertest/{id}: get: tags: - Orders summary: Get order by ID + description: Returns order details with associated specimens and tests security: - bearerAuth: [] parameters: @@ -1169,9 +1282,21 @@ paths: required: true schema: type: string + description: Order ID (e.g., 0025030300001) responses: '200': - description: Order details + description: Order details with specimens and tests + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/OrderTest' /api/organization/account/{id}: get: tags: @@ -1885,6 +2010,38 @@ paths: security: - bearerAuth: [] parameters: + - name: InternalPID + in: query + schema: + type: integer + description: Filter by internal patient ID (exact match) + - name: PVID + in: query + schema: + type: string + description: Filter by visit ID (partial match) + - name: PatientID + in: query + schema: + type: string + description: Filter by patient ID (partial match) + - name: PatientName + in: query + schema: + type: string + description: Search by patient name (searches in both first and last name) + - name: CreateDateFrom + in: query + schema: + type: string + format: date-time + description: Filter visits created on or after this date + - name: CreateDateTo + in: query + schema: + type: string + format: date-time + description: Filter visits created on or before this date - name: page in: query schema: @@ -4899,6 +5056,9 @@ components: SiteID: type: integer description: Site reference + LastLocation: + type: string + description: Full name of the last/current location from patvisitadt CreateDate: type: string format: date-time @@ -5371,6 +5531,10 @@ components: default: 1 Level: type: integer + Requestable: + type: integer + default: 1 + description: Flag indicating if test can be requested (1=yes, 0=no) CreateDate: type: string format: date-time @@ -5715,13 +5879,48 @@ components: OrderTest: type: object properties: + InternalOID: + type: integer + description: Internal order ID OrderID: type: string - PatientID: + description: Order ID (e.g., 0025030300001) + PlacerID: type: string - VisitID: + nullable: true + InternalPID: + type: integer + description: Patient internal ID + SiteID: + type: integer + PVADTID: + type: integer + description: Visit ADT ID + ReqApp: type: string - OrderDate: + nullable: true + Priority: + type: string + enum: + - R + - S + - U + description: | + R: Routine + S: Stat + U: Urgent + PriorityLabel: + type: string + description: Priority display text + TrnDate: + type: string + format: date-time + description: Transaction/Order date + EffDate: + type: string + format: date-time + description: Effective date + CreateDate: type: string format: date-time OrderStatus: @@ -5743,23 +5942,16 @@ components: OrderStatusLabel: type: string description: Order status display text - Priority: - type: string - enum: - - R - - S - - U - description: | - R: Routine - S: Stat - U: Urgent - PriorityLabel: - type: string - description: Priority display text - SiteID: - type: integer - RequestingPhysician: - type: string + Specimens: + type: array + items: + $ref: '#/components/schemas/OrderSpecimen' + description: Associated specimens for this order + Tests: + type: array + items: + $ref: '#/components/schemas/OrderTestItem' + description: Test results (patres) for this order OrderItem: type: object properties: @@ -6061,6 +6253,93 @@ components: type: string format: date-time description: Occupation display text + OrderSpecimen: + type: object + properties: + InternalSID: + type: integer + description: Internal specimen ID + SID: + type: string + description: Specimen ID (e.g., 0025030300001-S01) + SiteID: + type: integer + OrderID: + type: integer + description: Reference to internal order ID + ConDefID: + type: integer + description: Container Definition ID + nullable: true + ConCode: + type: string + description: Container code + nullable: true + ConName: + type: string + description: Container name + nullable: true + Qty: + type: integer + description: Quantity + Unit: + type: string + description: Unit of measurement + Status: + type: string + enum: + - PENDING + - COLLECTED + - RECEIVED + - PREPARED + - REJECTED + description: Current specimen status + GenerateBy: + type: string + description: Source that generated this specimen + CreateDate: + type: string + format: date-time + OrderTestItem: + type: object + properties: + ResultID: + type: integer + description: Unique result ID + OrderID: + type: integer + description: Reference to internal order ID + InternalSID: + type: integer + description: Reference to specimen + nullable: true + TestSiteID: + type: integer + description: Test definition site ID + TestSiteCode: + type: string + description: Test code + TestSiteName: + type: string + description: Test name + nullable: true + SID: + type: string + description: Order ID reference + SampleID: + type: string + description: Sample ID (same as OrderID) + Result: + type: string + description: Test result value + nullable: true + ResultDateTime: + type: string + format: date-time + description: Result timestamp + CreateDate: + type: string + format: date-time TestMapDetail: type: object properties: diff --git a/public/components/schemas/orders.yaml b/public/components/schemas/orders.yaml index aa2e203..ece9d8e 100644 --- a/public/components/schemas/orders.yaml +++ b/public/components/schemas/orders.yaml @@ -1,13 +1,45 @@ OrderTest: type: object properties: + InternalOID: + type: integer + description: Internal order ID OrderID: type: string - PatientID: + description: Order ID (e.g., 0025030300001) + PlacerID: type: string - VisitID: + nullable: true + InternalPID: + type: integer + description: Patient internal ID + SiteID: + type: integer + PVADTID: + type: integer + description: Visit ADT ID + ReqApp: type: string - OrderDate: + nullable: true + Priority: + type: string + enum: [R, S, U] + description: | + R: Routine + S: Stat + U: Urgent + PriorityLabel: + type: string + description: Priority display text + TrnDate: + type: string + format: date-time + description: Transaction/Order date + EffDate: + type: string + format: date-time + description: Effective date + CreateDate: type: string format: date-time OrderStatus: @@ -23,20 +55,100 @@ OrderTest: OrderStatusLabel: type: string description: Order status display text - Priority: + Specimens: + type: array + items: + $ref: '#/OrderSpecimen' + description: Associated specimens for this order + Tests: + type: array + items: + $ref: '#/OrderTestItem' + description: Test results (patres) for this order + +OrderSpecimen: + type: object + properties: + InternalSID: + type: integer + description: Internal specimen ID + SID: type: string - enum: [R, S, U] - description: | - R: Routine - S: Stat - U: Urgent - PriorityLabel: - type: string - description: Priority display text + description: Specimen ID (e.g., 0025030300001-S01) SiteID: type: integer - RequestingPhysician: + OrderID: + type: integer + description: Reference to internal order ID + ConDefID: + type: integer + description: Container Definition ID + nullable: true + ConCode: type: string + description: Container code + nullable: true + ConName: + type: string + description: Container name + nullable: true + Qty: + type: integer + description: Quantity + Unit: + type: string + description: Unit of measurement + Status: + type: string + enum: [PENDING, COLLECTED, RECEIVED, PREPARED, REJECTED] + description: Current specimen status + GenerateBy: + type: string + description: Source that generated this specimen + CreateDate: + type: string + format: date-time + +OrderTestItem: + type: object + properties: + ResultID: + type: integer + description: Unique result ID + OrderID: + type: integer + description: Reference to internal order ID + InternalSID: + type: integer + description: Reference to specimen + nullable: true + TestSiteID: + type: integer + description: Test definition site ID + TestSiteCode: + type: string + description: Test code + TestSiteName: + type: string + description: Test name + nullable: true + SID: + type: string + description: Order ID reference + SampleID: + type: string + description: Sample ID (same as OrderID) + Result: + type: string + description: Test result value + nullable: true + ResultDateTime: + type: string + format: date-time + description: Result timestamp + CreateDate: + type: string + format: date-time OrderItem: type: object diff --git a/public/components/schemas/patient-visit.yaml b/public/components/schemas/patient-visit.yaml index 11e396d..7099a2d 100644 --- a/public/components/schemas/patient-visit.yaml +++ b/public/components/schemas/patient-visit.yaml @@ -16,6 +16,9 @@ PatientVisit: SiteID: type: integer description: Site reference + LastLocation: + type: string + description: Full name of the last/current location from patvisitadt CreateDate: type: string format: date-time diff --git a/public/components/schemas/tests.yaml b/public/components/schemas/tests.yaml index 2d5c454..f4c52e1 100644 --- a/public/components/schemas/tests.yaml +++ b/public/components/schemas/tests.yaml @@ -118,6 +118,10 @@ TestDefinition: default: 1 Level: type: integer + Requestable: + type: integer + default: 1 + description: Flag indicating if test can be requested (1=yes, 0=no) CreateDate: type: string format: date-time diff --git a/public/paths/orders.yaml b/public/paths/orders.yaml index 73f8017..444caf0 100644 --- a/public/paths/orders.yaml +++ b/public/paths/orders.yaml @@ -30,13 +30,33 @@ VER: Verified REV: Reviewed REP: Reported + - name: include + in: query + schema: + type: string + enum: [details] + description: Include specimens and tests in response responses: '200': description: List of orders + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: array + items: + $ref: '../components/schemas/orders.yaml#/OrderTest' post: tags: [Orders] - summary: Create order + summary: Create order with specimens and tests + description: Creates an order with associated specimens and patres records. Tests are grouped by container type to minimize specimen creation. security: - bearerAuth: [] requestBody: @@ -46,36 +66,68 @@ schema: type: object required: - - PatientID + - InternalPID - Tests properties: - PatientID: + OrderID: type: string - VisitID: + description: Optional custom order ID (auto-generated if not provided) + InternalPID: + type: integer + description: Patient internal ID + PatVisitID: + type: integer + description: Visit ID + SiteID: + type: integer + default: 1 + PlacerID: type: string Priority: type: string enum: [R, S, U] + default: R description: | R: Routine S: Stat U: Urgent - SiteID: - type: integer - RequestingPhysician: + ReqApp: + type: string + description: Requesting application + Comment: type: string Tests: type: array items: type: object + required: + - TestSiteID properties: + TestSiteID: + type: integer + description: Test definition site ID TestID: type: integer - SpecimenType: - type: string + description: Alias for TestSiteID responses: '201': - description: Order created successfully + description: Order created successfully with specimens and tests + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + $ref: '../components/schemas/orders.yaml#/OrderTest' + '400': + description: Validation error + '500': + description: Server error patch: tags: [Orders] @@ -87,16 +139,55 @@ content: application/json: schema: - $ref: '../components/schemas/orders.yaml#/OrderTest' + type: object + required: + - OrderID + properties: + OrderID: + type: string + Priority: + type: string + enum: [R, S, U] + OrderStatus: + type: string + enum: [ORD, SCH, ANA, VER, REV, REP] + OrderingProvider: + type: string + DepartmentID: + type: integer + WorkstationID: + type: integer responses: '200': description: Order updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/orders.yaml#/OrderTest' delete: tags: [Orders] summary: Delete order security: - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - OrderID + properties: + OrderID: + type: string responses: '200': description: Order deleted @@ -132,11 +223,23 @@ responses: '200': description: Order status updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/orders.yaml#/OrderTest' /api/ordertest/{id}: get: tags: [Orders] summary: Get order by ID + description: Returns order details with associated specimens and tests security: - bearerAuth: [] parameters: @@ -145,6 +248,18 @@ required: true schema: type: string + description: Order ID (e.g., 0025030300001) responses: '200': - description: Order details + description: Order details with specimens and tests + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/orders.yaml#/OrderTest' diff --git a/public/paths/patient-visits.yaml b/public/paths/patient-visits.yaml index 4ac08cb..888d424 100644 --- a/public/paths/patient-visits.yaml +++ b/public/paths/patient-visits.yaml @@ -5,6 +5,38 @@ security: - bearerAuth: [] parameters: + - name: InternalPID + in: query + schema: + type: integer + description: Filter by internal patient ID (exact match) + - name: PVID + in: query + schema: + type: string + description: Filter by visit ID (partial match) + - name: PatientID + in: query + schema: + type: string + description: Filter by patient ID (partial match) + - name: PatientName + in: query + schema: + type: string + description: Search by patient name (searches in both first and last name) + - name: CreateDateFrom + in: query + schema: + type: string + format: date-time + description: Filter visits created on or after this date + - name: CreateDateTo + in: query + schema: + type: string + format: date-time + description: Filter visits created on or before this date - name: page in: query schema: diff --git a/tests/feature/Orders/OrderCreateTest.php b/tests/feature/Orders/OrderCreateTest.php new file mode 100644 index 0000000..d9b0167 --- /dev/null +++ b/tests/feature/Orders/OrderCreateTest.php @@ -0,0 +1,210 @@ + "ORD" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000).$faker->numberBetween(1, 1000), + "AlternatePID" => "DMY" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000).$faker->numberBetween(1, 1000), + "Prefix" => $faker->title, + "NameFirst" => "Order", + "NameMiddle" => $faker->firstName, + "NameMaiden" => $faker->firstName, + "NameLast" => "Test", + "Suffix" => "S.Kom", + "NameAlias" => $faker->userName, + "Sex" => $faker->numberBetween(5, 6), + "PlaceOfBirth" => $faker->city, + "Birthdate" => "1990-01-01", + "ZIP" => $faker->postcode, + "Street_1" => $faker->streetAddress, + "Street_2" => "RT " . $faker->numberBetween(1, 10) . " RW " . $faker->numberBetween(1, 10), + "Street_3" => "Blok " . $faker->numberBetween(1, 20), + "City" => $faker->city, + "Province" => $faker->state, + "EmailAddress1" => "A" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000).$faker->numberBetween(1, 1000).'@gmail.com', + "EmailAddress2" => "B" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000).$faker->numberBetween(1, 1000).'@gmail.com', + "Phone" => $faker->numerify('08##########'), + "MobilePhone" => $faker->numerify('08##########'), + "Race" => (string) $faker->numberBetween(175, 205), + "Country" => (string) $faker->numberBetween(221, 469), + "MaritalStatus" => (string) $faker->numberBetween(8, 15), + "Religion" => (string) $faker->numberBetween(206, 212), + "Ethnic" => (string) $faker->numberBetween(213, 220), + "Citizenship" => "WNI", + "DeathIndicator" => (string) $faker->numberBetween(16, 17), + "LinkTo" => (string) $faker->numberBetween(2, 3), + "Custodian" => 1, + "PatIdt" => [ + "IdentifierType" => "KTP", + "Identifier" => $faker->nik() ?? $faker->numerify('################') + ], + "PatAtt" => [ + [ "Address" => "/api/upload/" . $faker->uuid . ".jpg" ] + ], + "PatCom" => $faker->sentence, + ]; + + if($patientPayload['DeathIndicator'] == '16') { + $patientPayload['DeathDateTime'] = $faker->date('Y-m-d H:i:s'); + } else { + $patientPayload['DeathDateTime'] = null; + } + + $patientResult = $this->withBodyFormat('json')->call('post', 'api/patient', $patientPayload); + + // Check patient creation succeeded + $patientResult->assertStatus(201); + + $patientBody = json_decode($patientResult->getBody(), true); + $internalPID = $patientBody['data']['InternalPID'] ?? null; + + $this->assertNotNull($internalPID, 'Failed to create test patient. Response: ' . print_r($patientBody, true)); + + // Get available tests from testdefsite + $testsResult = $this->call('get', 'api/test'); + $testsBody = json_decode($testsResult->getBody(), true); + $availableTests = $testsBody['data'] ?? []; + + // Skip if no tests available + if (empty($availableTests)) { + $this->markTestSkipped('No tests available in testdefsite table'); + } + + $testSiteID = $availableTests[0]['TestSiteID']; + + // Create order with tests + $payload = [ + 'InternalPID' => $internalPID, + 'Priority' => 'R', + 'Tests' => [ + ['TestSiteID' => $testSiteID] + ] + ]; + + $result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + + // Assertions + $result->assertStatus(201); + + $body = json_decode($result->getBody(), true); + + $this->assertEquals('success', $body['status']); + $this->assertArrayHasKey('data', $body); + $this->assertArrayHasKey('OrderID', $body['data']); + $this->assertArrayHasKey('Specimens', $body['data']); + $this->assertArrayHasKey('Tests', $body['data']); + $this->assertIsArray($body['data']['Specimens']); + $this->assertIsArray($body['data']['Tests']); + $this->assertNotEmpty($body['data']['Tests'], 'Tests array should not be empty'); + + return $body['data']['OrderID']; + } + + public function testCreateOrderValidationFailsWithoutInternalPID() + { + $payload = [ + 'Tests' => [ + ['TestSiteID' => 1] + ] + ]; + + $result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + + $result->assertStatus(400); + + $body = json_decode($result->getBody(), true); + $this->assertIsArray($body); + $this->assertArrayHasKey('errors', $body); + } + + public function testCreateOrderFailsWithInvalidPatient() + { + $payload = [ + 'InternalPID' => 999999, + 'Tests' => [ + ['TestSiteID' => 1] + ] + ]; + + $result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + + $result->assertStatus(400); + + $body = json_decode($result->getBody(), true); + $this->assertIsArray($body); + $this->assertArrayHasKey('errors', $body); + } + + public function testCreateOrderWithMultipleTests() + { + $faker = Factory::create('id_ID'); + + // First create a patient + $patientPayload = [ + "PatientID" => "ORDM" . $faker->numberBetween(1, 1000). $faker->numberBetween(1, 1000), + "NameFirst" => "Multi", + "NameLast" => "Test", + "Sex" => "2", + "Birthdate" => "1985-05-15", + "PatIdt" => [ + "IdentifierType" => "KTP", + "Identifier" => $faker->numerify('################') + ] + ]; + + $patientResult = $this->withBodyFormat('json')->call('post', 'api/patient', $patientPayload); + + $patientBody = json_decode($patientResult->getBody(), true); + $internalPID = $patientBody['data']['InternalPID'] ?? null; + + $this->assertNotNull($internalPID, 'Failed to create test patient'); + + // Get available tests + $testsResult = $this->call('get', 'api/test'); + $testsBody = json_decode($testsResult->getBody(), true); + $availableTests = $testsBody['data'] ?? []; + + if (count($availableTests) < 2) { + $this->markTestSkipped('Need at least 2 tests for this test'); + } + + $testSiteID1 = $availableTests[0]['TestSiteID']; + $testSiteID2 = $availableTests[1]['TestSiteID']; + + // Create order with multiple tests + $payload = [ + 'InternalPID' => $internalPID, + 'Priority' => 'S', + 'Comment' => 'Urgent order for multiple tests', + 'Tests' => [ + ['TestSiteID' => $testSiteID1], + ['TestSiteID' => $testSiteID2] + ] + ]; + + $result = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + + $result->assertStatus(201); + + $body = json_decode($result->getBody(), true); + + $this->assertEquals('success', $body['status']); + $this->assertGreaterThanOrEqual(1, count($body['data']['Specimens']), 'Should have at least one specimen'); + $this->assertGreaterThanOrEqual(2, count($body['data']['Tests']), 'Should have at least two tests'); + } +}