diff --git a/.gitignore b/.gitignore index 0dd1610..8912ef5 100644 --- a/.gitignore +++ b/.gitignore @@ -126,5 +126,4 @@ _modules/* /phpunit*.xml /public/.htaccess -/.serena -AGENTS.md \ No newline at end of file +/.serena \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3d2001e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,328 @@ +# AGENTS.md - Code Guidelines for CLQMS + +> **CLQMS (Clinical Laboratory Quality Management System)** - A headless REST API backend for clinical laboratory workflows built with CodeIgniter 4. + +--- + +## Build, Test & Lint Commands + +### Running Tests +```bash +# Run all tests +./vendor/bin/phpunit + +# Run a specific test file +./vendor/bin/phpunit tests/feature/Patients/PatientCreateTest.php + +# Run a specific test method +./vendor/bin/phpunit --filter testCreatePatientSuccess tests/feature/Patients/PatientCreateTest.php + +# Run tests with coverage +./vendor/bin/phpunit --coverage-html build/logs/html + +# Run tests by suite +./vendor/bin/phpunit --testsuite App +``` + +### CodeIgniter CLI Commands +```bash +# Run spark commands +php spark + +# Generate migration +php spark make:migration + +# Generate model +php spark make:model + +# Generate controller +php spark make:controller + +# Generate seeder +php spark make:seeder + +# Run migrations +php spark migrate + +# Rollback migrations +php spark migrate:rollback +``` + +### Composer Commands +```bash +# Install dependencies +composer install + +# Run tests via composer +composer test + +# Update autoloader +composer dump-autoload +``` + +--- + +## Code Style Guidelines + +### PHP Standards +- **PHP Version**: 8.1+ +- **PSR-4 Autoloading**: `App\` maps to `app/`, `Config\` maps to `app/Config/` +- **PSR-12 Coding Style** (follow where applicable) + +### Naming Conventions + +| Element | Convention | Example | +|---------|-----------|---------| +| Classes | PascalCase | `PatientController` | +| Methods | camelCase | `createPatient()` | +| Properties | snake_case (legacy) / camelCase (new) | `$patient_id` / `$patientId` | +| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` | +| Tables | snake_case | `patient_visits` | +| Columns | PascalCase (legacy) | `PatientID`, `NameFirst` | +| JSON fields | PascalCase | `"PatientID": "123"` | + +### File Organization +``` +app/ +├── Config/ # Configuration files +├── Controllers/ # API controllers (grouped by feature) +│ ├── Patient/ +│ ├── Organization/ +│ └── Specimen/ +├── Models/ # Data models +├── Filters/ # Request filters (Auth, CORS) +├── Traits/ # Reusable traits +├── Libraries/ # Custom libraries +├── Helpers/ # Helper functions +└── Database/ + ├── Migrations/ + └── Seeds/ +``` + +### Imports & Namespaces +- Always use fully qualified namespaces at the top +- Group imports: Framework first, then App, then external +- Use statements must be in alphabetical order within groups + +```php +model = new \App\Models\ExampleModel(); + } + + // GET /example + public function index() + { + // Implementation + } + + // POST /example + public function create() + { + // Implementation + } +} +``` + +### Response Format +All API responses must use the standardized format: + +```php +// Success response +return $this->respond([ + 'status' => 'success', + 'message' => 'Operation completed', + 'data' => $data +], 200); + +// Error response +return $this->respond([ + 'status' => 'failed', + 'message' => 'Error description', + 'data' => [] +], 400); +``` + +### Error Handling +- Use try-catch for JWT operations and external calls +- Return structured error responses with appropriate HTTP status codes +- Log errors using CodeIgniter's logging: `log_message('error', $message)` + +```php +try { + $decoded = JWT::decode($token, new Key($key, 'HS256')); +} catch (\Firebase\JWT\ExpiredException $e) { + return $this->respond(['status' => 'failed', 'message' => 'Token expired'], 401); +} catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'message' => 'Invalid token'], 401); +} +``` + +### Database Operations +- Use CodeIgniter's Query Builder or Model methods +- Prefer parameterized queries over raw SQL +- Use transactions for multi-table operations + +```php +$this->db->transStart(); +// ... database operations +$this->db->transComplete(); + +if ($this->db->transStatus() === false) { + return $this->respond(['status' => 'error', 'message' => 'Transaction failed'], 500); +} +``` + +### Testing Guidelines + +#### Test Structure +```php +withBodyFormat('json') + ->post($this->endpoint, $payload); + + $result->assertStatus(201); + } +} +``` + +#### Test Naming +- Use descriptive method names: `test` +- Example: `testCreatePatientValidationFail`, `testCreatePatientSuccess` + +#### Test Status Codes +- 200: Success (GET, PATCH) +- 201: Created (POST) +- 400: Validation Error +- 401: Unauthorized +- 404: Not Found +- 500: Server Error + +### API Design +- **Base URL**: `/api/` +- **Authentication**: JWT token via HttpOnly cookie +- **Content-Type**: `application/json` +- **HTTP Methods**: + - `GET` - Read + - `POST` - Create + - `PATCH` - Update (partial) + - `DELETE` - Delete + +### Routes Pattern +```php +$routes->group('api/patient', function ($routes) { + $routes->get('/', 'Patient\PatientController::index'); + $routes->post('/', 'Patient\PatientController::create'); + $routes->get('(:num)', 'Patient\PatientController::show/$1'); + $routes->patch('/', 'Patient\PatientController::update'); + $routes->delete('/', 'Patient\PatientController::delete'); +}); +``` + +### Security Guidelines +- Always use the `auth` filter for protected routes +- Sanitize all user inputs +- Use parameterized queries to prevent SQL injection +- Store JWT secret in `.env` file +- Never commit `.env` files + +--- + +## Project-Specific Conventions + +### Legacy Field Naming +Database uses PascalCase for column names (legacy convention): +- `PatientID`, `NameFirst`, `NameLast` +- `Birthdate`, `CreatedAt`, `UpdatedAt` + +### ValueSet System +Use the `App\Libraries\Lookups` class for static dropdown values: +```php +use App\Libraries\Lookups; + +$genders = Lookups::get('gender'); +$options = Lookups::getOptions('gender'); +``` + +### Models +Extend `BaseModel` for automatic UTC date handling: +```php +db = \Config\Database::connect(); $this->model = new \App\Models\Test\TestDefSiteModel; $this->modelCal = new \App\Models\Test\TestDefCalModel; - $this->modelTech = new \App\Models\Test\TestDefTechModel; + $this->modelGrp = new \App\Models\Test\TestDefGrpModel; $this->modelMap = new \App\Models\Test\TestMapModel; $this->modelRefNum = new \App\Models\RefRange\RefNumModel; @@ -50,13 +50,12 @@ class TestsController extends BaseController ->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType, testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt, testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate, - COALESCE(tech.DisciplineID, cal.DisciplineID) as DisciplineID, - COALESCE(tech.DepartmentID, cal.DepartmentID) as DepartmentID, + COALESCE(testdefsite.DisciplineID, cal.DisciplineID) as DisciplineID, + COALESCE(testdefsite.DepartmentID, cal.DepartmentID) as DepartmentID, d.DisciplineName, dept.DepartmentName") - ->join('testdeftech tech', 'tech.TestSiteID = testdefsite.TestSiteID AND tech.EndDate IS NULL', 'left') ->join('testdefcal cal', 'cal.TestSiteID = testdefsite.TestSiteID AND cal.EndDate IS NULL', 'left') - ->join('discipline d', 'd.DisciplineID = COALESCE(tech.DisciplineID, cal.DisciplineID)', 'left') - ->join('department dept', 'dept.DepartmentID = COALESCE(tech.DepartmentID, cal.DepartmentID)', 'left') + ->join('discipline d', 'd.DisciplineID = COALESCE(testdefsite.DisciplineID, cal.DisciplineID)', 'left') + ->join('department dept', 'dept.DepartmentID = COALESCE(testdefsite.DepartmentID, cal.DepartmentID)', 'left') ->where('testdefsite.EndDate IS NULL'); if ($siteId) { @@ -141,12 +140,13 @@ class TestsController extends BaseController $row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll(); } else { - $row['testdeftech'] = $this->db->table('testdeftech') - ->select('testdeftech.*, d.DisciplineName, dept.DepartmentName') - ->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left') - ->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left') - ->where('testdeftech.TestSiteID', $id) - ->where('testdeftech.EndDate IS NULL') + // Technical details are now stored directly in testdefsite + $row['testdeftech'] = $this->db->table('testdefsite') + ->select('testdefsite.*, d.DisciplineName, dept.DepartmentName') + ->join('discipline d', 'd.DisciplineID=testdefsite.DisciplineID', 'left') + ->join('department dept', 'dept.DepartmentID=testdefsite.DepartmentID', 'left') + ->where('testdefsite.TestSiteID', $id) + ->where('testdefsite.EndDate IS NULL') ->get()->getResultArray(); $row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll(); @@ -371,9 +371,6 @@ class TestsController extends BaseController ->where('TestSiteID', $id) ->update(['EndDate' => $now]); } elseif (in_array($typeCode, ['TEST', 'PARAM'])) { - $this->db->table('testdeftech') - ->where('TestSiteID', $id) - ->update(['EndDate' => $now]); $this->modelRefNum->where('TestSiteID', $id)->set('EndDate', $now)->update(); $this->modelRefTxt->where('TestSiteID', $id)->set('EndDate', $now)->update(); @@ -459,8 +456,8 @@ class TestsController extends BaseController private function saveTechDetails($testSiteID, $data, $action, $typeCode) { + // Technical details are now stored directly in testdefsite table $techData = [ - 'TestSiteID' => $testSiteID, 'DisciplineID' => $data['DisciplineID'] ?? null, 'DepartmentID' => $data['DepartmentID'] ?? null, 'ResultType' => $data['ResultType'] ?? null, @@ -477,20 +474,8 @@ class TestsController extends BaseController 'ExpectedTAT' => $data['ExpectedTAT'] ?? null ]; - if ($action === 'update') { - $exists = $this->db->table('testdeftech') - ->where('TestSiteID', $testSiteID) - ->where('EndDate IS NULL') - ->get()->getRowArray(); - - if ($exists) { - $this->modelTech->update($exists['TestTechID'], $techData); - } else { - $this->modelTech->insert($techData); - } - } else { - $this->modelTech->insert($techData); - } + // Update the testdefsite record directly + $this->model->update($testSiteID, $techData); } private function saveRefNumRanges($testSiteID, $ranges, $action, $siteID) diff --git a/app/Database/Migrations/2026-01-01-000008_CreateTestDefinitions.php b/app/Database/Migrations/2026-01-01-000008_CreateTestDefinitions.php index ea2312c..753d324 100644 --- a/app/Database/Migrations/2026-01-01-000008_CreateTestDefinitions.php +++ b/app/Database/Migrations/2026-01-01-000008_CreateTestDefinitions.php @@ -13,23 +13,7 @@ class CreateTestDefinitions extends Migration { 'TestSiteName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => false], 'TestType' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false], 'Description' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true], - 'SeqScr' => ['type' => 'int', 'null' => true], - 'SeqRpt' => ['type' => 'int', 'null' => true], - 'IndentLeft' => ['type' => 'int', 'null' => true, 'default' => 0], - 'FontStyle' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true], - 'VisibleScr' => ['type' => 'int', 'null' => true, 'default' => 1], - 'VisibleRpt' => ['type' => 'int', 'null' => true, 'default' => 1], - 'CountStat' => ['type' => 'int', 'null' => true, 'default' => 1], - 'CreateDate' => ['type' => 'Datetime', 'null' => true], - 'StartDate' => ['type' => 'Datetime', 'null' => true], - 'EndDate' => ['type' => 'Datetime', 'null' => true], - ]); - $this->forge->addKey('TestSiteID', true); - $this->forge->createTable('testdefsite'); - - $this->forge->addField([ - 'TestTechID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true], - 'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false], + // Technical details merged from testdeftech 'DisciplineID' => ['type' => 'INT', 'null' => true], 'DepartmentID' => ['type' => 'INT', 'null' => true], 'ResultType' => ['type' => 'VARCHAR', 'constraint'=> 20, 'null' => true], @@ -44,12 +28,22 @@ class CreateTestDefinitions extends Migration { 'CollReq' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true], 'Method' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true], 'ExpectedTAT' => ['type' => 'INT', 'null' => true], + 'SeqScr' => ['type' => 'int', 'null' => true], + 'SeqRpt' => ['type' => 'int', 'null' => true], + 'IndentLeft' => ['type' => 'int', 'null' => true, 'default' => 0], + 'FontStyle' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true], + 'VisibleScr' => ['type' => 'int', 'null' => true, 'default' => 1], + 'VisibleRpt' => ['type' => 'int', 'null' => true, 'default' => 1], + 'CountStat' => ['type' => 'int', 'null' => true, 'default' => 1], + 'Level' => ['type' => 'int', 'null' => true], 'CreateDate' => ['type' => 'Datetime', 'null' => true], - 'EndDate' => ['type' => 'Datetime', 'null' => true] + 'StartDate' => ['type' => 'Datetime', 'null' => true], + 'EndDate' => ['type' => 'Datetime', 'null' => true], ]); - $this->forge->addKey('TestTechID', true); - $this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE'); - $this->forge->createTable('testdeftech'); + $this->forge->addKey('TestSiteID', true); + $this->forge->createTable('testdefsite'); + + // testdeftech table removed - merged into testdefsite $this->forge->addField([ 'TestCalID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true], @@ -166,7 +160,7 @@ class CreateTestDefinitions extends Migration { $this->forge->dropTable('testmap'); $this->forge->dropTable('testdefgrp'); $this->forge->dropTable('testdefcal'); - $this->forge->dropTable('testdeftech'); + // testdeftech table removed - merged into testdefsite $this->forge->dropTable('testdefsite'); } } diff --git a/app/Database/Seeds/TestSeeder.php b/app/Database/Seeds/TestSeeder.php index db0dd9d..7323497 100644 --- a/app/Database/Seeds/TestSeeder.php +++ b/app/Database/Seeds/TestSeeder.php @@ -28,142 +28,98 @@ class TestSeeder extends Seeder // ======================================== // TEST TYPE - Actual Laboratory Tests // ======================================== - // Hematology Tests - $data = ['SiteID' => '1', 'TestSiteCode' => 'HB', 'TestSiteName' => 'Hemoglobin', 'TestType' => 'TEST', 'Description' => '', 'SeqScr' => '2', 'SeqRpt' => '2', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + // Hematology Tests - Technical details merged into testdefsite + $data = ['SiteID' => '1', 'TestSiteCode' => 'HB', 'TestSiteName' => 'Hemoglobin', 'TestType' => 'TEST', 'Description' => '', 'SeqScr' => '2', 'SeqRpt' => '2', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['HB'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['HB'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'HCT', 'TestSiteName' => 'Hematocrit', 'TestType' => 'TEST', 'Description' => '', 'SeqScr' => '3', 'SeqRpt' => '3', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'HCT', 'TestSiteName' => 'Hematocrit', 'TestType' => 'TEST', 'Description' => '', 'SeqScr' => '3', 'SeqRpt' => '3', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => '%', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['HCT'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['HCT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => '%', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'RBC', 'TestSiteName' => 'Red Blood Cell', 'TestType' => 'TEST', 'Description' => 'Eritrosit', 'SeqScr' => '4', 'SeqRpt' => '4', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'RBC', 'TestSiteName' => 'Red Blood Cell', 'TestType' => 'TEST', 'Description' => 'Eritrosit', 'SeqScr' => '4', 'SeqRpt' => '4', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^6/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['RBC'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['RBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^6/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'WBC', 'TestSiteName' => 'White Blood Cell', 'TestType' => 'TEST', 'Description' => 'Leukosit', 'SeqScr' => '5', 'SeqRpt' => '5', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'WBC', 'TestSiteName' => 'White Blood Cell', 'TestType' => 'TEST', 'Description' => 'Leukosit', 'SeqScr' => '5', 'SeqRpt' => '5', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['WBC'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['WBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'PLT', 'TestSiteName' => 'Platelet', 'TestType' => 'TEST', 'Description' => 'Trombosit', 'SeqScr' => '6', 'SeqRpt' => '6', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'PLT', 'TestSiteName' => 'Platelet', 'TestType' => 'TEST', 'Description' => 'Trombosit', 'SeqScr' => '6', 'SeqRpt' => '6', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['PLT'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['PLT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'MCV', 'TestSiteName' => 'MCV', 'TestType' => 'TEST', 'Description' => 'Mean Corpuscular Volume', 'SeqScr' => '7', 'SeqRpt' => '7', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'MCV', 'TestSiteName' => 'MCV', 'TestType' => 'TEST', 'Description' => 'Mean Corpuscular Volume', 'SeqScr' => '7', 'SeqRpt' => '7', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'fL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['MCV'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['MCV'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'fL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'MCH', 'TestSiteName' => 'MCH', 'TestType' => 'TEST', 'Description' => 'Mean Corpuscular Hemoglobin', 'SeqScr' => '8', 'SeqRpt' => '8', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'MCH', 'TestSiteName' => 'MCH', 'TestType' => 'TEST', 'Description' => 'Mean Corpuscular Hemoglobin', 'SeqScr' => '8', 'SeqRpt' => '8', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'pg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['MCH'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['MCH'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'pg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'MCHC', 'TestSiteName' => 'MCHC', 'TestType' => 'TEST', 'Description' => 'Mean Corpuscular Hemoglobin Concentration', 'SeqScr' => '9', 'SeqRpt' => '9', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'MCHC', 'TestSiteName' => 'MCHC', 'TestType' => 'TEST', 'Description' => 'Mean Corpuscular Hemoglobin Concentration', 'SeqScr' => '9', 'SeqRpt' => '9', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['MCHC'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['MCHC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); // Chemistry Tests - $data = ['SiteID' => '1', 'TestSiteCode' => 'GLU', 'TestSiteName' => 'Glucose', 'TestType' => 'TEST', 'Description' => 'Glukosa Sewaktu', 'SeqScr' => '11', 'SeqRpt' => '11', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'GLU', 'TestSiteName' => 'Glucose', 'TestType' => 'TEST', 'Description' => 'Glukosa Sewaktu', 'SeqScr' => '11', 'SeqRpt' => '11', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '0.0555', 'Unit2' => 'mmol/L', 'Decimal' => '0', 'Method' => 'Hexokinase', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['GLU'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['GLU'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '0.0555', 'Unit2' => 'mmol/L', 'Decimal' => '0', 'Method' => 'Hexokinase', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'CREA', 'TestSiteName' => 'Creatinine', 'TestType' => 'TEST', 'Description' => 'Kreatinin', 'SeqScr' => '12', 'SeqRpt' => '12', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'CREA', 'TestSiteName' => 'Creatinine', 'TestType' => 'TEST', 'Description' => 'Kreatinin', 'SeqScr' => '12', 'SeqRpt' => '12', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '88.4', 'Unit2' => 'umol/L', 'Decimal' => '2', 'Method' => 'Enzymatic', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['CREA'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['CREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '88.4', 'Unit2' => 'umol/L', 'Decimal' => '2', 'Method' => 'Enzymatic', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'UREA', 'TestSiteName' => 'Blood Urea Nitrogen', 'TestType' => 'TEST', 'Description' => 'BUN', 'SeqScr' => '13', 'SeqRpt' => '13', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'UREA', 'TestSiteName' => 'Blood Urea Nitrogen', 'TestType' => 'TEST', 'Description' => 'BUN', 'SeqScr' => '13', 'SeqRpt' => '13', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Urease-GLDH', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['UREA'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['UREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Urease-GLDH', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'SGOT', 'TestSiteName' => 'AST (SGOT)', 'TestType' => 'TEST', 'Description' => 'Aspartate Aminotransferase', 'SeqScr' => '14', 'SeqRpt' => '14', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'SGOT', 'TestSiteName' => 'AST (SGOT)', 'TestType' => 'TEST', 'Description' => 'Aspartate Aminotransferase', 'SeqScr' => '14', 'SeqRpt' => '14', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['SGOT'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['SGOT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'SGPT', 'TestSiteName' => 'ALT (SGPT)', 'TestType' => 'TEST', 'Description' => 'Alanine Aminotransferase', 'SeqScr' => '15', 'SeqRpt' => '15', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'SGPT', 'TestSiteName' => 'ALT (SGPT)', 'TestType' => 'TEST', 'Description' => 'Alanine Aminotransferase', 'SeqScr' => '15', 'SeqRpt' => '15', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['SGPT'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['SGPT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'CHOL', 'TestSiteName' => 'Total Cholesterol', 'TestType' => 'TEST', 'Description' => 'Kolesterol Total', 'SeqScr' => '16', 'SeqRpt' => '16', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'CHOL', 'TestSiteName' => 'Total Cholesterol', 'TestType' => 'TEST', 'Description' => 'Kolesterol Total', 'SeqScr' => '16', 'SeqRpt' => '16', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Enzymatic', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['CHOL'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['CHOL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Enzymatic', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'TG', 'TestSiteName' => 'Triglycerides', 'TestType' => 'TEST', 'Description' => 'Trigliserida', 'SeqScr' => '17', 'SeqRpt' => '17', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'TG', 'TestSiteName' => 'Triglycerides', 'TestType' => 'TEST', 'Description' => 'Trigliserida', 'SeqScr' => '17', 'SeqRpt' => '17', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'GPO-PAP', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['TG'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['TG'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'GPO-PAP', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'HDL', 'TestSiteName' => 'HDL Cholesterol', 'TestType' => 'TEST', 'Description' => 'Kolesterol HDL', 'SeqScr' => '18', 'SeqRpt' => '18', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'HDL', 'TestSiteName' => 'HDL Cholesterol', 'TestType' => 'TEST', 'Description' => 'Kolesterol HDL', 'SeqScr' => '18', 'SeqRpt' => '18', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['HDL'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['HDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'LDL', 'TestSiteName' => 'LDL Cholesterol', 'TestType' => 'TEST', 'Description' => 'Kolesterol LDL', 'SeqScr' => '19', 'SeqRpt' => '19', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'LDL', 'TestSiteName' => 'LDL Cholesterol', 'TestType' => 'TEST', 'Description' => 'Kolesterol LDL', 'SeqScr' => '19', 'SeqRpt' => '19', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['LDL'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['LDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); // ======================================== // PARAM TYPE - Parameters (non-lab values) // ======================================== - $data = ['SiteID' => '1', 'TestSiteCode' => 'HEIGHT', 'TestSiteName' => 'Height', 'TestType' => 'PARAM', 'Description' => 'Tinggi Badan', 'SeqScr' => '40', 'SeqRpt' => '40', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'HEIGHT', 'TestSiteName' => 'Height', 'TestType' => 'PARAM', 'Description' => 'Tinggi Badan', 'SeqScr' => '40', 'SeqRpt' => '40', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'cm', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['HEIGHT'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['HEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'cm', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'WEIGHT', 'TestSiteName' => 'Weight', 'TestType' => 'PARAM', 'Description' => 'Berat Badan', 'SeqScr' => '41', 'SeqRpt' => '41', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'WEIGHT', 'TestSiteName' => 'Weight', 'TestType' => 'PARAM', 'Description' => 'Berat Badan', 'SeqScr' => '41', 'SeqRpt' => '41', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'kg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => '', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['WEIGHT'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['WEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'kg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => '', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'AGE', 'TestSiteName' => 'Age', 'TestType' => 'PARAM', 'Description' => 'Usia', 'SeqScr' => '42', 'SeqRpt' => '42', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'AGE', 'TestSiteName' => 'Age', 'TestType' => 'PARAM', 'Description' => 'Usia', 'SeqScr' => '42', 'SeqRpt' => '42', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'years', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['AGE'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['AGE'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'years', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'SYSTL', 'TestSiteName' => 'Systolic BP', 'TestType' => 'PARAM', 'Description' => 'Tekanan Darah Sistolik', 'SeqScr' => '43', 'SeqRpt' => '43', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'SYSTL', 'TestSiteName' => 'Systolic BP', 'TestType' => 'PARAM', 'Description' => 'Tekanan Darah Sistolik', 'SeqScr' => '43', 'SeqRpt' => '43', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['SYSTL'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['SYSTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'DIASTL', 'TestSiteName' => 'Diastolic BP', 'TestType' => 'PARAM', 'Description' => 'Tekanan Darah Diastolik', 'SeqScr' => '44', 'SeqRpt' => '44', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'DIASTL', 'TestSiteName' => 'Diastolic BP', 'TestType' => 'PARAM', 'Description' => 'Tekanan Darah Diastolik', 'SeqScr' => '44', 'SeqRpt' => '44', 'IndentLeft' => '0', 'VisibleScr' => '1', 'VisibleRpt' => '0', 'CountStat' => '0', 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['DIASTL'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['DIASTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => 'NMRIC', 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); // ======================================== // CALC TYPE - Calculated Tests @@ -232,28 +188,20 @@ class TestSeeder extends Seeder ]); // Urinalysis Tests (with valueset result type) - $data = ['SiteID' => '1', 'TestSiteCode' => 'UCOLOR', 'TestSiteName' => 'Urine Color', 'TestType' => 'TEST', 'Description' => 'Warna Urine', 'SeqScr' => '31', 'SeqRpt' => '31', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'UCOLOR', 'TestSiteName' => 'Urine Color', 'TestType' => 'TEST', 'Description' => 'Warna Urine', 'SeqScr' => '31', 'SeqRpt' => '31', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => 'VSET', 'RefType' => 'TEXT', 'VSet' => '1001', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Visual', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['UCOLOR'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['UCOLOR'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => 'VSET', 'RefType' => 'TEXT', 'VSet' => '1001', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Visual', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'UGLUC', 'TestSiteName' => 'Urine Glucose', 'TestType' => 'TEST', 'Description' => 'Glukosa Urine', 'SeqScr' => '32', 'SeqRpt' => '32', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'UGLUC', 'TestSiteName' => 'Urine Glucose', 'TestType' => 'TEST', 'Description' => 'Glukosa Urine', 'SeqScr' => '32', 'SeqRpt' => '32', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => 'VSET', 'RefType' => 'TEXT', 'VSet' => '1002', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['UGLUC'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['UGLUC'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => 'VSET', 'RefType' => 'TEXT', 'VSet' => '1002', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'UPROT', 'TestSiteName' => 'Urine Protein', 'TestType' => 'TEST', 'Description' => 'Protein Urine', 'SeqScr' => '33', 'SeqRpt' => '33', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'UPROT', 'TestSiteName' => 'Urine Protein', 'TestType' => 'TEST', 'Description' => 'Protein Urine', 'SeqScr' => '33', 'SeqRpt' => '33', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => 'VSET', 'RefType' => 'TEXT', 'VSet' => '1003', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['UPROT'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['UPROT'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => 'VSET', 'RefType' => 'TEXT', 'VSet' => '1003', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); - $data = ['SiteID' => '1', 'TestSiteCode' => 'PH', 'TestSiteName' => 'Urine pH', 'TestType' => 'TEST', 'Description' => 'pH Urine', 'SeqScr' => '34', 'SeqRpt' => '34', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'CreateDate' => "$now"]; + $data = ['SiteID' => '1', 'TestSiteCode' => 'PH', 'TestSiteName' => 'Urine pH', 'TestType' => 'TEST', 'Description' => 'pH Urine', 'SeqScr' => '34', 'SeqRpt' => '34', 'IndentLeft' => '1', 'VisibleScr' => '1', 'VisibleRpt' => '1', 'CountStat' => '1', 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Dipstick', 'CreateDate' => "$now"]; $this->db->table('testdefsite')->insert($data); $tIDs['PH'] = $this->db->insertID(); - $data = ['TestSiteID' => $tIDs['PH'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => 'NMRIC', 'RefType' => 'NMRC', 'VSet' => '', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Dipstick', 'CreateDate' => "$now"]; - $this->db->table('testdeftech')->insert($data); } } diff --git a/app/Libraries/Data/result_type.json b/app/Libraries/Data/result_type.json index 09081da..38b5bfb 100644 --- a/app/Libraries/Data/result_type.json +++ b/app/Libraries/Data/result_type.json @@ -5,6 +5,7 @@ {"key": "NMRIC", "value": "Numeric"}, {"key": "RANGE", "value": "Range"}, {"key": "TEXT", "value": "Text"}, - {"key": "VSET", "value": "Value set"} + {"key": "VSET", "value": "Value set"}, + {"key": "NORES", "value": "No Result"} ] } diff --git a/app/Models/RefRange/RefTHoldModel.php b/app/Models/RefRange/RefTHoldModel.php deleted file mode 100644 index 9fc3f1e..0000000 --- a/app/Models/RefRange/RefTHoldModel.php +++ /dev/null @@ -1,20 +0,0 @@ -where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); } elseif (in_array($typeCode, ['TEST', 'PARAM'])) { - $row['testdeftech'] = $db->table('testdeftech') - ->select('testdeftech.*, d.DisciplineName, dept.DepartmentName') - ->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left') - ->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left') - ->where('testdeftech.TestSiteID', $TestSiteID) - ->where('testdeftech.EndDate IS NULL') - ->get()->getResultArray(); + // Technical details are now flattened into the main row + if ($row['DisciplineID']) { + $discipline = $db->table('discipline')->where('DisciplineID', $row['DisciplineID'])->get()->getRowArray(); + $row['DisciplineName'] = $discipline['DisciplineName'] ?? null; + } + if ($row['DepartmentID']) { + $department = $db->table('department')->where('DepartmentID', $row['DepartmentID'])->get()->getRowArray(); + $row['DepartmentName'] = $department['DepartmentName'] ?? null; + } $testMapModel = new \App\Models\Test\TestMapModel(); $row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll(); diff --git a/app/Models/Test/TestDefTechModel.php b/app/Models/Test/TestDefTechModel.php deleted file mode 100644 index 069fc3a..0000000 --- a/app/Models/Test/TestDefTechModel.php +++ /dev/null @@ -1,82 +0,0 @@ -table('testdeftech') - ->select('testdeftech.*, d.DisciplineName, dept.DepartmentName') - ->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left') - ->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left') - ->where('testdeftech.TestSiteID', $testSiteID) - ->where('testdeftech.EndDate IS NULL') - ->get()->getResultArray(); - } - - /** - * Get tests by discipline - */ - public function getTestsByDiscipline($disciplineID, $siteID = null) { - $builder = $this->select('testdeftech.*, testdefsite.TestSiteCode, testdefsite.TestSiteName') - ->join('testdefsite', 'testdefsite.TestSiteID=testdeftech.TestSiteID', 'left') - ->where('testdeftech.DisciplineID', $disciplineID) - ->where('testdeftech.EndDate IS NULL'); - - if ($siteID) { - $builder->where('testdefsite.SiteID', $siteID); - } - - return $builder->findAll(); - } - - /** - * Get tests by department - */ - public function getTestsByDepartment($departmentID, $siteID = null) { - $builder = $this->select('testdeftech.*, testdefsite.TestSiteCode, testdefsite.TestSiteName') - ->join('testdefsite', 'testdefsite.TestSiteID=testdeftech.TestSiteID', 'left') - ->where('testdeftech.DepartmentID', $departmentID) - ->where('testdeftech.EndDate IS NULL'); - - if ($siteID) { - $builder->where('testdefsite.SiteID', $siteID); - } - - return $builder->findAll(); - } -} diff --git a/docs/audit-logging-plan.md b/docs/audit-logging-plan.md new file mode 100644 index 0000000..27c820b --- /dev/null +++ b/docs/audit-logging-plan.md @@ -0,0 +1,473 @@ +# Audit Logging Architecture Plan for CLQMS + +> **Clinical Laboratory Quality Management System (CLQMS)** - A comprehensive audit trail strategy for tracking changes across master data, patient records, and laboratory operations. + +--- + +## Executive Summary + +This document outlines a unified audit logging architecture for CLQMS, designed to provide complete traceability of data changes while maintaining optimal performance and maintainability. The approach separates audit logs into three domain-specific tables, utilizing JSON for flexible value storage. + +--- + +## 1. Current State Analysis + +### Existing Audit Infrastructure + +| Aspect | Current Status | +|--------|---------------| +| **Database Tables** | 3 tables exist in migrations (patreglog, patvisitlog, specimenlog) | +| **Implementation** | Tables created but not actively used | +| **Structure** | Fixed column approach (FldName, FldValuePrev) | +| **Code Coverage** | No models or controllers implemented | +| **Application Logging** | Basic CodeIgniter file logging for debug/errors | + +### Pain Points Identified + +- ❌ **3 separate tables** with nearly identical schemas +- ❌ **Fixed column structure** - rigid and requires schema changes for new entities +- ❌ **No implementation** - audit tables exist but aren't populated +- ❌ **Maintenance overhead** - adding new entities requires new migrations + +--- + +## 2. Proposed Architecture + +### 2.1 Domain Separation + +We categorize audit logs by **data domain** and **access patterns**: + +| Table | Domain | Volume | Retention | Use Case | +|-------|--------|--------|-----------|----------| +| `master_audit_log` | Reference Data | Low | Permanent | Organizations, Users, ValueSets | +| `patient_audit_log` | Patient Records | Medium | 7 years | Demographics, Contacts, Insurance | +| `order_audit_log` | Operations | High | 2 years | Orders, Tests, Specimens, Results | + +### 2.2 Unified Table Structure + +#### Master Audit Log + +```sql +CREATE TABLE master_audit_log ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + entity_type VARCHAR(50) NOT NULL, -- 'organization', 'user', 'valueset' + entity_id VARCHAR(36) NOT NULL, -- UUID or primary key + action ENUM('CREATE', 'UPDATE', 'DELETE', 'PATCH') NOT NULL, + + old_values JSON NULL, -- Complete snapshot before change + new_values JSON NULL, -- Complete snapshot after change + changed_fields JSON, -- Array of modified field names + + -- Context + user_id VARCHAR(36), + site_id VARCHAR(36), + ip_address VARCHAR(45), + user_agent VARCHAR(500), + app_version VARCHAR(20), + + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_entity (entity_type, entity_id), + INDEX idx_created (created_at), + INDEX idx_user (user_id, created_at) +) ENGINE=InnoDB; +``` + +#### Patient Audit Log + +```sql +CREATE TABLE patient_audit_log ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + entity_type VARCHAR(50) NOT NULL, -- 'patient', 'contact', 'insurance' + entity_id VARCHAR(36) NOT NULL, + patient_id VARCHAR(36), -- Context FK for patient + + action ENUM('CREATE', 'UPDATE', 'DELETE', 'MERGE', 'UNMERGE') NOT NULL, + + old_values JSON NULL, + new_values JSON NULL, + changed_fields JSON, + reason TEXT, -- Why the change was made + + -- Context + user_id VARCHAR(36), + site_id VARCHAR(36), + ip_address VARCHAR(45), + session_id VARCHAR(100), + + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_entity (entity_type, entity_id), + INDEX idx_patient (patient_id, created_at), + INDEX idx_created (created_at), + INDEX idx_user (user_id, created_at) +) ENGINE=InnoDB; +``` + +#### Order/Test Audit Log + +```sql +CREATE TABLE order_audit_log ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + entity_type VARCHAR(50) NOT NULL, -- 'order', 'test', 'specimen', 'result' + entity_id VARCHAR(36) NOT NULL, + + -- Context FKs + patient_id VARCHAR(36), + visit_id VARCHAR(36), + order_id VARCHAR(36), + + action ENUM('CREATE', 'UPDATE', 'DELETE', 'CANCEL', 'REORDER', 'COLLECT', 'RESULT') NOT NULL, + + old_values JSON NULL, + new_values JSON NULL, + changed_fields JSON, + status_transition VARCHAR(100), -- e.g., 'pending->collected' + + -- Context + user_id VARCHAR(36), + site_id VARCHAR(36), + device_id VARCHAR(36), -- Instrument/edge device + ip_address VARCHAR(45), + session_id VARCHAR(100), + + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + + INDEX idx_entity (entity_type, entity_id), + INDEX idx_order (order_id, created_at), + INDEX idx_patient (patient_id, created_at), + INDEX idx_created (created_at), + INDEX idx_user (user_id, created_at) +) ENGINE=InnoDB; +``` + +--- + +## 3. JSON Value Structure + +### Example Audit Entry + +```json +{ + "id": 15243, + "entity_type": "patient", + "entity_id": "PAT-2026-001234", + "action": "UPDATE", + + "old_values": { + "NameFirst": "John", + "NameLast": "Doe", + "Gender": "M", + "BirthDate": "1990-01-15", + "Phone": "+1-555-0100" + }, + + "new_values": { + "NameFirst": "Johnny", + "NameLast": "Doe-Smith", + "Gender": "M", + "BirthDate": "1990-01-15", + "Phone": "+1-555-0199" + }, + + "changed_fields": ["NameFirst", "NameLast", "Phone"], + + "user_id": "USR-001", + "site_id": "SITE-001", + "created_at": "2026-02-19T14:30:00Z" +} +``` + +### Benefits of JSON Approach + +✅ **Schema Evolution** - Add new fields without migrations +✅ **Complete Snapshots** - Reconstruct full record state at any point +✅ **Flexible Queries** - MySQL 8.0+ supports JSON indexing and extraction +✅ **Audit Integrity** - Store exactly what changed, no data loss + +--- + +## 4. Implementation Strategy + +### 4.1 Central Audit Service + +```php + $entityType, + 'entity_id' => $entityId, + 'action' => $action, + 'old_values' => $oldValues ? json_encode($oldValues) : null, + 'new_values' => $newValues ? json_encode($newValues) : null, + 'changed_fields' => json_encode($changedFields), + 'user_id' => auth()->id() ?? 'SYSTEM', + 'site_id' => session('site_id') ?? 'MAIN', + 'created_at' => date('Y-m-d H:i:s') + ]; + + // Route to appropriate table + $table = match($category) { + 'master' => 'master_audit_log', + 'patient' => 'patient_audit_log', + 'order' => 'order_audit_log', + default => throw new \InvalidArgumentException("Unknown category: $category") + }; + + // Async logging recommended for high-volume operations + self::dispatchAuditJob($table, $data); + } + + private static function calculateChangedFields(?array $old, ?array $new): array + { + if (!$old || !$new) return []; + + $changes = []; + $allKeys = array_unique(array_merge(array_keys($old), array_keys($new))); + + foreach ($allKeys as $key) { + if (($old[$key] ?? null) !== ($new[$key] ?? null)) { + $changes[] = $key; + } + } + + return $changes; + } +} +``` + +### 4.2 Model Integration + +```php +getPatientId(), + action: $action, + oldValues: $oldValues, + newValues: $newValues + ); + } + + // Override save method to auto-log + public function save($data): bool + { + $oldData = $this->find($data['PatientID'] ?? null); + + $result = parent::save($data); + + if ($result) { + $this->logAudit( + $oldData ? 'UPDATE' : 'CREATE', + $oldData?->toArray(), + $this->find($data['PatientID'])->toArray() + ); + } + + return $result; + } +} +``` + +--- + +## 5. Query Patterns & Performance + +### 5.1 Common Queries + +```sql +-- View entity history +SELECT * FROM patient_audit_log +WHERE entity_type = 'patient' +AND entity_id = 'PAT-2026-001234' +ORDER BY created_at DESC; + +-- User activity report +SELECT entity_type, action, COUNT(*) as count +FROM patient_audit_log +WHERE user_id = 'USR-001' +AND created_at > DATE_SUB(NOW(), INTERVAL 7 DAY) +GROUP BY entity_type, action; + +-- Find all changes to a specific field +SELECT * FROM order_audit_log +WHERE JSON_CONTAINS(changed_fields, '"result_value"') +AND patient_id = 'PAT-001' +AND created_at > '2026-01-01'; +``` + +### 5.2 Partitioning Strategy (Order/Test) + +For high-volume tables, implement monthly partitioning: + +```sql +CREATE TABLE order_audit_log ( + -- ... columns + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB +PARTITION BY RANGE (YEAR(created_at) * 100 + MONTH(created_at)) ( + PARTITION p202601 VALUES LESS THAN (202602), + PARTITION p202602 VALUES LESS THAN (202603), + PARTITION p_future VALUES LESS THAN MAXVALUE +); +``` + +--- + +## 6. Soft Delete Handling + +Soft deletes ARE captured as audit entries with complete snapshots: + +```php +// When soft deleting a patient: +AuditService::log( + category: 'patient', + entityType: 'patient', + entityId: $patientId, + action: 'DELETE', + oldValues: $fullRecordBeforeDelete, // Complete last known state + newValues: null, + reason: 'Patient requested data removal' +); +``` + +This ensures: +- ✅ Full audit trail even for deleted records +- ✅ Compliance with "right to be forgotten" (GDPR) +- ✅ Ability to restore accidentally deleted records + +--- + +## 7. Migration Plan + +### Phase 1: Foundation (Week 1) +- [ ] Drop existing unused tables (patreglog, patvisitlog, specimenlog) +- [ ] Create new audit tables with JSON columns +- [ ] Create AuditService class +- [ ] Add database indexes + +### Phase 2: Core Implementation (Week 2) +- [ ] Integrate AuditService into Patient model +- [ ] Integrate AuditService into Order model +- [ ] Integrate AuditService into Master data models +- [ ] Add audit trail to authentication events + +### Phase 3: API & UI (Week 3) +- [ ] Create API endpoints for querying audit logs +- [ ] Build admin interface for audit review +- [ ] Add audit export functionality (CSV/PDF) + +### Phase 4: Optimization (Week 4) +- [ ] Implement async logging queue +- [ ] Add table partitioning for order_audit_log +- [ ] Set up retention policies and archiving +- [ ] Performance testing and tuning + +--- + +## 8. Retention & Archiving Strategy + +| Table | Retention Period | Archive Action | +|-------|---------------|----------------| +| `master_audit_log` | Permanent | None (keep forever) | +| `patient_audit_log` | 7 years | Move to cold storage after 7 years | +| `order_audit_log` | 2 years | Partition rotation: drop old partitions | + +### Automated Maintenance + +```sql +-- Monthly job: Archive old patient audit logs +INSERT INTO patient_audit_log_archive +SELECT * FROM patient_audit_log +WHERE created_at < DATE_SUB(NOW(), INTERVAL 7 YEAR); + +DELETE FROM patient_audit_log +WHERE created_at < DATE_SUB(NOW(), INTERVAL 7 YEAR); + +-- Monthly job: Drop old order partitions +ALTER TABLE order_audit_log DROP PARTITION p202501; +``` + +--- + +## 9. Questions for Stakeholders + +Before implementation, please confirm: + +1. **Retention Policy**: Are the proposed retention periods (master=forever, patient=7 years, order=2 years) compliant with your regulatory requirements? + +2. **Async vs Sync**: Should audit logging be synchronous (block on failure) or asynchronous (queue-based)? Recommended: async for order/test operations. + +3. **Archive Storage**: Where should archived audit logs be stored? Options: separate database, file storage (S3), or compressed tables. + +4. **User Access**: Which user roles need access to audit trails? Should users see their own audit history? + +5. **Compliance**: Do you need specific compliance features (e.g., HIPAA audit trail requirements, 21 CFR Part 11 for FDA)? + +--- + +## 10. Key Design Decisions Summary + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Table Count** | 3 tables | Separates concerns, optimizes queries, different retention | +| **JSON vs Columns** | JSON for values | Flexible, handles schema changes, complete snapshots | +| **Full vs Diff** | Full snapshots | Easier to reconstruct history, no data loss | +| **Soft Deletes** | Captured in audit | Compliance and restore capability | +| **Partitioning** | Order table only | High volume, time-based queries | +| **Async Logging** | Recommended | Don't block user operations | + +--- + +## Conclusion + +This unified audit logging architecture provides: + +✅ **Complete traceability** across all data domains +✅ **Regulatory compliance** with proper retention +✅ **Performance optimization** through domain separation +✅ **Flexibility** via JSON value storage +✅ **Maintainability** with centralized service + +The approach balances audit integrity with system performance, ensuring CLQMS can scale while maintaining comprehensive audit trails. + +--- + +*Document Version: 1.0* +*Author: CLQMS Development Team* +*Date: February 19, 2026* diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index 200194a..69f3bfe 100644 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -2644,10 +2644,118 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TestDefinition' + type: object + properties: + SiteID: + type: integer + description: Site ID (required) + TestSiteCode: + type: string + description: Test code (required) + TestSiteName: + type: string + description: Test name (required) + TestType: + type: string + enum: + - TEST + - PARAM + - CALC + - GROUP + - TITLE + description: Test type (required) + Description: + type: string + DisciplineID: + type: integer + DepartmentID: + type: integer + ResultType: + type: string + enum: + - NMRIC + - VSET + RefType: + type: string + enum: + - NMRC + - TEXT + - THOLD + - VSET + VSet: + type: integer + ReqQty: + type: number + format: decimal + ReqQtyUnit: + type: string + Unit1: + type: string + Factor: + type: number + format: decimal + Unit2: + type: string + Decimal: + type: integer + CollReq: + type: string + Method: + type: string + ExpectedTAT: + type: integer + SeqScr: + type: integer + SeqRpt: + type: integer + IndentLeft: + type: integer + FontStyle: + type: string + VisibleScr: + type: integer + VisibleRpt: + type: integer + CountStat: + type: integer + details: + type: object + description: Type-specific details + refnum: + type: array + items: + type: object + reftxt: + type: array + items: + type: object + testmap: + type: array + items: + type: object + required: + - SiteID + - TestSiteCode + - TestSiteName + - TestType responses: '201': description: Test definition created + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: created + message: + type: string + data: + type: object + properties: + TestSiteId: + type: integer patch: tags: - Tests @@ -2659,10 +2767,112 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TestDefinition' + type: object + properties: + TestSiteID: + type: integer + description: Test Site ID (required) + TestSiteCode: + type: string + TestSiteName: + type: string + TestType: + type: string + enum: + - TEST + - PARAM + - CALC + - GROUP + - TITLE + Description: + type: string + DisciplineID: + type: integer + DepartmentID: + type: integer + ResultType: + type: string + enum: + - NMRIC + - VSET + RefType: + type: string + enum: + - NMRC + - TEXT + - THOLD + - VSET + VSet: + type: integer + ReqQty: + type: number + format: decimal + ReqQtyUnit: + type: string + Unit1: + type: string + Factor: + type: number + format: decimal + Unit2: + type: string + Decimal: + type: integer + CollReq: + type: string + Method: + type: string + ExpectedTAT: + type: integer + SeqScr: + type: integer + SeqRpt: + type: integer + IndentLeft: + type: integer + FontStyle: + type: string + VisibleScr: + type: integer + VisibleRpt: + type: integer + CountStat: + type: integer + details: + type: object + description: Type-specific details + refnum: + type: array + items: + type: object + reftxt: + type: array + items: + type: object + testmap: + type: array + items: + type: object + required: + - TestSiteID responses: '200': description: Test definition updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + TestSiteId: + type: integer /api/tests/{id}: get: tags: @@ -2676,9 +2886,70 @@ paths: required: true schema: type: integer + description: Test Site ID responses: '200': description: Test definition details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '#/components/schemas/TestDefinition' + '404': + description: Test not found + delete: + tags: + - Tests + summary: Soft delete test definition + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Site ID to delete + requestBody: + content: + application/json: + schema: + type: object + properties: + TestSiteID: + type: integer + description: Optional - can be provided in body instead of path + responses: + '200': + description: Test disabled successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + TestSiteId: + type: integer + EndDate: + type: string + format: date-time + '404': + description: Test not found + '422': + description: Test already disabled /api/valueset: get: tags: @@ -3832,11 +4103,13 @@ components: TestDefinition: type: object properties: - id: + TestSiteID: type: integer - TestCode: + SiteID: + type: integer + TestSiteCode: type: string - TestName: + TestSiteName: type: string TestType: type: string @@ -3852,6 +4125,8 @@ components: CALC: Calculated GROUP: Panel/Profile TITLE: Section header + Description: + type: string DisciplineID: type: integer DisciplineName: @@ -3860,10 +4135,112 @@ components: type: integer DepartmentName: type: string - Unit: + ResultType: type: string - Formula: + enum: + - NMRIC + - VSET + description: | + NMRIC: Numeric result + VSET: Value set result + RefType: type: string + enum: + - NMRC + - TEXT + - THOLD + - VSET + description: | + NMRC: Numeric reference range + TEXT: Text reference + THOLD: Threshold reference + VSET: Value set reference + VSet: + type: integer + description: Value set ID for VSET result type + ReqQty: + type: number + format: decimal + description: Required sample quantity + ReqQtyUnit: + type: string + description: Unit for required quantity + Unit1: + type: string + description: Primary unit + Factor: + type: number + format: decimal + description: Conversion factor + Unit2: + type: string + description: Secondary unit (after conversion) + Decimal: + type: integer + description: Number of decimal places + CollReq: + type: string + description: Collection requirements + Method: + type: string + description: Test method + ExpectedTAT: + type: integer + description: Expected turnaround time + SeqScr: + type: integer + description: Screen sequence + SeqRpt: + type: integer + description: Report sequence + IndentLeft: + type: integer + default: 0 + FontStyle: + type: string + VisibleScr: + type: integer + default: 1 + description: Screen visibility (0=hidden, 1=visible) + VisibleRpt: + type: integer + default: 1 + description: Report visibility (0=hidden, 1=visible) + CountStat: + type: integer + default: 1 + Level: + type: integer + CreateDate: + type: string + format: date-time + StartDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + FormulaInput: + type: string + description: Input variables for calculated tests + FormulaCode: + type: string + description: Formula expression for calculated tests + testdefcal: + type: array + description: Calculated test details (only for CALC type) + items: + type: object + testdefgrp: + type: array + description: Group members (only for GROUP type) + items: + type: object + testmap: + type: array + description: Test mappings + items: + $ref: '#/components/schemas/TestMap' refnum: type: array description: Numeric reference ranges (optional). Mutually exclusive with reftxt - a test can only have ONE reference type. @@ -3939,36 +4316,66 @@ components: Flag: type: string examples: - $1: - Unit: mg/dL - refnum: - - RefNumID: 1 - NumRefType: NMRC - NumRefTypeLabel: Numeric - RangeType: REF - RangeTypeLabel: Reference Range - Sex: '2' - SexLabel: Male - LowSign: GE - LowSignLabel: '>=' - HighSign: LE - HighSignLabel: <= - Low: 70 - High: 100 - AgeStart: 18 - AgeEnd: 99 - Flag: 'N' - Interpretation: Normal - TEST_numeric_thold: - summary: Technical test - threshold reference (THOLD) + TEST_numeric: + summary: Technical test with numeric reference value: - id: 1 - TestCode: GLU - TestName: Glucose + TestSiteID: 1 + SiteID: 1 + TestSiteCode: GLU + TestSiteName: Glucose TestType: TEST - DisciplineID: 1 - DepartmentID: 1 - Unit: mg/dL + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: NMRC + Unit1: mg/dL + ReqQty: 300 + ReqQtyUnit: uL + Decimal: 0 + Method: Hexokinase + SeqScr: 11 + SeqRpt: 11 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + refnum: + - RefNumID: 1 + NumRefType: NMRC + NumRefTypeLabel: Numeric + RangeType: REF + RangeTypeLabel: Reference Range + Sex: '2' + SexLabel: Male + LowSign: GE + LowSignLabel: '>=' + HighSign: LE + HighSignLabel: <= + Low: 70 + High: 100 + AgeStart: 18 + AgeEnd: 99 + Flag: 'N' + Interpretation: Normal + TEST_threshold: + summary: Technical test with threshold reference (panic) + value: + TestSiteID: 2 + SiteID: 1 + TestSiteCode: GLU + TestSiteName: Glucose + TestType: TEST + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mg/dL + Decimal: 0 + Method: Hexokinase + SeqScr: 11 + SeqRpt: 11 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 refnum: - RefNumID: 2 NumRefType: THOLD @@ -3984,136 +4391,179 @@ components: AgeEnd: 120 Flag: L Interpretation: Critical Low - $2: - Unit: null - reftxt: - - RefTxtID: 1 - TxtRefType: TEXT - TxtRefTypeLabel: Text - Sex: '2' - SexLabel: Male - AgeStart: 18 - AgeEnd: 99 - RefTxt: NORM=Normal;HYPO=Hypochromic;MACRO=Macrocytic - Flag: 'N' - TEST_text_vset: - summary: Technical test - text reference (VSET) + TEST_text: + summary: Technical test with text reference value: - id: 1 - TestCode: STAGE - TestName: Disease Stage + TestSiteID: 3 + SiteID: 1 + TestSiteCode: STAGE + TestSiteName: Disease Stage TestType: TEST DisciplineID: 1 DepartmentID: 1 - Unit: null + ResultType: VSET + RefType: TEXT + SeqScr: 50 + SeqRpt: 50 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 reftxt: - - RefTxtID: 2 - TxtRefType: VSET - TxtRefTypeLabel: Value Set - Sex: '1' - SexLabel: Female - AgeStart: 0 - AgeEnd: 120 - RefTxt: STG1=Stage 1;STG2=Stage 2;STG3=Stage 3;STG4=Stage 4 + - RefTxtID: 1 + TxtRefType: TEXT + TxtRefTypeLabel: Text + Sex: '2' + SexLabel: Male + AgeStart: 18 + AgeEnd: 99 + RefTxt: NORM=Normal;HYPO=Hypochromic;MACRO=Macrocytic Flag: 'N' PARAM: - summary: Parameter - no reference range allowed + summary: Parameter test value: - id: 2 - TestCode: GLU_FAST - TestName: Fasting Glucose + TestSiteID: 4 + SiteID: 1 + TestSiteCode: HEIGHT + TestSiteName: Height TestType: PARAM - DisciplineID: 1 - DepartmentID: 1 - Unit: mg/dL - $3: - Unit: null - Formula: BUN / Creatinine - refnum: - - RefNumID: 5 - NumRefType: NMRC - NumRefTypeLabel: Numeric - RangeType: REF - RangeTypeLabel: Reference Range - Sex: '1' - SexLabel: Female - LowSign: GE - LowSignLabel: '>=' - HighSign: LE - HighSignLabel: <= - Low: 10 - High: 20 - AgeStart: 18 - AgeEnd: 120 - Flag: 'N' - Interpretation: Normal - $4: - Unit: null - Formula: BUN / Creatinine - refnum: - - RefNumID: 6 - NumRefType: THOLD - NumRefTypeLabel: Threshold - RangeType: PANIC - RangeTypeLabel: Panic Range - Sex: '1' - SexLabel: Female - LowSign: GT - LowSignLabel: '>' - Low: 20 - AgeStart: 18 - AgeEnd: 120 - Flag: H - Interpretation: Elevated - possible prerenal cause + DisciplineID: 10 + ResultType: NMRIC + Unit1: cm + Decimal: 0 + SeqScr: 40 + SeqRpt: 40 + VisibleScr: 1 + VisibleRpt: 0 + CountStat: 0 + CALC: + summary: Calculated test with reference + value: + TestSiteID: 5 + SiteID: 1 + TestSiteCode: EGFR + TestSiteName: eGFR + TestType: CALC + DisciplineID: 2 + DepartmentID: 2 + Unit1: mL/min/1.73m2 + Decimal: 0 + SeqScr: 20 + SeqRpt: 20 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 0 + testdefcal: + - TestCalID: 1 + DisciplineID: 2 + DepartmentID: 2 + FormulaInput: CREA,AGE,GENDER + FormulaCode: CKD_EPI(CREA,AGE,GENDER) + Unit1: mL/min/1.73m2 + Decimal: 0 + refnum: + - RefNumID: 5 + NumRefType: NMRC + NumRefTypeLabel: Numeric + RangeType: REF + RangeTypeLabel: Reference Range + Sex: '1' + SexLabel: Female + LowSign: GE + LowSignLabel: '>=' + HighSign: LE + HighSignLabel: <= + Low: 10 + High: 20 + AgeStart: 18 + AgeEnd: 120 + Flag: 'N' + Interpretation: Normal GROUP: - summary: Panel/Profile - no reference range allowed + summary: Panel/Profile test value: - id: 4 - TestCode: LIPID - TestName: Lipid Panel + TestSiteID: 6 + SiteID: 1 + TestSiteCode: LIPID + TestSiteName: Lipid Panel TestType: GROUP - DisciplineID: 1 - DepartmentID: 1 - Unit: null + DisciplineID: 2 + DepartmentID: 2 + SeqScr: 51 + SeqRpt: 51 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + testdefgrp: + - TestGrpID: 1 + Member: 100 + TestSiteCode: CHOL + TestSiteName: Total Cholesterol + - TestGrpID: 2 + Member: 101 + TestSiteCode: TG + TestSiteName: Triglycerides TITLE: - summary: Section header - no reference range allowed + summary: Section header value: - id: 5 - TestCode: CHEM_HEADER - TestName: '--- CHEMISTRY ---' + TestSiteID: 7 + SiteID: 1 + TestSiteCode: CHEM_HEADER + TestSiteName: '--- CHEMISTRY ---' TestType: TITLE - DisciplineID: 1 - DepartmentID: 1 - Unit: null + DisciplineID: 2 + DepartmentID: 2 + SeqScr: 100 + SeqRpt: 100 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 0 TestMap: type: object properties: - id: - type: integer TestMapID: type: integer - TestCode: - type: string - HostCode: - type: string - HostName: - type: string - ClientCode: - type: string - ClientName: - type: string + TestSiteID: + type: integer HostType: type: string description: Host type code - HostTypeLabel: + HostID: type: string - description: Host type display text + description: Host identifier + HostDataSource: + type: string + description: Host data source + 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 - ClientTypeLabel: + ClientID: type: string - description: Client type display text + description: Client identifier + ClientDataSource: + type: string + description: Client data source + 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 + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + description: Soft delete timestamp OrderTest: type: object properties: diff --git a/public/components/schemas/tests.yaml b/public/components/schemas/tests.yaml index d77ddef..1621859 100644 --- a/public/components/schemas/tests.yaml +++ b/public/components/schemas/tests.yaml @@ -1,11 +1,13 @@ TestDefinition: type: object properties: - id: + TestSiteID: type: integer - TestCode: + SiteID: + type: integer + TestSiteCode: type: string - TestName: + TestSiteName: type: string TestType: type: string @@ -16,6 +18,8 @@ TestDefinition: CALC: Calculated GROUP: Panel/Profile TITLE: Section header + Description: + type: string DisciplineID: type: integer DisciplineName: @@ -24,10 +28,106 @@ TestDefinition: type: integer DepartmentName: type: string - Unit: + ResultType: type: string - Formula: + enum: [NMRIC, VSET] + description: | + NMRIC: Numeric result + VSET: Value set result + RefType: type: string + enum: [NMRC, TEXT, THOLD, VSET] + description: | + NMRC: Numeric reference range + TEXT: Text reference + THOLD: Threshold reference + VSET: Value set reference + VSet: + type: integer + description: Value set ID for VSET result type + ReqQty: + type: number + format: decimal + description: Required sample quantity + ReqQtyUnit: + type: string + description: Unit for required quantity + Unit1: + type: string + description: Primary unit + Factor: + type: number + format: decimal + description: Conversion factor + Unit2: + type: string + description: Secondary unit (after conversion) + Decimal: + type: integer + description: Number of decimal places + CollReq: + type: string + description: Collection requirements + Method: + type: string + description: Test method + ExpectedTAT: + type: integer + description: Expected turnaround time + SeqScr: + type: integer + description: Screen sequence + SeqRpt: + type: integer + description: Report sequence + IndentLeft: + type: integer + default: 0 + FontStyle: + type: string + VisibleScr: + type: integer + default: 1 + description: Screen visibility (0=hidden, 1=visible) + VisibleRpt: + type: integer + default: 1 + description: Report visibility (0=hidden, 1=visible) + CountStat: + type: integer + default: 1 + Level: + type: integer + CreateDate: + type: string + format: date-time + StartDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + FormulaInput: + type: string + description: Input variables for calculated tests + FormulaCode: + type: string + description: Formula expression for calculated tests + testdefcal: + type: array + description: Calculated test details (only for CALC type) + items: + type: object + testdefgrp: + type: array + description: Group members (only for GROUP type) + items: + type: object + testmap: + type: array + description: Test mappings + items: + $ref: '#/TestMap' refnum: type: array description: Numeric reference ranges (optional). Mutually exclusive with reftxt - a test can only have ONE reference type. @@ -99,9 +199,29 @@ TestDefinition: Flag: type: string examples: - $1: - Unit: mg/dL - refnum: + TEST_numeric: + summary: Technical test with numeric reference + value: + TestSiteID: 1 + SiteID: 1 + TestSiteCode: GLU + TestSiteName: Glucose + TestType: TEST + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: NMRC + Unit1: mg/dL + ReqQty: 300 + ReqQtyUnit: uL + Decimal: 0 + Method: Hexokinase + SeqScr: 11 + SeqRpt: 11 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + refnum: - RefNumID: 1 NumRefType: NMRC NumRefTypeLabel: Numeric @@ -119,16 +239,26 @@ TestDefinition: AgeEnd: 99 Flag: N Interpretation: Normal - TEST_numeric_thold: - summary: Technical test - threshold reference (THOLD) + TEST_threshold: + summary: Technical test with threshold reference (panic) value: - id: 1 - TestCode: GLU - TestName: Glucose + TestSiteID: 2 + SiteID: 1 + TestSiteCode: GLU + TestSiteName: Glucose TestType: TEST - DisciplineID: 1 - DepartmentID: 1 - Unit: mg/dL + DisciplineID: 2 + DepartmentID: 2 + ResultType: NMRIC + RefType: THOLD + Unit1: mg/dL + Decimal: 0 + Method: Hexokinase + SeqScr: 11 + SeqRpt: 11 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 refnum: - RefNumID: 2 NumRefType: THOLD @@ -144,9 +274,24 @@ TestDefinition: AgeEnd: 120 Flag: L Interpretation: Critical Low - $2: - Unit: null - reftxt: + TEST_text: + summary: Technical test with text reference + value: + TestSiteID: 3 + SiteID: 1 + TestSiteCode: STAGE + TestSiteName: Disease Stage + TestType: TEST + DisciplineID: 1 + DepartmentID: 1 + ResultType: VSET + RefType: TEXT + SeqScr: 50 + SeqRpt: 50 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + reftxt: - RefTxtID: 1 TxtRefType: TEXT TxtRefTypeLabel: Text @@ -156,123 +301,150 @@ TestDefinition: AgeEnd: 99 RefTxt: 'NORM=Normal;HYPO=Hypochromic;MACRO=Macrocytic' Flag: N - TEST_text_vset: - summary: Technical test - text reference (VSET) + PARAM: + summary: Parameter test value: - id: 1 - TestCode: STAGE - TestName: Disease Stage - TestType: TEST - DisciplineID: 1 - DepartmentID: 1 - Unit: null - reftxt: - - RefTxtID: 2 - TxtRefType: VSET - TxtRefTypeLabel: Value Set + TestSiteID: 4 + SiteID: 1 + TestSiteCode: HEIGHT + TestSiteName: Height + TestType: PARAM + DisciplineID: 10 + ResultType: NMRIC + Unit1: cm + Decimal: 0 + SeqScr: 40 + SeqRpt: 40 + VisibleScr: 1 + VisibleRpt: 0 + CountStat: 0 + CALC: + summary: Calculated test with reference + value: + TestSiteID: 5 + SiteID: 1 + TestSiteCode: EGFR + TestSiteName: eGFR + TestType: CALC + DisciplineID: 2 + DepartmentID: 2 + Unit1: mL/min/1.73m2 + Decimal: 0 + SeqScr: 20 + SeqRpt: 20 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 0 + testdefcal: + - TestCalID: 1 + DisciplineID: 2 + DepartmentID: 2 + FormulaInput: CREA,AGE,GENDER + FormulaCode: CKD_EPI(CREA,AGE,GENDER) + Unit1: mL/min/1.73m2 + Decimal: 0 + refnum: + - RefNumID: 5 + NumRefType: NMRC + NumRefTypeLabel: Numeric + RangeType: REF + RangeTypeLabel: Reference Range Sex: '1' SexLabel: Female - AgeStart: 0 + LowSign: GE + LowSignLabel: ">=" + HighSign: LE + HighSignLabel: "<=" + Low: 10 + High: 20 + AgeStart: 18 AgeEnd: 120 - RefTxt: 'STG1=Stage 1;STG2=Stage 2;STG3=Stage 3;STG4=Stage 4' Flag: N - PARAM: - summary: Parameter - no reference range allowed - value: - id: 2 - TestCode: GLU_FAST - TestName: Fasting Glucose - TestType: PARAM - DisciplineID: 1 - DepartmentID: 1 - Unit: mg/dL - $3: - Unit: null - Formula: "BUN / Creatinine" - refnum: - - RefNumID: 5 - NumRefType: NMRC - NumRefTypeLabel: Numeric - RangeType: REF - RangeTypeLabel: Reference Range - Sex: '1' - SexLabel: Female - LowSign: GE - LowSignLabel: ">=" - HighSign: LE - HighSignLabel: "<=" - Low: 10 - High: 20 - AgeStart: 18 - AgeEnd: 120 - Flag: N - Interpretation: Normal - $4: - Unit: null - Formula: "BUN / Creatinine" - refnum: - - RefNumID: 6 - NumRefType: THOLD - NumRefTypeLabel: Threshold - RangeType: PANIC - RangeTypeLabel: Panic Range - Sex: '1' - SexLabel: Female - LowSign: GT - LowSignLabel: ">" - Low: 20 - AgeStart: 18 - AgeEnd: 120 - Flag: H - Interpretation: Elevated - possible prerenal cause - + Interpretation: Normal GROUP: - summary: Panel/Profile - no reference range allowed + summary: Panel/Profile test value: - id: 4 - TestCode: LIPID - TestName: Lipid Panel + TestSiteID: 6 + SiteID: 1 + TestSiteCode: LIPID + TestSiteName: Lipid Panel TestType: GROUP - DisciplineID: 1 - DepartmentID: 1 - Unit: null + DisciplineID: 2 + DepartmentID: 2 + SeqScr: 51 + SeqRpt: 51 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 1 + testdefgrp: + - TestGrpID: 1 + Member: 100 + TestSiteCode: CHOL + TestSiteName: Total Cholesterol + - TestGrpID: 2 + Member: 101 + TestSiteCode: TG + TestSiteName: Triglycerides TITLE: - summary: Section header - no reference range allowed + summary: Section header value: - id: 5 - TestCode: CHEM_HEADER - TestName: '--- CHEMISTRY ---' + TestSiteID: 7 + SiteID: 1 + TestSiteCode: CHEM_HEADER + TestSiteName: '--- CHEMISTRY ---' TestType: TITLE - DisciplineID: 1 - DepartmentID: 1 - Unit: null + DisciplineID: 2 + DepartmentID: 2 + SeqScr: 100 + SeqRpt: 100 + VisibleScr: 1 + VisibleRpt: 1 + CountStat: 0 TestMap: type: object properties: - id: - type: integer TestMapID: type: integer - TestCode: - type: string - HostCode: - type: string - HostName: - type: string - ClientCode: - type: string - ClientName: - type: string + TestSiteID: + type: integer HostType: type: string description: Host type code - HostTypeLabel: + HostID: type: string - description: Host type display text + description: Host identifier + HostDataSource: + type: string + description: Host data source + 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 - ClientTypeLabel: + ClientID: type: string - description: Client type display text + description: Client identifier + ClientDataSource: + type: string + description: Client data source + 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 + CreateDate: + type: string + format: date-time + EndDate: + type: string + format: date-time + description: Soft delete timestamp diff --git a/public/paths/tests.yaml b/public/paths/tests.yaml index 8a6bfb1..7cb9474 100644 --- a/public/paths/tests.yaml +++ b/public/paths/tests.yaml @@ -76,10 +76,107 @@ content: application/json: schema: - $ref: '../components/schemas/tests.yaml#/TestDefinition' + type: object + properties: + SiteID: + type: integer + description: Site ID (required) + TestSiteCode: + type: string + description: Test code (required) + TestSiteName: + type: string + description: Test name (required) + TestType: + type: string + enum: [TEST, PARAM, CALC, GROUP, TITLE] + description: Test type (required) + Description: + type: string + DisciplineID: + type: integer + DepartmentID: + type: integer + ResultType: + type: string + enum: [NMRIC, VSET] + RefType: + type: string + enum: [NMRC, TEXT, THOLD, VSET] + VSet: + type: integer + ReqQty: + type: number + format: decimal + ReqQtyUnit: + type: string + Unit1: + type: string + Factor: + type: number + format: decimal + Unit2: + type: string + Decimal: + type: integer + CollReq: + type: string + Method: + type: string + ExpectedTAT: + type: integer + SeqScr: + type: integer + SeqRpt: + type: integer + IndentLeft: + type: integer + FontStyle: + type: string + VisibleScr: + type: integer + VisibleRpt: + type: integer + CountStat: + type: integer + details: + type: object + description: Type-specific details + refnum: + type: array + items: + type: object + reftxt: + type: array + items: + type: object + testmap: + type: array + items: + type: object + required: + - SiteID + - TestSiteCode + - TestSiteName + - TestType responses: '201': description: Test definition created + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: created + message: + type: string + data: + type: object + properties: + TestSiteId: + type: integer patch: tags: [Tests] @@ -91,10 +188,101 @@ content: application/json: schema: - $ref: '../components/schemas/tests.yaml#/TestDefinition' + type: object + properties: + TestSiteID: + type: integer + description: Test Site ID (required) + TestSiteCode: + type: string + TestSiteName: + type: string + TestType: + type: string + enum: [TEST, PARAM, CALC, GROUP, TITLE] + Description: + type: string + DisciplineID: + type: integer + DepartmentID: + type: integer + ResultType: + type: string + enum: [NMRIC, VSET] + RefType: + type: string + enum: [NMRC, TEXT, THOLD, VSET] + VSet: + type: integer + ReqQty: + type: number + format: decimal + ReqQtyUnit: + type: string + Unit1: + type: string + Factor: + type: number + format: decimal + Unit2: + type: string + Decimal: + type: integer + CollReq: + type: string + Method: + type: string + ExpectedTAT: + type: integer + SeqScr: + type: integer + SeqRpt: + type: integer + IndentLeft: + type: integer + FontStyle: + type: string + VisibleScr: + type: integer + VisibleRpt: + type: integer + CountStat: + type: integer + details: + type: object + description: Type-specific details + refnum: + type: array + items: + type: object + reftxt: + type: array + items: + type: object + testmap: + type: array + items: + type: object + required: + - TestSiteID responses: '200': description: Test definition updated + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + TestSiteId: + type: integer /api/tests/{id}: get: @@ -108,6 +296,67 @@ required: true schema: type: integer + description: Test Site ID responses: '200': description: Test definition details + content: + application/json: + schema: + type: object + properties: + status: + type: string + message: + type: string + data: + $ref: '../components/schemas/tests.yaml#/TestDefinition' + '404': + description: Test not found + + delete: + tags: [Tests] + summary: Soft delete test definition + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + schema: + type: integer + description: Test Site ID to delete + requestBody: + content: + application/json: + schema: + type: object + properties: + TestSiteID: + type: integer + description: Optional - can be provided in body instead of path + responses: + '200': + description: Test disabled successfully + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + TestSiteId: + type: integer + EndDate: + type: string + format: date-time + '404': + description: Test not found + '422': + description: Test already disabled diff --git a/tests/unit/TestDef/TestDefModelsTest.php b/tests/unit/TestDef/TestDefModelsTest.php index d070c67..662356d 100644 --- a/tests/unit/TestDef/TestDefModelsTest.php +++ b/tests/unit/TestDef/TestDefModelsTest.php @@ -4,7 +4,7 @@ namespace Tests\Unit\TestDef; use CodeIgniter\Test\CIUnitTestCase; use App\Models\Test\TestDefSiteModel; -use App\Models\Test\TestDefTechModel; + use App\Models\Test\TestDefCalModel; use App\Models\Test\TestDefGrpModel; use App\Models\Test\TestMapModel; @@ -12,7 +12,7 @@ use App\Models\Test\TestMapModel; class TestDefModelsTest extends CIUnitTestCase { protected $testDefSiteModel; - protected $testDefTechModel; + protected $testDefCalModel; protected $testDefGrpModel; protected $testMapModel; @@ -21,7 +21,7 @@ class TestDefModelsTest extends CIUnitTestCase { parent::setUp(); $this->testDefSiteModel = new TestDefSiteModel(); - $this->testDefTechModel = new TestDefTechModel(); + $this->testDefCalModel = new TestDefCalModel(); $this->testDefGrpModel = new TestDefGrpModel(); $this->testMapModel = new TestMapModel(); @@ -76,43 +76,6 @@ class TestDefModelsTest extends CIUnitTestCase $this->assertEquals('EndDate', $this->testDefSiteModel->deletedField); } - /** - * Test TestDefTechModel has correct table name - */ - public function testTestDefTechModelTable() - { - $this->assertEquals('testdeftech', $this->testDefTechModel->table); - } - - /** - * Test TestDefTechModel has correct primary key - */ - public function testTestDefTechModelPrimaryKey() - { - $this->assertEquals('TestTechID', $this->testDefTechModel->primaryKey); - } - - /** - * Test TestDefTechModel has correct allowed fields - */ - public function testTestDefTechModelAllowedFields() - { - $allowedFields = $this->testDefTechModel->allowedFields; - - $this->assertContains('TestSiteID', $allowedFields); - $this->assertContains('DisciplineID', $allowedFields); - $this->assertContains('DepartmentID', $allowedFields); - $this->assertContains('ResultType', $allowedFields); - $this->assertContains('RefType', $allowedFields); - $this->assertContains('VSet', $allowedFields); - $this->assertContains('Unit1', $allowedFields); - $this->assertContains('Factor', $allowedFields); - $this->assertContains('Unit2', $allowedFields); - $this->assertContains('Decimal', $allowedFields); - $this->assertContains('Method', $allowedFields); - $this->assertContains('ExpectedTAT', $allowedFields); - } - /** * Test TestDefCalModel has correct table name */ @@ -218,7 +181,6 @@ class TestDefModelsTest extends CIUnitTestCase */ public function testAllModelsUseSoftDeletes() { - $this->assertTrue($this->testDefTechModel->useSoftDeletes); $this->assertTrue($this->testDefCalModel->useSoftDeletes); $this->assertTrue($this->testDefGrpModel->useSoftDeletes); $this->assertTrue($this->testMapModel->useSoftDeletes); @@ -229,7 +191,6 @@ class TestDefModelsTest extends CIUnitTestCase */ public function testAllModelsUseEndDateAsDeletedField() { - $this->assertEquals('EndDate', $this->testDefTechModel->deletedField); $this->assertEquals('EndDate', $this->testDefCalModel->deletedField); $this->assertEquals('EndDate', $this->testDefGrpModel->deletedField); $this->assertEquals('EndDate', $this->testMapModel->deletedField);