From 5e0a7f21f5e2f59ee84d5d44a2b07153259b94fa Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Thu, 26 Feb 2026 16:48:10 +0700 Subject: [PATCH] feat: add TestMapDetail controller, model and migration for test site mapping - Add TestMapDetailController for managing test mapping details - Create TestMapDetailModel with CRUD operations - Add migration for TestSiteID column in testmap table - Update TestMapController and TestMapModel - Update routes, seeder and PatVisitModel - Regenerate API documentation bundle --- app/Config/Routes.php | 15 +- app/Controllers/Test/TestMapController.php | 138 +++++-- .../Test/TestMapDetailController.php | 253 ++++++++++++ app/Controllers/TestsController.php | 40 +- ...026-01-01-000004_CreateTestDefinitions.php | 49 ++- .../2026-01-01-000012_CreatePatientVisits.php | 1 + ...26-02-26-090320_AddTestSiteIDToTestmap.php | 29 ++ app/Database/Seeds/TestSeeder.php | 389 +++++------------- app/Models/PatVisit/PatVisitModel.php | 2 +- app/Models/Test/TestMapDetailModel.php | 53 +++ app/Models/Test/TestMapModel.php | 103 +++-- public/api-docs.bundled.yaml | 389 ++++++++++++++---- public/components/schemas/tests.yaml | 42 +- public/paths/testmap.yaml | 334 +++++++++++---- 14 files changed, 1281 insertions(+), 556 deletions(-) create mode 100644 app/Controllers/Test/TestMapDetailController.php create mode 100644 app/Database/Migrations/2026-02-26-090320_AddTestSiteIDToTestmap.php create mode 100644 app/Models/Test/TestMapDetailModel.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e161742..edb87bc 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -279,8 +279,19 @@ $routes->group('api', function ($routes) { // Filter routes $routes->get('by-testsite/(:num)', 'Test\TestMapController::showByTestSite/$1'); - $routes->get('by-host/(:any)/(:any)', 'Test\TestMapController::showByHost/$1/$2'); - $routes->get('by-client/(:any)/(:any)', 'Test\TestMapController::showByClient/$1/$2'); + + // TestMapDetail nested routes + $routes->group('detail', function ($routes) { + $routes->get('/', 'Test\TestMapDetailController::index'); + $routes->get('(:num)', 'Test\TestMapDetailController::show/$1'); + $routes->post('/', 'Test\TestMapDetailController::create'); + $routes->patch('/', 'Test\TestMapDetailController::update'); + $routes->delete('/', 'Test\TestMapDetailController::delete'); + $routes->get('by-testmap/(:num)', 'Test\TestMapDetailController::showByTestMap/$1'); + $routes->post('batch', 'Test\TestMapDetailController::batchCreate'); + $routes->patch('batch', 'Test\TestMapDetailController::batchUpdate'); + $routes->delete('batch', 'Test\TestMapDetailController::batchDelete'); + }); }); }); diff --git a/app/Controllers/Test/TestMapController.php b/app/Controllers/Test/TestMapController.php index d57a792..10cfe66 100644 --- a/app/Controllers/Test/TestMapController.php +++ b/app/Controllers/Test/TestMapController.php @@ -5,6 +5,7 @@ use App\Traits\ResponseTrait; use App\Controllers\BaseController; use App\Libraries\ValueSet; use App\Models\Test\TestMapModel; +use App\Models\Test\TestMapDetailModel; class TestMapController extends BaseController { use ResponseTrait; @@ -12,14 +13,21 @@ class TestMapController extends BaseController { protected $db; protected $rules; protected $model; + protected $modelDetail; public function __construct() { $this->db = \Config\Database::connect(); $this->model = new TestMapModel; + $this->modelDetail = new TestMapDetailModel; + $this->rules = [ + 'TestSiteID' => 'required|integer', + 'HostID' => 'required|integer', + 'ClientID' => 'required|integer', + ]; } public function index() { - $rows = $this->model->findAll(); + $rows = $this->model->getUniqueGroupings(); if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); } $rows = ValueSet::transformLabels($rows, [ @@ -31,7 +39,7 @@ class TestMapController extends BaseController { } public function show($id = null) { - $row = $this->model->where('TestMapID',$id)->first(); + $row = $this->model->where('TestMapID',$id)->where('EndDate IS NULL')->first(); if (empty($row)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); } $row = ValueSet::transformLabels([$row], [ @@ -39,6 +47,9 @@ class TestMapController extends BaseController { 'ClientType' => 'entity_type', ])[0]; + // Include testmapdetail records + $row['details'] = $this->modelDetail->getDetailsByTestMap($id); + return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200); } @@ -75,7 +86,19 @@ class TestMapController extends BaseController { $row = $this->model->where('TestMapID', $id)->where('EndDate IS NULL')->first(); if (empty($row)) { return $this->respond([ 'status' => 'failed', 'message' => "Data not found or already deleted.", 'data' => null ], 404); } + $this->db->transStart(); + + // Soft delete the testmap $this->model->update($id, ['EndDate' => date('Y-m-d H:i:s')]); + + // Soft delete all related details + $this->modelDetail->where('TestMapID', $id) + ->where('EndDate IS NULL') + ->set('EndDate', date('Y-m-d H:i:s')) + ->update(); + + $this->db->transComplete(); + return $this->respond([ 'status' => 'success', 'message' => "data deleted successfully", 'data' => $id ], 200); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); @@ -93,35 +116,102 @@ class TestMapController extends BaseController { 'ClientType' => 'entity_type', ]); - return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200); - } - - public function showByHost($hostType = null, $hostID = null) { - if (!$hostType || !$hostID) { return $this->failValidationErrors('HostType and HostID are required.'); } - - $rows = $this->model->getMappingsByHost($hostType, $hostID); - if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); } - - $rows = ValueSet::transformLabels($rows, [ - 'HostType' => 'entity_type', - 'ClientType' => 'entity_type', - ]); + // Include testmapdetail records for each mapping + foreach ($rows as &$row) { + $row['details'] = $this->modelDetail->getDetailsByTestMap($row['TestMapID']); + } return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200); } - public function showByClient($clientType = null, $clientID = null) { - if (!$clientType || !$clientID) { return $this->failValidationErrors('ClientType and ClientID are required.'); } + public function batchCreate() { + $items = $this->request->getJSON(true); + if (!is_array($items)) { return $this->failValidationErrors('Expected array of items'); } - $rows = $this->model->getMappingsByClient($clientType, $clientID); - if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); } + $results = ['success' => [], 'failed' => []]; + $this->db->transStart(); + + foreach ($items as $index => $item) { + if (!$this->validateData($item, $this->rules)) { + $results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()]; + continue; + } + try { + $id = $this->model->insert($item); + $results['success'][] = ['index' => $index, 'TestMapID' => $id]; + } catch (\Exception $e) { + $results['failed'][] = ['index' => $index, 'error' => $e->getMessage()]; + } + } + + $this->db->transComplete(); + return $this->respond([ + 'status' => empty($results['failed']) ? 'success' : 'partial', + 'message' => 'Batch create completed', + 'data' => $results + ], 200); + } - $rows = ValueSet::transformLabels($rows, [ - 'HostType' => 'entity_type', - 'ClientType' => 'entity_type', - ]); + public function batchUpdate() { + $items = $this->request->getJSON(true); + if (!is_array($items)) { return $this->failValidationErrors('Expected array of items'); } + + $results = ['success' => [], 'failed' => []]; + $this->db->transStart(); + + foreach ($items as $index => $item) { + $id = $item['TestMapID'] ?? null; + if (!$id) { + $results['failed'][] = ['index' => $index, 'error' => 'TestMapID required']; + continue; + } + if (!$this->validateData($item, $this->rules)) { + $results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()]; + continue; + } + try { + $this->model->update($id, $item); + $results['success'][] = ['index' => $index, 'TestMapID' => $id]; + } catch (\Exception $e) { + $results['failed'][] = ['index' => $index, 'error' => $e->getMessage()]; + } + } + + $this->db->transComplete(); + return $this->respond([ + 'status' => empty($results['failed']) ? 'success' : 'partial', + 'message' => 'Batch update completed', + 'data' => $results + ], 200); + } - return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200); + public function batchDelete() { + $ids = $this->request->getJSON(true); + if (!is_array($ids)) { return $this->failValidationErrors('Expected array of TestMapIDs'); } + + $results = ['success' => [], 'failed' => []]; + $this->db->transStart(); + + foreach ($ids as $id) { + try { + $row = $this->model->where('TestMapID', $id)->where('EndDate IS NULL')->first(); + if (empty($row)) { + $results['failed'][] = ['TestMapID' => $id, 'error' => 'Not found or already deleted']; + continue; + } + $this->model->update($id, ['EndDate' => date('Y-m-d H:i:s')]); + $results['success'][] = $id; + } catch (\Exception $e) { + $results['failed'][] = ['TestMapID' => $id, 'error' => $e->getMessage()]; + } + } + + $this->db->transComplete(); + return $this->respond([ + 'status' => empty($results['failed']) ? 'success' : 'partial', + 'message' => 'Batch delete completed', + 'data' => $results + ], 200); } } diff --git a/app/Controllers/Test/TestMapDetailController.php b/app/Controllers/Test/TestMapDetailController.php new file mode 100644 index 0000000..4400018 --- /dev/null +++ b/app/Controllers/Test/TestMapDetailController.php @@ -0,0 +1,253 @@ +db = \Config\Database::connect(); + $this->model = new TestMapDetailModel; + $this->rules = [ + 'TestMapID' => 'required|integer', + 'HostTestCode' => 'permit_empty|max_length[10]', + 'HostTestName' => 'permit_empty|max_length[100]', + 'ConDefID' => 'permit_empty|integer', + 'ClientTestCode' => 'permit_empty|max_length[10]', + 'ClientTestName' => 'permit_empty|max_length[100]', + ]; + } + + public function index() { + $testMapID = $this->request->getGet('TestMapID'); + + if ($testMapID) { + $rows = $this->model->getDetailsByTestMap($testMapID); + } else { + $rows = $this->model->where('EndDate IS NULL')->findAll(); + } + + if (empty($rows)) { + return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); + } + + return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200); + } + + public function show($id = null) { + if (!$id) { + return $this->failValidationErrors('TestMapDetailID is required.'); + } + + $row = $this->model->where('TestMapDetailID', $id)->where('EndDate IS NULL')->first(); + + if (empty($row)) { + return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); + } + + return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200); + } + + public function showByTestMap($testMapID = null) { + if (!$testMapID) { + return $this->failValidationErrors('TestMapID is required.'); + } + + $rows = $this->model->getDetailsByTestMap($testMapID); + + if (empty($rows)) { + return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); + } + + return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200); + } + + public function create() { + $input = $this->request->getJSON(true); + + if (!$this->validateData($input, $this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + + try { + $id = $this->model->insert($input); + return $this->respondCreated([ + 'status' => 'success', + 'message' => "data created successfully", + 'data' => $id + ]); + } catch (\Exception $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + + public function update() { + $input = $this->request->getJSON(true); + $id = $input["TestMapDetailID"] ?? null; + + if (!$id) { + return $this->failValidationErrors('TestMapDetailID is required.'); + } + + if (!$this->validateData($input, $this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + + try { + $this->model->update($id, $input); + return $this->respond([ + 'status' => 'success', + 'message' => "data updated successfully", + 'data' => $id + ], 200); + } catch (\Exception $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + + public function delete() { + $input = $this->request->getJSON(true); + $id = $input["TestMapDetailID"] ?? null; + + if (!$id) { + return $this->failValidationErrors('TestMapDetailID is required.'); + } + + try { + $row = $this->model->where('TestMapDetailID', $id)->where('EndDate IS NULL')->first(); + + if (empty($row)) { + return $this->respond([ + 'status' => 'failed', + 'message' => "Data not found or already deleted.", + 'data' => null + ], 404); + } + + $this->model->update($id, ['EndDate' => date('Y-m-d H:i:s')]); + + return $this->respond([ + 'status' => 'success', + 'message' => "data deleted successfully", + 'data' => $id + ], 200); + } catch (\Exception $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + + public function batchCreate() { + $items = $this->request->getJSON(true); + + if (!is_array($items)) { + return $this->failValidationErrors('Expected array of items'); + } + + $results = ['success' => [], 'failed' => []]; + $this->db->transStart(); + + foreach ($items as $index => $item) { + if (!$this->validateData($item, $this->rules)) { + $results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()]; + continue; + } + try { + $id = $this->model->insert($item); + $results['success'][] = ['index' => $index, 'TestMapDetailID' => $id]; + } catch (\Exception $e) { + $results['failed'][] = ['index' => $index, 'error' => $e->getMessage()]; + } + } + + $this->db->transComplete(); + + return $this->respond([ + 'status' => empty($results['failed']) ? 'success' : 'partial', + 'message' => 'Batch create completed', + 'data' => $results + ], 200); + } + + public function batchUpdate() { + $items = $this->request->getJSON(true); + + if (!is_array($items)) { + return $this->failValidationErrors('Expected array of items'); + } + + $results = ['success' => [], 'failed' => []]; + $this->db->transStart(); + + foreach ($items as $index => $item) { + $id = $item['TestMapDetailID'] ?? null; + + if (!$id) { + $results['failed'][] = ['index' => $index, 'error' => 'TestMapDetailID required']; + continue; + } + + if (!$this->validateData($item, $this->rules)) { + $results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()]; + continue; + } + + try { + $this->model->update($id, $item); + $results['success'][] = ['index' => $index, 'TestMapDetailID' => $id]; + } catch (\Exception $e) { + $results['failed'][] = ['index' => $index, 'error' => $e->getMessage()]; + } + } + + $this->db->transComplete(); + + return $this->respond([ + 'status' => empty($results['failed']) ? 'success' : 'partial', + 'message' => 'Batch update completed', + 'data' => $results + ], 200); + } + + public function batchDelete() { + $ids = $this->request->getJSON(true); + + if (!is_array($ids)) { + return $this->failValidationErrors('Expected array of TestMapDetailIDs'); + } + + $results = ['success' => [], 'failed' => []]; + $this->db->transStart(); + + foreach ($ids as $id) { + try { + $row = $this->model->where('TestMapDetailID', $id)->where('EndDate IS NULL')->first(); + + if (empty($row)) { + $results['failed'][] = ['TestMapDetailID' => $id, 'error' => 'Not found or already deleted']; + continue; + } + + $this->model->update($id, ['EndDate' => date('Y-m-d H:i:s')]); + $results['success'][] = $id; + } catch (\Exception $e) { + $results['failed'][] = ['TestMapDetailID' => $id, 'error' => $e->getMessage()]; + } + } + + $this->db->transComplete(); + + return $this->respond([ + 'status' => empty($results['failed']) ? 'success' : 'partial', + 'message' => 'Batch delete completed', + 'data' => $results + ], 200); + } +} diff --git a/app/Controllers/TestsController.php b/app/Controllers/TestsController.php index 4a6d259..e8b6b18 100644 --- a/app/Controllers/TestsController.php +++ b/app/Controllers/TestsController.php @@ -17,6 +17,7 @@ class TestsController extends BaseController protected $modelGrp; protected $modelMap; + protected $modelMapDetail; protected $modelRefNum; protected $modelRefTxt; @@ -28,6 +29,7 @@ class TestsController extends BaseController $this->modelGrp = new \App\Models\Test\TestDefGrpModel; $this->modelMap = new \App\Models\Test\TestMapModel; + $this->modelMapDetail = new \App\Models\Test\TestMapDetailModel; $this->modelRefNum = new \App\Models\RefRange\RefNumModel; $this->modelRefTxt = new \App\Models\RefRange\RefTxtModel; @@ -638,26 +640,52 @@ class TestsController extends BaseController private function saveTestMap($testSiteID, $mappings, $action) { if ($action === 'update') { + // Soft delete existing testmaps and their details for this test site + $existingMaps = $this->modelMap->where('TestSiteID', $testSiteID) + ->where('EndDate IS NULL') + ->findAll(); + + foreach ($existingMaps as $existingMap) { + // Soft delete details first + $this->modelMapDetail->where('TestMapID', $existingMap['TestMapID']) + ->where('EndDate IS NULL') + ->set('EndDate', date('Y-m-d H:i:s')) + ->update(); + } + + // Soft delete the testmap headers $this->db->table('testmap') ->where('TestSiteID', $testSiteID) + ->where('EndDate IS NULL') ->update(['EndDate' => date('Y-m-d H:i:s')]); } if (is_array($mappings)) { foreach ($mappings as $map) { + // Create testmap header $mapData = [ 'TestSiteID' => $testSiteID, 'HostType' => $map['HostType'] ?? null, 'HostID' => $map['HostID'] ?? null, - 'HostTestCode' => $map['HostTestCode'] ?? null, - 'HostTestName' => $map['HostTestName'] ?? null, 'ClientType' => $map['ClientType'] ?? null, 'ClientID' => $map['ClientID'] ?? null, - 'ConDefID' => $map['ConDefID'] ?? null, - 'ClientTestCode' => $map['ClientTestCode'] ?? null, - 'ClientTestName' => $map['ClientTestName'] ?? null ]; - $this->modelMap->insert($mapData); + $testMapID = $this->modelMap->insert($mapData); + + // Create testmapdetail records if details are provided + if ($testMapID && isset($map['details']) && is_array($map['details'])) { + foreach ($map['details'] as $detail) { + $detailData = [ + 'TestMapID' => $testMapID, + 'HostTestCode' => $detail['HostTestCode'] ?? null, + 'HostTestName' => $detail['HostTestName'] ?? null, + 'ConDefID' => $detail['ConDefID'] ?? null, + 'ClientTestCode' => $detail['ClientTestCode'] ?? null, + 'ClientTestName' => $detail['ClientTestName'] ?? null, + ]; + $this->modelMapDetail->insert($detailData); + } + } } } } diff --git a/app/Database/Migrations/2026-01-01-000004_CreateTestDefinitions.php b/app/Database/Migrations/2026-01-01-000004_CreateTestDefinitions.php index 6c94c11..21cd319 100644 --- a/app/Database/Migrations/2026-01-01-000004_CreateTestDefinitions.php +++ b/app/Database/Migrations/2026-01-01-000004_CreateTestDefinitions.php @@ -77,25 +77,6 @@ class CreateTestDefinitions extends Migration { $this->forge->addForeignKey('Member', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE'); $this->forge->createTable('testdefgrp'); - $this->forge->addField([ - 'TestMapID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true], - 'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false], - 'HostType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true], - 'HostID' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true], - 'HostTestCode' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true], - 'HostTestName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true], - 'ClientType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true], - 'ClientID' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true], - 'ConDefID' => ['type' => 'INT', 'null' => true], - 'ClientTestCode' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true], - 'ClientTestName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true], - 'CreateDate' => ['type' => 'Datetime', 'null' => true], - 'EndDate' => ['type' => 'Datetime', 'null' => true] - ]); - $this->forge->addKey('TestMapID', true); - $this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE'); - $this->forge->createTable('testmap'); - $this->forge->addField([ 'RefNumID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true], 'SiteID' => ['type' => 'INT', 'null' => true], @@ -149,16 +130,42 @@ class CreateTestDefinitions extends Migration { ]); $this->forge->addKey('FlagDefID', true); $this->forge->createTable('flagdef'); + + $this->forge->addField([ + 'TestMapID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true], + 'HostType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true], + 'HostID' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true], + 'ClientType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true], + 'ClientID' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true], + 'CreateDate' => ['type' => 'Datetime', 'null' => true], + 'EndDate' => ['type' => 'Datetime', 'null' => true] + ]); + $this->forge->addKey('TestMapID', true); + $this->forge->createTable('testmap'); + + $this->forge->addField([ + 'TestMapDetailID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true], + 'TestMapID' => ['type' => 'INT'], + 'HostTestCode' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true], + 'HostTestName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true], + 'ConDefID' => ['type' => 'INT', 'null' => true], + 'ClientTestCode' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true], + 'ClientTestName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true], + 'CreateDate' => ['type' => 'Datetime', 'null' => true], + 'EndDate' => ['type' => 'Datetime', 'null' => true] + ]); + $this->forge->addKey('TestMapDetailID', true); + $this->forge->createTable('testmapdetail'); } public function down() { + $this->forge->dropTable('testmapdetail'); + $this->forge->dropTable('testmap'); $this->forge->dropTable('flagdef'); $this->forge->dropTable('reftxt'); $this->forge->dropTable('refnum'); - $this->forge->dropTable('testmap'); $this->forge->dropTable('testdefgrp'); $this->forge->dropTable('testdefcal'); - // testdeftech table removed - merged into testdefsite $this->forge->dropTable('testdefsite'); } } diff --git a/app/Database/Migrations/2026-01-01-000012_CreatePatientVisits.php b/app/Database/Migrations/2026-01-01-000012_CreatePatientVisits.php index 981cbbb..ed13bbe 100644 --- a/app/Database/Migrations/2026-01-01-000012_CreatePatientVisits.php +++ b/app/Database/Migrations/2026-01-01-000012_CreatePatientVisits.php @@ -12,6 +12,7 @@ class CreatePatientVisits extends Migration { 'PVID' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => true], 'InternalPID' => ['type' => 'INT', 'constraint' => 11, 'null' => true], 'EpisodeID' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'LastVisitADT' => ['type' => 'VARCHAR', 'constraint' => 5, 'null' => true], 'CreateDate' => ['type' => 'DATETIME', 'null' => true], 'EndDate' => ['type' => 'DATETIME', 'null' => true], 'ArchivedDate'=> ['type' => 'DATETIME', 'null' => true], diff --git a/app/Database/Migrations/2026-02-26-090320_AddTestSiteIDToTestmap.php b/app/Database/Migrations/2026-02-26-090320_AddTestSiteIDToTestmap.php new file mode 100644 index 0000000..b0de162 --- /dev/null +++ b/app/Database/Migrations/2026-02-26-090320_AddTestSiteIDToTestmap.php @@ -0,0 +1,29 @@ +forge->addColumn('testmap', [ + 'TestSiteID' => [ + 'type' => 'INT', + 'unsigned' => true, + 'null' => true, + 'after' => 'TestMapID' + ] + ]); + + // Add foreign key if it doesn't exist + $this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE'); + } + + public function down() + { + $this->forge->dropForeignKey('testmap', 'testmap_TestSiteID_foreign'); + $this->forge->dropColumn('testmap', 'TestSiteID'); + } +} diff --git a/app/Database/Seeds/TestSeeder.php b/app/Database/Seeds/TestSeeder.php index 9dbedde..1e99bb8 100644 --- a/app/Database/Seeds/TestSeeder.php +++ b/app/Database/Seeds/TestSeeder.php @@ -24,6 +24,26 @@ class TestSeeder extends Seeder $now = date('Y-m-d H:i:s'); $tIDs = []; + $testMapHeaders = []; // Store TestMapID by mapping key + $testMapDetails = []; // Store detail records + + // Helper to get or create TestMap header ID + $getTestMapID = function($testSiteID, $hostType, $hostID, $clientType, $clientID) use (&$testMapHeaders, &$now) { + $key = "{$hostType}_{$hostID}_{$clientType}_{$clientID}"; + if (!isset($testMapHeaders[$key])) { + $data = [ + 'TestSiteID' => $testSiteID, + 'HostType' => $hostType, + 'HostID' => $hostID, + 'ClientType' => $clientType, + 'ClientID' => $clientID, + 'CreateDate' => $now + ]; + $this->db->table('testmap')->insert($data); + $testMapHeaders[$key] = $this->db->insertID(); + } + return $testMapHeaders[$key]; + }; // ======================================== // TEST TYPE - Actual Laboratory Tests @@ -207,6 +227,7 @@ class TestSeeder extends Seeder // ======================================== // TEST MAP - Specimen Mapping // Maps tests to workstations and containers + // New structure: testmap (header) + testmapdetail (details) // ======================================== // Get container IDs (inserted by SpecimenSeeder in order) @@ -223,291 +244,109 @@ class TestSeeder extends Seeder $wsIAuto = 6; // Imunologi Auto $wsUAuto = 8; // Urin Auto - $testMaps = []; - - // ============================================ - // SITE → WORKSTATION mappings (no ConDefID) - // ============================================ - - // Hematology tests → HAUTO workstation - $hematologyTests = ['HB', 'HCT', 'RBC', 'WBC', 'PLT', 'MCV', 'MCH', 'MCHC']; - foreach ($hematologyTests as $testCode) { - $testMaps[] = [ - 'TestSiteID' => $tIDs[$testCode], - 'HostType' => 'SITE', - 'HostID' => '1', - 'HostTestCode' => $testCode, - 'HostTestName' => $testCode, - 'ClientType' => 'WORKSTATION', - 'ClientID' => (string)$wsHAuto, - 'ConDefID' => null, - 'ClientTestCode' => $testCode, - 'ClientTestName' => $testCode, - 'CreateDate' => $now - ]; - } - - // Chemistry tests → CAUTO workstation - $chemistryTests = ['GLU', 'CREA', 'UREA', 'SGOT', 'SGPT', 'CHOL', 'TG', 'HDL', 'LDL']; - foreach ($chemistryTests as $testCode) { - $testMaps[] = [ - 'TestSiteID' => $tIDs[$testCode], - 'HostType' => 'SITE', - 'HostID' => '1', - 'HostTestCode' => $testCode, - 'HostTestName' => $testCode, - 'ClientType' => 'WORKSTATION', - 'ClientID' => (string)$wsCAuto, - 'ConDefID' => null, - 'ClientTestCode' => $testCode, - 'ClientTestName' => $testCode, - 'CreateDate' => $now - ]; - } - - // Calculated chemistry tests → CAUTO workstation - $calcChemistryTests = ['EGFR', 'LDLCALC']; - foreach ($calcChemistryTests as $testCode) { - $testMaps[] = [ - 'TestSiteID' => $tIDs[$testCode], - 'HostType' => 'SITE', - 'HostID' => '1', - 'HostTestCode' => $testCode, - 'HostTestName' => $testCode, - 'ClientType' => 'WORKSTATION', - 'ClientID' => (string)$wsCAuto, - 'ConDefID' => null, - 'ClientTestCode' => $testCode, - 'ClientTestName' => $testCode, - 'CreateDate' => $now - ]; - } - - // Urinalysis tests → UAUTO workstation - $urineTests = ['UCOLOR', 'UGLUC', 'UPROT', 'PH']; - foreach ($urineTests as $testCode) { - $testMaps[] = [ - 'TestSiteID' => $tIDs[$testCode], - 'HostType' => 'SITE', - 'HostID' => '1', - 'HostTestCode' => $testCode, - 'HostTestName' => $testCode, - 'ClientType' => 'WORKSTATION', - 'ClientID' => (string)$wsUAuto, - 'ConDefID' => null, - 'ClientTestCode' => $testCode, - 'ClientTestName' => $testCode, - 'CreateDate' => $now - ]; - } - - // Panel/Group tests - $testMaps[] = [ - 'TestSiteID' => $tIDs['CBC'], - 'HostType' => 'SITE', - 'HostID' => '1', - 'HostTestCode' => 'CBC', - 'HostTestName' => 'CBC', - 'ClientType' => 'WORKSTATION', - 'ClientID' => (string)$wsHAuto, - 'ConDefID' => null, - 'ClientTestCode' => 'CBC', - 'ClientTestName' => 'CBC', - 'CreateDate' => $now - ]; - - $testMaps[] = [ - 'TestSiteID' => $tIDs['LIPID'], - 'HostType' => 'SITE', - 'HostID' => '1', - 'HostTestCode' => 'LIPID', - 'HostTestName' => 'LIPID', - 'ClientType' => 'WORKSTATION', - 'ClientID' => (string)$wsCAuto, - 'ConDefID' => null, - 'ClientTestCode' => 'LIPID', - 'ClientTestName' => 'LIPID', - 'CreateDate' => $now - ]; - - $testMaps[] = [ - 'TestSiteID' => $tIDs['LFT'], - 'HostType' => 'SITE', - 'HostID' => '1', - 'HostTestCode' => 'LFT', - 'HostTestName' => 'LFT', - 'ClientType' => 'WORKSTATION', - 'ClientID' => (string)$wsCAuto, - 'ConDefID' => null, - 'ClientTestCode' => 'LFT', - 'ClientTestName' => 'LFT', - 'CreateDate' => $now - ]; - - $testMaps[] = [ - 'TestSiteID' => $tIDs['RFT'], - 'HostType' => 'SITE', - 'HostID' => '1', - 'HostTestCode' => 'RFT', - 'HostTestName' => 'RFT', - 'ClientType' => 'WORKSTATION', - 'ClientID' => (string)$wsCAuto, - 'ConDefID' => null, - 'ClientTestCode' => 'RFT', - 'ClientTestName' => 'RFT', - 'CreateDate' => $now - ]; - - // BMI parameter - no container needed (calculated from parameters) - $testMaps[] = [ - 'TestSiteID' => $tIDs['BMI'], - 'HostType' => 'SITE', - 'HostID' => '1', - 'HostTestCode' => 'BMI', - 'HostTestName' => 'BMI', - 'ClientType' => 'WORKSTATION', - 'ClientID' => (string)$wsCAuto, - 'ConDefID' => null, - 'ClientTestCode' => 'BMI', - 'ClientTestName' => 'BMI', - 'CreateDate' => $now - ]; - - // ============================================ - // WORKSTATION → INSTRUMENT mappings (with ConDefID) - // ============================================ - // Equipment IDs from OrganizationSeeder $instHematology = 1; // Sysmex XN-1000 $instChemistry = 2; // Roche Cobas C501 $instImmunology = 3; // Architect i2000 $instUrinalysis = 4; // Urisys 1100 - // Hematology tests → HAUTO → Sysmex (EDTA container) - foreach ($hematologyTests as $testCode) { - $testMaps[] = [ - 'TestSiteID' => $tIDs[$testCode], - 'HostType' => 'WORKSTATION', - 'HostID' => (string)$wsHAuto, - 'HostTestCode' => $testCode, - 'HostTestName' => $testCode, - 'ClientType' => 'INSTRUMENT', - 'ClientID' => (string)$instHematology, - 'ConDefID' => $conEDTA, - 'ClientTestCode' => $testCode, - 'ClientTestName' => $testCode, + // Helper to get or create TestMap header ID (relationship only) + $getTestMapID = function($hostType, $hostID, $clientType, $clientID) use (&$testMapHeaders, &$now) { + $key = "{$hostType}_{$hostID}_{$clientType}_{$clientID}"; + if (!isset($testMapHeaders[$key])) { + $data = [ + 'HostType' => $hostType, + 'HostID' => $hostID, + 'ClientType' => $clientType, + 'ClientID' => $clientID, + 'CreateDate' => $now + ]; + $this->db->table('testmap')->insert($data); + $testMapHeaders[$key] = $this->db->insertID(); + } + return $testMapHeaders[$key]; + }; + + // Helper to add detail record + $addDetail = function($testMapID, $hostTestCode, $hostTestName, $conDefID, $clientTestCode, $clientTestName) use (&$testMapDetails, &$now) { + $testMapDetails[] = [ + 'TestMapID' => $testMapID, + 'HostTestCode' => $hostTestCode, + 'HostTestName' => $hostTestName, + 'ConDefID' => $conDefID, + 'ClientTestCode' => $clientTestCode, + 'ClientTestName' => $clientTestName, 'CreateDate' => $now ]; + }; + + // ============================================ + // TEST MAP CONFIGURATION + // Grouped by discipline for maintainability + // ============================================ + $testMappings = [ + // Hematology: Site → HAUTO → Sysmex (EDTA) + [ + 'tests' => ['HB', 'HCT', 'RBC', 'WBC', 'PLT', 'MCV', 'MCH', 'MCHC'], + 'panels' => ['CBC'], + 'siteToWs' => ['ws' => $wsHAuto, 'con' => null], + 'wsToInst' => ['ws' => $wsHAuto, 'inst' => $instHematology, 'con' => $conEDTA], + ], + // Chemistry: Site → CAUTO → Cobas (SST) + [ + 'tests' => ['GLU', 'CREA', 'UREA', 'SGOT', 'SGPT', 'CHOL', 'TG', 'HDL', 'LDL'], + 'panels' => ['LIPID', 'LFT', 'RFT'], + 'siteToWs' => ['ws' => $wsCAuto, 'con' => null], + 'wsToInst' => ['ws' => $wsCAuto, 'inst' => $instChemistry, 'con' => $conSST], + ], + // Calculated: Site → CAUTO → Cobas (SST) + [ + 'tests' => ['EGFR', 'LDLCALC'], + 'panels' => [], + 'siteToWs' => ['ws' => $wsCAuto, 'con' => null], + 'wsToInst' => ['ws' => $wsCAuto, 'inst' => $instChemistry, 'con' => $conSST], + ], + // Urinalysis: Site → UAUTO → Urisys (Pot Urin) + [ + 'tests' => ['UCOLOR', 'UGLUC', 'UPROT', 'PH'], + 'panels' => [], + 'siteToWs' => ['ws' => $wsUAuto, 'con' => null], + 'wsToInst' => ['ws' => $wsUAuto, 'inst' => $instUrinalysis, 'con' => $conPotUrin], + ], + // BMI: Site → CAUTO (no instrument mapping - calculated) + [ + 'tests' => ['BMI'], + 'panels' => [], + 'siteToWs' => ['ws' => $wsCAuto, 'con' => null], + 'wsToInst' => null, + ], + ]; + + foreach ($testMappings as $mapping) { + // Site → Workstation mapping (one header per workstation) + $testMapSiteWsID = $getTestMapID('SITE', '1', 'WORKSTATION', (string)$mapping['siteToWs']['ws']); + foreach ($mapping['tests'] as $testCode) { + $addDetail($testMapSiteWsID, $testCode, $testCode, $mapping['siteToWs']['con'], $testCode, $testCode); + } + foreach ($mapping['panels'] as $panelCode) { + $addDetail($testMapSiteWsID, $panelCode, $panelCode, $mapping['siteToWs']['con'], $panelCode, $panelCode); + } + + // Workstation → Instrument mapping (one header per instrument) + if ($mapping['wsToInst'] !== null) { + $testMapWsInstID = $getTestMapID('WORKSTATION', (string)$mapping['wsToInst']['ws'], 'INSTRUMENT', (string)$mapping['wsToInst']['inst']); + foreach ($mapping['tests'] as $testCode) { + $addDetail($testMapWsInstID, $testCode, $testCode, $mapping['wsToInst']['con'], $testCode, $testCode); + } + foreach ($mapping['panels'] as $panelCode) { + $addDetail($testMapWsInstID, $panelCode, $panelCode, $mapping['wsToInst']['con'], $panelCode, $panelCode); + } + } } - // Chemistry tests → CAUTO → Cobas (SST container) - foreach ($chemistryTests as $testCode) { - $testMaps[] = [ - 'TestSiteID' => $tIDs[$testCode], - 'HostType' => 'WORKSTATION', - 'HostID' => (string)$wsCAuto, - 'HostTestCode' => $testCode, - 'HostTestName' => $testCode, - 'ClientType' => 'INSTRUMENT', - 'ClientID' => (string)$instChemistry, - 'ConDefID' => $conSST, - 'ClientTestCode' => $testCode, - 'ClientTestName' => $testCode, - 'CreateDate' => $now - ]; + // Insert all testmapdetail records + if (!empty($testMapDetails)) { + $this->db->table('testmapdetail')->insertBatch($testMapDetails); } - - // Calculated chemistry tests → CAUTO → Cobas (SST container) - foreach ($calcChemistryTests as $testCode) { - $testMaps[] = [ - 'TestSiteID' => $tIDs[$testCode], - 'HostType' => 'WORKSTATION', - 'HostID' => (string)$wsCAuto, - 'HostTestCode' => $testCode, - 'HostTestName' => $testCode, - 'ClientType' => 'INSTRUMENT', - 'ClientID' => (string)$instChemistry, - 'ConDefID' => $conSST, - 'ClientTestCode' => $testCode, - 'ClientTestName' => $testCode, - 'CreateDate' => $now - ]; - } - - // Urinalysis tests → UAUTO → Urisys (Pot Urin container) - foreach ($urineTests as $testCode) { - $testMaps[] = [ - 'TestSiteID' => $tIDs[$testCode], - 'HostType' => 'WORKSTATION', - 'HostID' => (string)$wsUAuto, - 'HostTestCode' => $testCode, - 'HostTestName' => $testCode, - 'ClientType' => 'INSTRUMENT', - 'ClientID' => (string)$instUrinalysis, - 'ConDefID' => $conPotUrin, - 'ClientTestCode' => $testCode, - 'ClientTestName' => $testCode, - 'CreateDate' => $now - ]; - } - - // Panel/Group tests → Instruments - $testMaps[] = [ - 'TestSiteID' => $tIDs['CBC'], - 'HostType' => 'WORKSTATION', - 'HostID' => (string)$wsHAuto, - 'HostTestCode' => 'CBC', - 'HostTestName' => 'CBC', - 'ClientType' => 'INSTRUMENT', - 'ClientID' => (string)$instHematology, - 'ConDefID' => $conEDTA, - 'ClientTestCode' => 'CBC', - 'ClientTestName' => 'CBC', - 'CreateDate' => $now - ]; - - $testMaps[] = [ - 'TestSiteID' => $tIDs['LIPID'], - 'HostType' => 'WORKSTATION', - 'HostID' => (string)$wsCAuto, - 'HostTestCode' => 'LIPID', - 'HostTestName' => 'LIPID', - 'ClientType' => 'INSTRUMENT', - 'ClientID' => (string)$instChemistry, - 'ConDefID' => $conSST, - 'ClientTestCode' => 'LIPID', - 'ClientTestName' => 'LIPID', - 'CreateDate' => $now - ]; - - $testMaps[] = [ - 'TestSiteID' => $tIDs['LFT'], - 'HostType' => 'WORKSTATION', - 'HostID' => (string)$wsCAuto, - 'HostTestCode' => 'LFT', - 'HostTestName' => 'LFT', - 'ClientType' => 'INSTRUMENT', - 'ClientID' => (string)$instChemistry, - 'ConDefID' => $conSST, - 'ClientTestCode' => 'LFT', - 'ClientTestName' => 'LFT', - 'CreateDate' => $now - ]; - - $testMaps[] = [ - 'TestSiteID' => $tIDs['RFT'], - 'HostType' => 'WORKSTATION', - 'HostID' => (string)$wsCAuto, - 'HostTestCode' => 'RFT', - 'HostTestName' => 'RFT', - 'ClientType' => 'INSTRUMENT', - 'ClientID' => (string)$instChemistry, - 'ConDefID' => $conSST, - 'ClientTestCode' => 'RFT', - 'ClientTestName' => 'RFT', - 'CreateDate' => $now - ]; - - $this->db->table('testmap')->insertBatch($testMaps); } } diff --git a/app/Models/PatVisit/PatVisitModel.php b/app/Models/PatVisit/PatVisitModel.php index 48a3484..21bcf18 100644 --- a/app/Models/PatVisit/PatVisitModel.php +++ b/app/Models/PatVisit/PatVisitModel.php @@ -9,7 +9,7 @@ use App\Models\PatVisit\PatVisitADTModel; class PatVisitModel extends BaseModel { protected $table = 'patvisit'; protected $primaryKey = 'InternalPVID'; - protected $allowedFields = ['PVID', 'InternalPID', 'EpisodeID', 'CreateDate', 'EndDate', 'ArchivedDate', 'DelDate']; + protected $allowedFields = ['PVID', 'InternalPID', 'EpisodeID', 'LastVisitADT', 'CreateDate', 'EndDate', 'ArchivedDate', 'DelDate']; protected $useTimestamps = true; protected $createdField = 'CreateDate'; diff --git a/app/Models/Test/TestMapDetailModel.php b/app/Models/Test/TestMapDetailModel.php new file mode 100644 index 0000000..94a48f8 --- /dev/null +++ b/app/Models/Test/TestMapDetailModel.php @@ -0,0 +1,53 @@ +where('TestMapID', $testMapID) + ->where('EndDate IS NULL') + ->findAll(); + } + + /** + * Get test map detail by host test code + */ + public function getDetailsByHostCode($hostTestCode) { + return $this->where('HostTestCode', $hostTestCode) + ->where('EndDate IS NULL') + ->findAll(); + } + + /** + * Get test map detail by client test code + */ + public function getDetailsByClientCode($clientTestCode) { + return $this->where('ClientTestCode', $clientTestCode) + ->where('EndDate IS NULL') + ->findAll(); + } +} diff --git a/app/Models/Test/TestMapModel.php b/app/Models/Test/TestMapModel.php index 45f675c..d316bdb 100644 --- a/app/Models/Test/TestMapModel.php +++ b/app/Models/Test/TestMapModel.php @@ -11,23 +11,57 @@ class TestMapModel extends BaseModel { 'TestSiteID', 'HostType', 'HostID', - 'HostTestCode', - 'HostTestName', 'ClientType', 'ClientID', - 'ConDefID', - 'ClientTestCode', - 'ClientTestName', 'CreateDate', 'EndDate' ]; - + protected $useTimestamps = true; protected $createdField = 'CreateDate'; protected $updatedField = ''; protected $useSoftDeletes = true; protected $deletedField = "EndDate"; + /** + * Get all test mappings with names + */ + public function getUniqueGroupings() { + $db = $this->db; + + $sql = " + SELECT + tm.TestMapID, + tm.TestSiteID, + tm.HostType, + tm.HostID, + CASE + WHEN tm.HostType = 'SITE' THEN s.SiteName + WHEN tm.HostType = 'WORKSTATION' THEN ws.WorkstationName + WHEN tm.HostType = 'INSTRUMENT' THEN el.InstrumentName + ELSE tm.HostID + END as HostName, + tm.ClientType, + tm.ClientID, + CASE + WHEN tm.ClientType = 'SITE' THEN cs.SiteName + WHEN tm.ClientType = 'WORKSTATION' THEN cws.WorkstationName + WHEN tm.ClientType = 'INSTRUMENT' THEN cel.InstrumentName + ELSE tm.ClientID + END as ClientName + FROM testmap tm + LEFT JOIN site s ON tm.HostType = 'SITE' AND s.SiteID = tm.HostID + LEFT JOIN workstation ws ON tm.HostType = 'WORKSTATION' AND ws.WorkstationID = tm.HostID + LEFT JOIN equipmentlist el ON tm.HostType = 'INSTRUMENT' AND el.EID = tm.HostID + LEFT JOIN site cs ON tm.ClientType = 'SITE' AND cs.SiteID = tm.ClientID + LEFT JOIN workstation cws ON tm.ClientType = 'WORKSTATION' AND cws.WorkstationID = tm.ClientID + LEFT JOIN equipmentlist cel ON tm.ClientType = 'INSTRUMENT' AND cel.EID = tm.ClientID + WHERE tm.EndDate IS NULL + "; + + return $db->query($sql)->getResultArray(); + } + /** * Get test mappings by test site */ @@ -38,57 +72,14 @@ class TestMapModel extends BaseModel { } /** - * Get test mappings by client (equipment/workstation) + * Get test mappings with details by test site */ - public function getMappingsByClient($clientType, $clientID) { - return $this->where('ClientType', $clientType) - ->where('ClientID', $clientID) - ->where('EndDate IS NULL') + public function getMappingsWithDetailsByTestSite($testSiteID) { + return $this->select('testmap.*, testmapdetail.*') + ->join('testmapdetail', 'testmapdetail.TestMapID = testmap.TestMapID', 'left') + ->where('testmap.TestSiteID', $testSiteID) + ->where('testmap.EndDate IS NULL') + ->where('testmapdetail.EndDate IS NULL') ->findAll(); } - - /** - * Get test mappings by host (site/HIS) - */ - public function getMappingsByHost($hostType, $hostID) { - return $this->where('HostType', $hostType) - ->where('HostID', $hostID) - ->where('EndDate IS NULL') - ->findAll(); - } - - /** - * Get test mapping by client test code and container - */ - public function getMappingByClientCode($clientTestCode, $conDefID = null) { - $builder = $this->where('ClientTestCode', $clientTestCode) - ->where('EndDate IS NULL'); - - if ($conDefID) { - $builder->where('ConDefID', $conDefID); - } - - return $builder->findAll(); - } - - /** - * Get test mapping by host test code - */ - public function getMappingByHostCode($hostTestCode) { - return $this->where('HostTestCode', $hostTestCode) - ->where('EndDate IS NULL') - ->findAll(); - } - - /** - * Check if mapping exists for client and host - */ - public function mappingExists($testSiteID, $clientType, $clientID, $clientTestCode) { - return $this->where('TestSiteID', $testSiteID) - ->where('ClientType', $clientType) - ->where('ClientID', $clientID) - ->where('ClientTestCode', $clientTestCode) - ->where('EndDate IS NULL') - ->countAllResults() > 0; - } } diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index 098ec39..867936f 100644 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -2781,25 +2781,12 @@ paths: get: tags: - Tests - summary: List all test mappings + summary: List all test mappings (unique groupings) security: - bearerAuth: [] - parameters: - - name: page - in: query - schema: - type: integer - default: 1 - description: Page number for pagination - - name: perPage - in: query - schema: - type: integer - default: 20 - description: Number of items per page responses: '200': - description: List of test mappings + description: List of unique test mapping groupings content: application/json: schema: @@ -2813,11 +2800,24 @@ paths: data: type: array items: - $ref: '#/components/schemas/TestMap' + type: object + properties: + HostType: + type: string + HostID: + type: string + HostName: + type: string + ClientType: + type: string + ClientID: + type: string + ClientName: + type: string post: tags: - Tests - summary: Create test mapping + summary: Create test mapping (header only) security: - bearerAuth: [] requestBody: @@ -2836,27 +2836,28 @@ paths: HostID: type: string description: Host identifier - HostTestCode: - type: string - description: Test code in host system - HostTestName: - type: string - description: Test name in host system ClientType: type: string description: Client type code ClientID: type: string description: Client identifier - ConDefID: - type: integer - description: Connection definition ID - ClientTestCode: - type: string - description: Test code in client system - ClientTestName: - type: string - description: Test name in client system + details: + type: array + description: Optional detail records to create + items: + type: object + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string required: - TestSiteID responses: @@ -2897,20 +2898,10 @@ paths: type: string HostID: type: string - HostTestCode: - type: string - HostTestName: - type: string ClientType: type: string ClientID: type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string required: - TestMapID responses: @@ -2932,7 +2923,7 @@ paths: delete: tags: - Tests - summary: Soft delete test mapping + summary: Soft delete test mapping (cascades to details) security: - bearerAuth: [] requestBody: @@ -2969,7 +2960,7 @@ paths: get: tags: - Tests - summary: Get test mapping by ID + summary: Get test mapping by ID with details security: - bearerAuth: [] parameters: @@ -2981,7 +2972,7 @@ paths: description: Test Map ID responses: '200': - description: Test mapping details + description: Test mapping details with nested detail records content: application/json: schema: @@ -2999,7 +2990,7 @@ paths: get: tags: - Tests - summary: Get test mappings by test site + summary: Get test mappings by test site with details security: - bearerAuth: [] parameters: @@ -3011,7 +3002,7 @@ paths: description: Test Site ID responses: '200': - description: List of test mappings for the test site + description: List of test mappings with details for the test site content: application/json: schema: @@ -3025,29 +3016,22 @@ paths: type: array items: $ref: '#/components/schemas/TestMap' - /api/test/testmap/by-host/{hostType}/{hostID}: + /api/test/testmap/detail: get: tags: - Tests - summary: Get test mappings by host + summary: List test mapping details security: - bearerAuth: [] parameters: - - name: hostType - in: path - required: true + - name: TestMapID + in: query schema: - type: string - description: Host type code - - name: hostID - in: path - required: true - schema: - type: string - description: Host identifier + type: integer + description: Filter by TestMapID responses: '200': - description: List of test mappings for the host + description: List of test mapping details content: application/json: schema: @@ -3060,30 +3044,149 @@ paths: data: type: array items: - $ref: '#/components/schemas/TestMap' - /api/test/testmap/by-client/{clientType}/{clientID}: + $ref: '#/components/schemas/TestMapDetail' + post: + tags: + - Tests + summary: Create test mapping detail + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapID: + type: integer + description: Test Map ID (required) + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + required: + - TestMapID + responses: + '201': + description: Test mapping detail created + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: integer + description: Created TestMapDetailID + patch: + tags: + - Tests + summary: Update test mapping detail + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapDetailID: + type: integer + description: Test Map Detail ID (required) + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + required: + - TestMapDetailID + responses: + '200': + description: Test mapping detail updated + delete: + tags: + - Tests + summary: Soft delete test mapping detail + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapDetailID: + type: integer + description: Test Map Detail ID to delete (required) + required: + - TestMapDetailID + responses: + '200': + description: Test mapping detail deleted + /api/test/testmap/detail/{id}: get: tags: - Tests - summary: Get test mappings by client + summary: Get test mapping detail by ID security: - bearerAuth: [] parameters: - - name: clientType + - name: id in: path required: true schema: - type: string - description: Client type code - - name: clientID - in: path - required: true - schema: - type: string - description: Client identifier + type: integer + description: Test Map Detail ID responses: '200': - description: List of test mappings for the client + description: Test mapping detail + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/TestMapDetail' + /api/test/testmap/detail/by-testmap/{testMapID}: + get: + tags: + - Tests + summary: Get test mapping details by test map ID + security: + - bearerAuth: [] + parameters: + - name: testMapID + in: path + required: true + schema: + type: integer + description: Test Map ID + responses: + '200': + description: List of test mapping details content: application/json: schema: @@ -3096,7 +3199,88 @@ paths: data: type: array items: - $ref: '#/components/schemas/TestMap' + $ref: '#/components/schemas/TestMapDetail' + /api/test/testmap/detail/batch: + post: + tags: + - Tests + summary: Batch create test mapping details + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Batch create results + patch: + tags: + - Tests + summary: Batch update test mapping details + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + TestMapDetailID: + type: integer + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Batch update results + delete: + tags: + - Tests + summary: Batch delete test mapping details + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: integer + description: TestMapDetailIDs to delete + responses: + '200': + description: Batch delete results /api/tests: get: tags: @@ -5104,31 +5288,27 @@ components: type: integer HostType: type: string - description: Host type code + description: Host type code (e.g., SITE, WORKSTATION, INSTRUMENT) HostID: type: string description: Host identifier - HostTestCode: - type: string - description: Test code in host system - HostTestName: - type: string - description: Test name in host system ClientType: type: string - description: Client type code + description: Client type code (e.g., SITE, WORKSTATION, INSTRUMENT) ClientID: type: string description: Client identifier - ConDefID: - type: integer - description: Connection definition ID - ClientTestCode: + HostName: type: string - description: Test code in client system - ClientTestName: + description: Resolved host name (from view) + ClientName: type: string - description: Test name in client system + description: Resolved client name (from view) + details: + type: array + description: Test mapping detail records + items: + $ref: '#/components/schemas/TestMapDetail' CreateDate: type: string format: date-time @@ -5485,6 +5665,35 @@ components: type: string format: date-time description: Occupation display text + TestMapDetail: + type: object + properties: + TestMapDetailID: + type: integer + TestMapID: + type: integer + HostTestCode: + type: string + description: Test code in host system + HostTestName: + type: string + description: Test name in host system + ConDefID: + type: integer + description: Container definition ID + ClientTestCode: + type: string + description: Test code in client system + ClientTestName: + type: string + description: Test name in client system + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + description: Soft delete timestamp ValueSetListItem: type: object description: Library/system value set summary (from JSON files) diff --git a/public/components/schemas/tests.yaml b/public/components/schemas/tests.yaml index f249fcc..6f45bb6 100644 --- a/public/components/schemas/tests.yaml +++ b/public/components/schemas/tests.yaml @@ -430,25 +430,51 @@ TestMap: type: integer HostType: type: string - description: Host type code + description: Host type code (e.g., SITE, WORKSTATION, INSTRUMENT) HostID: type: string description: Host identifier + ClientType: + type: string + description: Client type code (e.g., SITE, WORKSTATION, INSTRUMENT) + ClientID: + type: string + description: Client identifier + HostName: + type: string + description: Resolved host name (from view) + ClientName: + type: string + description: Resolved client name (from view) + details: + type: array + description: Test mapping detail records + items: + $ref: '#/TestMapDetail' + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + description: Soft delete timestamp + +TestMapDetail: + type: object + properties: + TestMapDetailID: + type: integer + TestMapID: + type: integer HostTestCode: type: string description: Test code in host system HostTestName: type: string description: Test name in host system - ClientType: - type: string - description: Client type code - ClientID: - type: string - description: Client identifier ConDefID: type: integer - description: Connection definition ID + description: Container definition ID ClientTestCode: type: string description: Test code in client system diff --git a/public/paths/testmap.yaml b/public/paths/testmap.yaml index e2376e8..632e467 100644 --- a/public/paths/testmap.yaml +++ b/public/paths/testmap.yaml @@ -4,19 +4,6 @@ summary: List all test mappings security: - bearerAuth: [] - parameters: - - name: page - in: query - schema: - type: integer - default: 1 - description: Page number for pagination - - name: perPage - in: query - schema: - type: integer - default: 20 - description: Number of items per page responses: '200': description: List of test mappings @@ -33,11 +20,28 @@ data: type: array items: - $ref: '../components/schemas/tests.yaml#/TestMap' + type: object + properties: + TestMapID: + type: integer + TestSiteID: + type: integer + HostType: + type: string + HostID: + type: string + HostName: + type: string + ClientType: + type: string + ClientID: + type: string + ClientName: + type: string post: tags: [Tests] - summary: Create test mapping + summary: Create test mapping (header only) security: - bearerAuth: [] requestBody: @@ -56,27 +60,28 @@ HostID: type: string description: Host identifier - HostTestCode: - type: string - description: Test code in host system - HostTestName: - type: string - description: Test name in host system ClientType: type: string description: Client type code ClientID: type: string description: Client identifier - ConDefID: - type: integer - description: Connection definition ID - ClientTestCode: - type: string - description: Test code in client system - ClientTestName: - type: string - description: Test name in client system + details: + type: array + description: Optional detail records to create + items: + type: object + properties: + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string required: - TestSiteID responses: @@ -117,20 +122,10 @@ type: string HostID: type: string - HostTestCode: - type: string - HostTestName: - type: string ClientType: type: string ClientID: type: string - ConDefID: - type: integer - ClientTestCode: - type: string - ClientTestName: - type: string required: - TestMapID responses: @@ -152,7 +147,7 @@ delete: tags: [Tests] - summary: Soft delete test mapping + summary: Soft delete test mapping (cascades to details) security: - bearerAuth: [] requestBody: @@ -189,7 +184,7 @@ /api/test/testmap/{id}: get: tags: [Tests] - summary: Get test mapping by ID + summary: Get test mapping by ID with details security: - bearerAuth: [] parameters: @@ -201,7 +196,7 @@ description: Test Map ID responses: '200': - description: Test mapping details + description: Test mapping details with nested detail records content: application/json: schema: @@ -219,7 +214,7 @@ /api/test/testmap/by-testsite/{testSiteID}: get: tags: [Tests] - summary: Get test mappings by test site + summary: Get test mappings by test site with details security: - bearerAuth: [] parameters: @@ -231,7 +226,7 @@ description: Test Site ID responses: '200': - description: List of test mappings for the test site + description: List of test mappings with details for the test site content: application/json: schema: @@ -246,28 +241,21 @@ items: $ref: '../components/schemas/tests.yaml#/TestMap' -/api/test/testmap/by-host/{hostType}/{hostID}: +/api/test/testmap/detail: get: tags: [Tests] - summary: Get test mappings by host + summary: List test mapping details security: - bearerAuth: [] parameters: - - name: hostType - in: path - required: true + - name: TestMapID + in: query schema: - type: string - description: Host type code - - name: hostID - in: path - required: true - schema: - type: string - description: Host identifier + type: integer + description: Filter by TestMapID responses: '200': - description: List of test mappings for the host + description: List of test mapping details content: application/json: schema: @@ -280,30 +268,149 @@ data: type: array items: - $ref: '../components/schemas/tests.yaml#/TestMap' + $ref: '../components/schemas/tests.yaml#/TestMapDetail' -/api/test/testmap/by-client/{clientType}/{clientID}: + post: + tags: [Tests] + summary: Create test mapping detail + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapID: + type: integer + description: Test Map ID (required) + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + required: + - TestMapID + responses: + '201': + description: Test mapping detail created + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + type: integer + description: Created TestMapDetailID + + patch: + tags: [Tests] + summary: Update test mapping detail + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapDetailID: + type: integer + description: Test Map Detail ID (required) + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + required: + - TestMapDetailID + responses: + '200': + description: Test mapping detail updated + + delete: + tags: [Tests] + summary: Soft delete test mapping detail + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + TestMapDetailID: + type: integer + description: Test Map Detail ID to delete (required) + required: + - TestMapDetailID + responses: + '200': + description: Test mapping detail deleted + +/api/test/testmap/detail/{id}: get: tags: [Tests] - summary: Get test mappings by client + summary: Get test mapping detail by ID security: - bearerAuth: [] parameters: - - name: clientType + - name: id in: path required: true schema: - type: string - description: Client type code - - name: clientID - in: path - required: true - schema: - type: string - description: Client identifier + type: integer + description: Test Map Detail ID responses: '200': - description: List of test mappings for the client + description: Test mapping detail + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/tests.yaml#/TestMapDetail' + +/api/test/testmap/detail/by-testmap/{testMapID}: + get: + tags: [Tests] + summary: Get test mapping details by test map ID + security: + - bearerAuth: [] + parameters: + - name: testMapID + in: path + required: true + schema: + type: integer + description: Test Map ID + responses: + '200': + description: List of test mapping details content: application/json: schema: @@ -316,4 +423,85 @@ data: type: array items: - $ref: '../components/schemas/tests.yaml#/TestMap' + $ref: '../components/schemas/tests.yaml#/TestMapDetail' + +/api/test/testmap/detail/batch: + post: + tags: [Tests] + summary: Batch create test mapping details + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Batch create results + + patch: + tags: [Tests] + summary: Batch update test mapping details + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: object + properties: + TestMapDetailID: + type: integer + TestMapID: + type: integer + HostTestCode: + type: string + HostTestName: + type: string + ConDefID: + type: integer + ClientTestCode: + type: string + ClientTestName: + type: string + responses: + '200': + description: Batch update results + + delete: + tags: [Tests] + summary: Batch delete test mapping details + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: integer + description: TestMapDetailIDs to delete + responses: + '200': + description: Batch delete results