refactor: reorganize documentation and update test-related files
- Remove deprecated docs folder with outdated documentation - Add new plans directory with ref_range_multiple_support_plan.md - Update test migrations, seeds, and views for improved functionality
This commit is contained in:
parent
97edfe50a8
commit
9e0b01e7e2
16
README.md
16
README.md
@ -50,6 +50,22 @@ Key documents:
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Valueset Reference (VSetDefID)
|
||||
|
||||
When working on UI components or dropdowns, **always check for existing ValueSets** before hardcoding options. Use the API endpoint `/api/valueset/valuesetdef/{ID}` to fetch options dynamically.
|
||||
|
||||
| VSetDefID | Purpose | Usage |
|
||||
|-----------|---------|-------|
|
||||
| 27 | Test Types | TEST, PARAM, GROUP, CALC, TITLE |
|
||||
| 28 | Methods | Lab test methods |
|
||||
| 29 | Specimen Types | Blood, Urine, etc. |
|
||||
| 30 | Ref Types | NMRC (Numeric), TEXT, LIST |
|
||||
| 31 | Range Types | STD (Standard), AGSX (Age/Sex), COND |
|
||||
|
||||
> **Important:** Always use ValueSet lookups for configurable options. This ensures consistency and allows administrators to modify options without code changes.
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Edge API - Instrument Integration
|
||||
|
||||
The **Edge API** provides endpoints for integrating laboratory instruments via the `tiny-edge` middleware. Results from instruments are staged in the `edgeres` table before processing into the main patient results (`patres`).
|
||||
|
||||
@ -31,7 +31,7 @@ class CreateTestsTable extends Migration {
|
||||
// testdeftech - Technical definition for TEST and PARAM types
|
||||
$this->forge->addField([
|
||||
'TestTechID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
|
||||
'DisciplineID' => ['type' => 'int', 'null' => true],
|
||||
'DepartmentID' => ['type' => 'int', 'null' => true],
|
||||
'ResultType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
@ -56,7 +56,7 @@ class CreateTestsTable extends Migration {
|
||||
// testdefcal - Calculation definition for CALC type
|
||||
$this->forge->addField([
|
||||
'TestCalID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
|
||||
'DisciplineID' => ['type' => 'INT', 'null' => true],
|
||||
'DepartmentID' => ['type' => 'INT', 'null' => true],
|
||||
'FormulaInput' => ['type' => 'text', 'null' => true],
|
||||
@ -77,8 +77,8 @@ class CreateTestsTable extends Migration {
|
||||
// testdefgrp - Group definition for GROUP type
|
||||
$this->forge->addField([
|
||||
'TestGrpID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'Member' => ['type' => 'INT', 'null' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
|
||||
'Member' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
||||
]);
|
||||
@ -90,7 +90,7 @@ class CreateTestsTable extends Migration {
|
||||
// testmap - Test mapping for all types
|
||||
$this->forge->addField([
|
||||
'TestMapID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteID' => ['type' => 'INT', 'unsigned' => true, 'null' => false],
|
||||
'HostType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'HostID' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
'HostDataSource' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||
|
||||
@ -25,104 +25,104 @@ class TestSeeder extends Seeder {
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'HB', 'TestSiteName' => 'Hemoglobin', 'TestType' => $vs[27]['TEST'], 'Description' => '', 'SeqScr' => '2', 'SeqRpt' => '2', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['HB'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['HB'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['HB'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => '', 'SeqScr' => '3', 'SeqRpt' => '3', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['HCT'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['HCT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => '%', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['HCT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Eritrosit', 'SeqScr' => '4', 'SeqRpt' => '4', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['RBC'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['RBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^6/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['RBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Leukosit', 'SeqScr' => '5', 'SeqRpt' => '5', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['WBC'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['WBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '2', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['WBC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Trombosit', 'SeqScr' => '6', 'SeqRpt' => '6', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['PLT'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['PLT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'x10^3/uL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['PLT'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Mean Corpuscular Volume', 'SeqScr' => '7', 'SeqRpt' => '7', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['MCV'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['MCV'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'fL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['MCV'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Mean Corpuscular Hemoglobin', 'SeqScr' => '8', 'SeqRpt' => '8', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['MCH'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['MCH'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'pg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['MCH'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Mean Corpuscular Hemoglobin Concentration', 'SeqScr' => '9', 'SeqRpt' => '9', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['MCHC'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['MCHC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['BLD'], 'ReqQty' => '500', 'ReqQtyUnit' => 'uL', 'Unit1' => 'g/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'CBC Analyzer', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['MCHC'], 'DisciplineID' => '1', 'DepartmentID' => '1', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Glukosa Sewaktu', 'SeqScr' => '11', 'SeqRpt' => '11', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['GLU'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['GLU'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '0.0555', 'Unit2' => 'mmol/L', 'Decimal' => '0', 'Method' => 'Hexokinase', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['GLU'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Kreatinin', 'SeqScr' => '12', 'SeqRpt' => '12', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['CREA'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['CREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '88.4', 'Unit2' => 'umol/L', 'Decimal' => '2', 'Method' => 'Enzymatic', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['CREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'BUN', 'SeqScr' => '13', 'SeqRpt' => '13', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['UREA'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['UREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Urease-GLDH', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['UREA'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Aspartate Aminotransferase', 'SeqScr' => '14', 'SeqRpt' => '14', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['SGOT'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['SGOT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['SGOT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Alanine Aminotransferase', 'SeqScr' => '15', 'SeqRpt' => '15', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['SGPT'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['SGPT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'U/L', 'Factor' => '0.017', 'Unit2' => 'ukat/L', 'Decimal' => '0', 'Method' => 'IFCC', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['SGPT'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Kolesterol Total', 'SeqScr' => '16', 'SeqRpt' => '16', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['CHOL'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['CHOL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Enzymatic', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['CHOL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Trigliserida', 'SeqScr' => '17', 'SeqRpt' => '17', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['TG'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['TG'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'GPO-PAP', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['TG'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Kolesterol HDL', 'SeqScr' => '18', 'SeqRpt' => '18', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['HDL'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['HDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['HDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Kolesterol LDL', 'SeqScr' => '19', 'SeqRpt' => '19', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['LDL'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['LDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['SER'], 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['LDL'], 'DisciplineID' => '2', 'DepartmentID' => '2', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '300', 'ReqQtyUnit' => 'uL', 'Unit1' => 'mg/dL', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => 'Direct', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
// ========================================
|
||||
@ -131,31 +131,31 @@ class TestSeeder extends Seeder {
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'HEIGHT', 'TestSiteName' => 'Height', 'TestType' => $vs[27]['PARAM'], 'Description' => 'Tinggi Badan', 'SeqScr' => '40', 'SeqRpt' => '40', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['HEIGHT'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['HEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'cm', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['HEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['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' => $vs[27]['PARAM'], 'Description' => 'Berat Badan', 'SeqScr' => '41', 'SeqRpt' => '41', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['WEIGHT'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['WEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'kg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['WEIGHT'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['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' => $vs[27]['PARAM'], 'Description' => 'Usia', 'SeqScr' => '42', 'SeqRpt' => '42', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['AGE'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['AGE'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'years', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['AGE'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['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' => $vs[27]['PARAM'], 'Description' => 'Tekanan Darah Sistolik', 'SeqScr' => '43', 'SeqRpt' => '43', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['SYSTL'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['SYSTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['SYSTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['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' => $vs[27]['PARAM'], 'Description' => 'Tekanan Darah Diastolik', 'SeqScr' => '44', 'SeqRpt' => '44', 'IndentLeft' => '0', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][0], 'CountStat' => $vs[2][0], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['DIASTL'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['DIASTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'SpcType' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['DIASTL'], 'DisciplineID' => '10', 'DepartmentID' => '', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => '', 'VSet' => '', 'ReqQty' => '', 'ReqQtyUnit' => '', 'Unit1' => 'mmHg', 'Factor' => '', 'Unit2' => '', 'Decimal' => '0', 'Method' => '', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
// ========================================
|
||||
@ -228,25 +228,25 @@ class TestSeeder extends Seeder {
|
||||
$data = ['SiteID' => '1', 'TestSiteCode' => 'UCOLOR', 'TestSiteName' => 'Urine Color', 'TestType' => $vs[27]['TEST'], 'Description' => 'Warna Urine', 'SeqScr' => '31', 'SeqRpt' => '31', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['UCOLOR'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['UCOLOR'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['TEXT'], 'VSet' => '1001', 'SpcType' => $vs[15]['UR'], 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Visual', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['UCOLOR'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Glukosa Urine', 'SeqScr' => '32', 'SeqRpt' => '32', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['UGLUC'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['UGLUC'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['TEXT'], 'VSet' => '1002', 'SpcType' => $vs[15]['UR'], 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['UGLUC'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'Protein Urine', 'SeqScr' => '33', 'SeqRpt' => '33', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['UPROT'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['UPROT'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['TEXT'], 'VSet' => '1003', 'SpcType' => $vs[15]['UR'], 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['UPROT'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['VSET'], 'RefType' => $vs[44]['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' => $vs[27]['TEST'], 'Description' => 'pH Urine', 'SeqScr' => '34', 'SeqRpt' => '34', 'IndentLeft' => '1', 'VisibleScr' => $vs[2][1], 'VisibleRpt' => $vs[2][1], 'CountStat' => $vs[2][1], 'CreateDate' => "$now"];
|
||||
$this->db->table('testdefsite')->insert($data);
|
||||
$tIDs['PH'] = $this->db->insertID();
|
||||
$data = ['TestSiteID' => $tIDs['PH'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'SpcType' => $vs[15]['UR'], 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$data = ['TestSiteID' => $tIDs['PH'], 'DisciplineID' => '4', 'DepartmentID' => '4', 'ResultType' => $vs[43]['NMRIC'], 'RefType' => $vs[44]['NMRC'], 'VSet' => '', 'ReqQty' => '10', 'ReqQtyUnit' => 'mL', 'Unit1' => '', 'Factor' => '', 'Unit2' => '', 'Decimal' => '1', 'Method' => 'Dipstick', 'CreateDate' => "$now"];
|
||||
$this->db->table('testdeftech')->insert($data);
|
||||
|
||||
}
|
||||
|
||||
@ -32,9 +32,32 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b mb-6" style="border-color: rgb(var(--color-border));">
|
||||
<button
|
||||
class="px-6 py-2 font-medium text-sm transition-colors border-b-2"
|
||||
:class="form.dialogTab === 'general' ? 'border-primary text-primary' : 'border-transparent text-slate-500 hover:text-slate-700'"
|
||||
style="--tw-text-opacity: 1; border-color: transition;"
|
||||
@click="form.dialogTab = 'general'"
|
||||
:style="form.dialogTab === 'general' ? 'border-color: rgb(var(--color-primary)); color: rgb(var(--color-primary));' : ''"
|
||||
>
|
||||
General
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-2 font-medium text-sm transition-colors border-b-2"
|
||||
:class="form.dialogTab === 'reff' ? 'border-primary text-primary' : 'border-transparent text-slate-500 hover:text-slate-700'"
|
||||
@click="form.dialogTab = 'reff'"
|
||||
:style="form.dialogTab === 'reff' ? 'border-color: rgb(var(--color-primary)); color: rgb(var(--color-primary));' : ''"
|
||||
>
|
||||
Reff
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form Content -->
|
||||
<div class="space-y-4">
|
||||
|
||||
<!-- General Tab -->
|
||||
<div x-show="form.dialogTab === 'general'" class="space-y-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
@ -135,7 +158,95 @@
|
||||
<span class="label-text">Count in Statistics</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reff Tab -->
|
||||
<div x-show="form.dialogTab === 'reff'" class="space-y-4" x-cloak>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Ref Type</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.RefType">
|
||||
<option value="">Select Ref Type</option>
|
||||
<template x-for="rt in refTypesList" :key="rt.VID">
|
||||
<option :value="rt.VValue" x-text="rt.VDesc"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Numeric Reference Range -->
|
||||
<template x-if="form.RefType === 'NMRC'">
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Ref Low</span></label>
|
||||
<input type="text" class="input" x-model="form.RefLow" placeholder="0.00" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Ref High</span></label>
|
||||
<input type="text" class="input" x-model="form.RefHigh" placeholder="10.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 border-t pt-4" style="border-color: rgb(var(--color-border));">
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium text-error">Crit Low</span></label>
|
||||
<input type="text" class="input border-error/30" x-model="form.CritLow" placeholder="0.00" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium text-error">Crit High</span></label>
|
||||
<input type="text" class="input border-error/30" x-model="form.CritHigh" placeholder="20.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Unit</span></label>
|
||||
<input type="text" class="input" x-model="form.Unit1" placeholder="mg/dL" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Decimals</span></label>
|
||||
<input type="number" class="input" x-model="form.Decimal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<!-- Descriptive Text -->
|
||||
<template x-if="form.RefType === 'TEXT'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Default Reference Text</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-32 pt-2"
|
||||
x-model="form.RefText"
|
||||
placeholder="e.g. Negative"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- List / Value Set -->
|
||||
<template x-if="form.RefType === 'LIST'">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Select Value Set</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.RefVSet">
|
||||
<option value="">Select a value set...</option>
|
||||
<template x-for="v in vsetDefsList" :key="v.VSetDefID">
|
||||
<option :value="v.VSetDefID" x-text="v.VSDesc"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div class="mt-4 p-4 rounded-lg bg-blue-50 border border-blue-100 flex items-start gap-3">
|
||||
<i class="fa-solid fa-circle-info text-blue-500 mt-0.5"></i>
|
||||
<p class="text-xs text-blue-700 leading-relaxed">
|
||||
Selecting a value set will restrict result entry to predefined values and use them for reference matching.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
|
||||
@ -160,6 +160,8 @@ function labTests() {
|
||||
workstationsList: [],
|
||||
equipmentList: [],
|
||||
containersList: [],
|
||||
refTypesList: [],
|
||||
vsetDefsList: [],
|
||||
availableTests: [],
|
||||
keyword: "",
|
||||
|
||||
@ -186,6 +188,17 @@ function labTests() {
|
||||
VisibleRpt: 1,
|
||||
CountStat: 1,
|
||||
StartDate: "",
|
||||
// Reference Configuration
|
||||
dialogTab: 'general', // general, reff
|
||||
RefType: 'NMRC', // NMRC, TEXT, LIST
|
||||
RefLow: "",
|
||||
RefHigh: "",
|
||||
CritLow: "",
|
||||
CritHigh: "",
|
||||
Unit1: "",
|
||||
Decimal: 2,
|
||||
RefText: "",
|
||||
RefVSet: "",
|
||||
// Technical fields (TEST, PARAM)
|
||||
DisciplineID: "",
|
||||
DepartmentID: "",
|
||||
@ -234,6 +247,8 @@ function labTests() {
|
||||
await this.fetchMethods();
|
||||
await this.fetchSpecimenTypes();
|
||||
await this.fetchContainers();
|
||||
await this.fetchRefTypes();
|
||||
await this.fetchVsetDefs();
|
||||
// Additional data can be loaded on demand when specific dialogs are opened
|
||||
|
||||
// Watch for typesList changes to ensure dropdown is populated
|
||||
@ -280,27 +295,22 @@ function labTests() {
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch methods
|
||||
// Fetch methods - Note: Methods might be custom per lab, not from a standard valueset
|
||||
// If you need a valueset for methods, create a custom one or use a different approach
|
||||
async fetchMethods() {
|
||||
try {
|
||||
// Methods could be fetched from a valueset or endpoint
|
||||
const res = await fetch(`${BASEURL}api/valueset/valuesetdef/28`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.methodsList = data.data || [];
|
||||
}
|
||||
// For now, methods list will be empty or fetched from a different source
|
||||
// You may want to create a custom valueset for lab methods
|
||||
this.methodsList = [];
|
||||
} catch (err) {
|
||||
// Silently fail - use empty array
|
||||
this.methodsList = [];
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch specimen types from valueset
|
||||
// Fetch specimen types from valueset (VSetDefID 15)
|
||||
async fetchSpecimenTypes() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valueset/valuesetdef/29`, {
|
||||
const res = await fetch(`${BASEURL}api/valueset/valuesetdef/15`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (res.ok) {
|
||||
@ -327,6 +337,36 @@ function labTests() {
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch ref types from valueset (VSetDefID 44)
|
||||
async fetchRefTypes() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valueset/valuesetdef/44`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.refTypesList = data.data || [];
|
||||
}
|
||||
} catch (err) {
|
||||
this.refTypesList = [];
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch all value set definitions
|
||||
async fetchVsetDefs() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valueset/valuesetdef`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.vsetDefsList = data.data || [];
|
||||
}
|
||||
} catch (err) {
|
||||
this.vsetDefsList = [];
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch lab test list
|
||||
async fetchList() {
|
||||
this.loading = true;
|
||||
@ -401,13 +441,23 @@ function labTests() {
|
||||
VisibleRpt: 1,
|
||||
CountStat: 1,
|
||||
StartDate: new Date().toISOString().split('T')[0],
|
||||
// Reference Configuration
|
||||
dialogTab: 'general',
|
||||
RefType: '',
|
||||
RefLow: "",
|
||||
RefHigh: "",
|
||||
CritLow: "",
|
||||
CritHigh: "",
|
||||
Unit1: "",
|
||||
Decimal: 2,
|
||||
RefText: "",
|
||||
RefVSet: "",
|
||||
// Technical fields
|
||||
DisciplineID: "",
|
||||
DepartmentID: "",
|
||||
WorkstationID: "",
|
||||
EquipmentID: "",
|
||||
ResultType: "",
|
||||
RefType: "",
|
||||
VSet: "",
|
||||
SpcType: "",
|
||||
SpcDesc: "",
|
||||
@ -425,6 +475,9 @@ function labTests() {
|
||||
// CALC fields
|
||||
FormulaInput: "",
|
||||
FormulaCode: "",
|
||||
// Reference fields
|
||||
RefNum: "",
|
||||
RefText: "",
|
||||
// Mapping fields
|
||||
testmap: []
|
||||
};
|
||||
@ -489,6 +542,17 @@ function labTests() {
|
||||
VisibleRpt: testData.VisibleRpt !== undefined ? testData.VisibleRpt : 1,
|
||||
CountStat: testData.CountStat !== undefined ? testData.CountStat : 1,
|
||||
StartDate: testData.StartDate ? testData.StartDate.split('T')[0] : "",
|
||||
// Reference Configuration
|
||||
dialogTab: 'general',
|
||||
RefType: testData.RefType || 'NMRC',
|
||||
RefLow: testData.RefLow || "",
|
||||
RefHigh: testData.RefHigh || "",
|
||||
CritLow: testData.CritLow || "",
|
||||
CritHigh: testData.CritHigh || "",
|
||||
Unit1: testData.Unit1 || "",
|
||||
Decimal: testData.Decimal !== undefined ? testData.Decimal : 2,
|
||||
RefText: testData.RefText || "",
|
||||
RefVSet: testData.RefVSet || "",
|
||||
// Technical fields
|
||||
DisciplineID: "",
|
||||
DepartmentID: "",
|
||||
@ -513,6 +577,9 @@ function labTests() {
|
||||
// CALC fields
|
||||
FormulaInput: "",
|
||||
FormulaCode: "",
|
||||
// Reference fields
|
||||
RefNum: testData.RefNum || "",
|
||||
RefText: testData.RefText || "",
|
||||
// Mapping fields
|
||||
testmap: []
|
||||
};
|
||||
@ -528,6 +595,14 @@ function labTests() {
|
||||
this.form.Unit2 = calData.Unit2 || "";
|
||||
this.form.Decimal = calData.Decimal || 2;
|
||||
this.form.Method = calData.Method || "";
|
||||
// Extract from CALC
|
||||
if (calData.RefType) this.form.RefType = calData.RefType;
|
||||
if (calData.RefLow) this.form.RefLow = calData.RefLow;
|
||||
if (calData.RefHigh) this.form.RefHigh = calData.RefHigh;
|
||||
if (calData.CritLow) this.form.CritLow = calData.CritLow;
|
||||
if (calData.CritHigh) this.form.CritHigh = calData.CritHigh;
|
||||
if (calData.RefText) this.form.RefText = calData.RefText;
|
||||
if (calData.VSet) this.form.RefVSet = calData.VSet;
|
||||
} else if (typeCode === 'GROUP' && testData.testdefgrp) {
|
||||
this.form.members = testData.testdefgrp.map(m => ({
|
||||
TestSiteID: m.Member,
|
||||
@ -555,6 +630,14 @@ function labTests() {
|
||||
this.form.CollReq = techData.CollReq || "";
|
||||
this.form.Method = techData.Method || "";
|
||||
this.form.ExpectedTAT = techData.ExpectedTAT || "";
|
||||
// Extract from TECH
|
||||
if (techData.RefType) this.form.RefType = techData.RefType;
|
||||
if (techData.RefLow) this.form.RefLow = techData.RefLow;
|
||||
if (techData.RefHigh) this.form.RefHigh = techData.RefHigh;
|
||||
if (techData.CritLow) this.form.CritLow = techData.CritLow;
|
||||
if (techData.CritHigh) this.form.CritHigh = techData.CritHigh;
|
||||
if (techData.RefText) this.form.RefText = techData.RefText;
|
||||
if (techData.VSet) this.form.RefVSet = techData.VSet;
|
||||
}
|
||||
|
||||
// Load test mappings
|
||||
@ -648,7 +731,16 @@ function labTests() {
|
||||
VisibleScr: this.form.VisibleScr,
|
||||
VisibleRpt: this.form.VisibleRpt,
|
||||
CountStat: this.form.CountStat,
|
||||
StartDate: this.form.StartDate
|
||||
StartDate: this.form.StartDate,
|
||||
RefType: this.form.RefType,
|
||||
RefLow: this.form.RefLow,
|
||||
RefHigh: this.form.RefHigh,
|
||||
CritLow: this.form.CritLow,
|
||||
CritHigh: this.form.CritHigh,
|
||||
Unit1: this.form.Unit1,
|
||||
Decimal: this.form.Decimal,
|
||||
RefText: this.form.RefText,
|
||||
RefVSet: this.form.RefVSet
|
||||
};
|
||||
|
||||
// Add type-specific details
|
||||
@ -659,11 +751,17 @@ function labTests() {
|
||||
FormulaInput: this.form.FormulaInput,
|
||||
FormulaCode: this.form.FormulaCode,
|
||||
RefType: this.form.RefType || 'NMRC',
|
||||
RefLow: this.form.RefLow,
|
||||
RefHigh: this.form.RefHigh,
|
||||
CritLow: this.form.CritLow,
|
||||
CritHigh: this.form.CritHigh,
|
||||
Unit1: this.form.Unit1,
|
||||
Factor: this.form.Factor,
|
||||
Unit2: this.form.Unit2,
|
||||
Decimal: this.form.Decimal,
|
||||
Method: this.form.Method
|
||||
Method: this.form.Method,
|
||||
RefText: this.form.RefText,
|
||||
VSet: this.form.RefVSet
|
||||
};
|
||||
} else if (this.form.TypeCode === 'GROUP') {
|
||||
payload.details = {
|
||||
@ -679,7 +777,11 @@ function labTests() {
|
||||
EquipmentID: this.form.EquipmentID,
|
||||
ResultType: this.form.ResultType,
|
||||
RefType: this.form.RefType,
|
||||
VSet: this.form.VSet,
|
||||
RefLow: this.form.RefLow,
|
||||
RefHigh: this.form.RefHigh,
|
||||
CritLow: this.form.CritLow,
|
||||
CritHigh: this.form.CritHigh,
|
||||
VSet: this.form.RefVSet || this.form.VSet,
|
||||
SpcType: this.form.SpcType,
|
||||
SpcDesc: this.form.SpcDesc,
|
||||
ReqQty: this.form.ReqQty,
|
||||
@ -690,6 +792,7 @@ function labTests() {
|
||||
Decimal: this.form.Decimal,
|
||||
CollReq: this.form.CollReq,
|
||||
Method: this.form.Method,
|
||||
RefText: this.form.RefText,
|
||||
ExpectedTAT: this.form.ExpectedTAT
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,363 +0,0 @@
|
||||
# CLQMS Database Design Review Report
|
||||
|
||||
**Prepared by:** Claude OPUS
|
||||
**Date:** December 12, 2025
|
||||
**Subject:** Technical Assessment of Current Database Schema
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This report presents a technical review of the CLQMS (Clinical Laboratory Quality Management System) database schema based on analysis of 16 migration files containing approximately 45+ tables. While the current design is functional, several critical issues have been identified that impact data integrity, development velocity, and long-term maintainability.
|
||||
|
||||
**Overall Assessment:** The application will function, but the design causes significant developer friction and will create increasing difficulties as the system scales.
|
||||
|
||||
---
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### 1. Missing Foreign Key Constraints
|
||||
|
||||
**Severity:** 🔴 Critical
|
||||
|
||||
The database schema defines **zero foreign key constraints**. All relationships are implemented as integer columns without referential integrity.
|
||||
|
||||
| Impact | Description |
|
||||
|--------|-------------|
|
||||
| Data Integrity | Orphaned records when parent records are deleted |
|
||||
| Data Corruption | Invalid references can be inserted without validation |
|
||||
| Performance | Relationship logic must be enforced in application code |
|
||||
| Debugging | Difficult to trace data lineage across tables |
|
||||
|
||||
**Example:** A patient can be deleted while their visits, orders, and results still reference the deleted `InternalPID`.
|
||||
|
||||
---
|
||||
|
||||
### 2. Test Definition Tables: Broken Relationships
|
||||
|
||||
**Severity:** 🔴 Critical — Impacts API Development
|
||||
|
||||
This issue directly blocks backend development. The test definition system spans **6 tables** with unclear and broken relationships:
|
||||
|
||||
```
|
||||
testdef → Master test catalog (company-wide definitions)
|
||||
testdefsite → Site-specific test configurations
|
||||
testdeftech → Technical settings (units, decimals, methods)
|
||||
testdefcal → Calculated test formulas
|
||||
testgrp → Test panel/profile groupings
|
||||
testmap → Host/Client analyzer code mappings
|
||||
```
|
||||
|
||||
#### The Core Problem: Missing Link Between `testdef` and `testdefsite`
|
||||
|
||||
**`testdef` table structure:**
|
||||
```
|
||||
TestID (PK), Parent, TestCode, TestName, Description, DisciplineID, Method, ...
|
||||
```
|
||||
|
||||
**`testdefsite` table structure:**
|
||||
```
|
||||
TestSiteID (PK), SiteID, TestSiteCode, TestSiteName, TestType, Description, ...
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
> **There is NO `TestID` column in `testdefsite`!**
|
||||
> The relationship between master tests and site-specific configurations is undefined.
|
||||
|
||||
The assumed relationship appears to be matching `TestCode` = `TestSiteCode`, which is:
|
||||
- **Fragile** — codes can change or differ
|
||||
- **Non-performant** — string matching vs integer FK lookup
|
||||
- **Undocumented** — developers must guess
|
||||
|
||||
#### Developer Impact
|
||||
|
||||
**Cannot create sample JSON payloads for API development.**
|
||||
|
||||
To return a complete test with all configurations, we need to JOIN:
|
||||
```
|
||||
testdef
|
||||
→ testdefsite (HOW? No FK exists!)
|
||||
→ testdeftech (via TestSiteID)
|
||||
→ testdefcal (via TestSiteID)
|
||||
→ testgrp (via TestSiteID)
|
||||
→ testmap (via TestSiteID)
|
||||
→ refnum/refthold/refvset/reftxt (via TestSiteID)
|
||||
```
|
||||
|
||||
#### What a Complete Test JSON Should Look Like
|
||||
|
||||
```json
|
||||
{
|
||||
"test": {
|
||||
"id": 1,
|
||||
"code": "GLU",
|
||||
"name": "Glucose",
|
||||
"discipline": "Chemistry",
|
||||
"method": "Hexokinase",
|
||||
"sites": [
|
||||
{
|
||||
"siteId": 1,
|
||||
"siteName": "Main Lab",
|
||||
"unit": "mg/dL",
|
||||
"decimalPlaces": 0,
|
||||
"referenceRange": { "low": 70, "high": 100 },
|
||||
"equipment": [
|
||||
{ "name": "Cobas 6000", "hostCode": "GLU" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"panelMemberships": ["BMP", "CMP"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### What We're Forced to Create Instead
|
||||
|
||||
```json
|
||||
{
|
||||
"testdef": { "TestID": 1, "TestCode": "GLU", "TestName": "Glucose" },
|
||||
"testdefsite": { "TestSiteID": 1, "SiteID": 1, "TestSiteCode": "GLU" },
|
||||
"testdeftech": { "TestTechID": 1, "TestSiteID": 1, "Unit1": "mg/dL" },
|
||||
"refnum": { "RefNumID": 1, "TestSiteID": 1, "Low": 70, "High": 100 }
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** How does the API consumer know `testdef.TestID=1` connects to `testdefsite.TestSiteID=1`? The relationship is implicit and undocumented.
|
||||
|
||||
#### Recommended Fix
|
||||
|
||||
Add `TestID` foreign key to `testdefsite`:
|
||||
|
||||
```sql
|
||||
ALTER TABLE testdefsite ADD COLUMN TestID INT NOT NULL;
|
||||
ALTER TABLE testdefsite ADD CONSTRAINT fk_testdefsite_testdef
|
||||
FOREIGN KEY (TestID) REFERENCES testdef(TestID);
|
||||
```
|
||||
|
||||
#### Deeper Problem: Over-Engineered Architecture
|
||||
|
||||
> [!WARNING]
|
||||
> **Even with `TestID` added, the test table design remains excessively complex and confusing.**
|
||||
|
||||
Adding the missing foreign key fixes the broken link, but does not address the fundamental over-engineering. To retrieve ONE complete test for ONE site, developers must JOIN across **10 tables**:
|
||||
|
||||
```
|
||||
testdef ← "What is this test?"
|
||||
└── testdefsite ← "Is it available at site X?"
|
||||
└── testdeftech ← "What units/decimals at site X?"
|
||||
└── testdefcal ← "Is it calculated at site X?"
|
||||
└── testgrp ← "What panels is it in at site X?"
|
||||
└── testmap ← "What analyzer codes at site X?"
|
||||
└── refnum ← "Numeric reference ranges"
|
||||
└── refthold ← "Threshold reference ranges"
|
||||
└── refvset ← "Value set references"
|
||||
└── reftxt ← "Text references"
|
||||
```
|
||||
|
||||
**10 tables for one test at one site.**
|
||||
|
||||
This design assumes maximum flexibility (every site configures everything differently), but creates:
|
||||
- **Excessive query complexity** — Simple lookups require 5+ JOINs
|
||||
- **Developer confusion** — Which table holds which data?
|
||||
- **Maintenance burden** — Changes ripple across multiple tables
|
||||
- **API design friction** — Difficult to create clean, intuitive endpoints
|
||||
|
||||
#### What a Simpler Design Would Look Like
|
||||
|
||||
| Current (10 tables) | Proposed (4 tables) |
|
||||
|---------------------|---------------------|
|
||||
| `testdef` | `tests` |
|
||||
| `testdefsite` + `testdeftech` + `testdefcal` | `test_configurations` |
|
||||
| `refnum` + `refthold` + `refvset` + `reftxt` | `test_reference_ranges` (with `type` column) |
|
||||
| `testgrp` | `test_panel_members` |
|
||||
| `testmap` | (merged into `test_configurations`) |
|
||||
|
||||
#### Recommendation
|
||||
|
||||
For long-term maintainability, consider a phased refactoring:
|
||||
|
||||
1. **Phase 1:** Add `TestID` FK (immediate unblock)
|
||||
2. **Phase 2:** Create database VIEWs that flatten the structure for API consumption
|
||||
3. **Phase 3:** Evaluate consolidation of `testdefsite`/`testdeftech`/`testdefcal` into single table
|
||||
4. **Phase 4:** Consolidate 4 reference range tables into one with discriminator column
|
||||
|
||||
---
|
||||
|
||||
### 3. Data Type Mismatches Across Tables
|
||||
|
||||
**Severity:** 🔴 Critical
|
||||
|
||||
The same logical field uses different data types in different tables, making JOINs impossible.
|
||||
|
||||
| Field | Table A | Type | Table B | Type |
|
||||
|-------|---------|------|---------|------|
|
||||
| `SiteID` | `ordertest` | `VARCHAR(15)` | `site` | `INT` |
|
||||
| `OccupationID` | `contactdetail` | `VARCHAR(50)` | `occupation` | `INT` |
|
||||
| `SpcType` | `testdeftech` | `INT` | `refnum` | `VARCHAR(10)` |
|
||||
| `Country` | `patient` | `INT` | `account` | `VARCHAR(50)` |
|
||||
| `City` | `locationaddress` | `INT` | `account` | `VARCHAR(150)` |
|
||||
|
||||
---
|
||||
|
||||
## High-Priority Issues
|
||||
|
||||
### 4. Inconsistent Naming Conventions
|
||||
|
||||
| Issue | Examples |
|
||||
|-------|----------|
|
||||
| Mixed case styles | `InternalPID`, `CreateDate` vs `AreaCode`, `Parent` |
|
||||
| Cryptic abbreviations | `patatt`, `patcom`, `patidt`, `patvisitadt` |
|
||||
| Inconsistent ID naming | `InternalPID`, `PatientID`, `PatIdtID`, `PatComID` |
|
||||
| Unclear field names | `VSet`, `VValue`, `AspCnt`, `ME`, `DIDType` |
|
||||
|
||||
---
|
||||
|
||||
### 5. Inconsistent Soft-Delete Strategy
|
||||
|
||||
Multiple date fields used inconsistently:
|
||||
|
||||
| Table | Fields Used |
|
||||
|-------|-------------|
|
||||
| `patient` | `CreateDate`, `DelDate` |
|
||||
| `patvisit` | `CreateDate`, `EndDate`, `ArchivedDate`, `DelDate` |
|
||||
| `patcom` | `CreateDate`, `EndDate` |
|
||||
| `testdef` | `CreateDate`, `EndDate` |
|
||||
|
||||
**No documented standard** for determining record state (active/deleted/archived/ended).
|
||||
|
||||
---
|
||||
|
||||
### 6. Duplicate Log Table Design
|
||||
|
||||
Three nearly identical audit tables exist:
|
||||
- `patreglog`
|
||||
- `patvisitlog`
|
||||
- `specimenlog`
|
||||
|
||||
**Recommendation:** Consolidate into single `audit_log` table.
|
||||
|
||||
---
|
||||
|
||||
## Medium Priority Issues
|
||||
|
||||
### 7. Redundant Data Storage
|
||||
|
||||
| Table | Redundancy |
|
||||
|-------|------------|
|
||||
| `patres` | Stores both `InternalSID` AND `SID` |
|
||||
| `patres` | Stores both `TestSiteID` AND `TestSiteCode` |
|
||||
| `patrestatus` | Duplicates `SID` from parent table |
|
||||
|
||||
### 8. Incomplete Table Designs
|
||||
|
||||
**`patrelation` table:** Missing `RelatedPatientID`, `RelationType`
|
||||
**`users` table:** Missing `email`, `created_at`, `updated_at`, `status`, `last_login`
|
||||
|
||||
### 9. Migration Script Bugs
|
||||
|
||||
| File | Issue |
|
||||
|------|-------|
|
||||
| `Specimen.php` | Creates `specimen`, drops `specimens` |
|
||||
| `CRMOrganizations.php` | Creates `account`/`site`, drops `accounts`/`sites` |
|
||||
| `PatRes.php` | Drops non-existent `patrestech` table |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate (Sprint 1-2)
|
||||
1. **Add `TestID` to `testdefsite`** — Unblocks API development
|
||||
2. **Fix migration script bugs** — Correct table names in `down()` methods
|
||||
3. **Document existing relationships** — Create ERD with assumed relationships
|
||||
|
||||
### Short-Term (Sprint 3-6)
|
||||
4. **Add foreign key constraints** — Prioritize patient → visit → order → result chain
|
||||
5. **Fix data type mismatches** — Create migration scripts for type alignment
|
||||
6. **Standardize soft-delete** — Use `deleted_at` only, everywhere
|
||||
|
||||
### Medium-Term (Sprint 7-12)
|
||||
7. **Consolidate audit logs** — Single polymorphic audit table
|
||||
8. **Normalize addresses** — Single `addresses` table
|
||||
9. **Rename cryptic columns** — Document and rename for clarity
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Tables by Migration
|
||||
|
||||
| Migration | Tables |
|
||||
|-----------|--------|
|
||||
| PatientReg | `patient`, `patatt`, `patcom`, `patidt`, `patreglog`, `patrelation` |
|
||||
| PatVisit | `patvisit`, `patdiag`, `patvisitadt`, `patvisitlog` |
|
||||
| Location | `location`, `locationaddress` |
|
||||
| Users | `users` |
|
||||
| Contact | `contact`, `contactdetail`, `occupation`, `medicalspecialty` |
|
||||
| ValueSet | `valueset`, `valuesetdef` |
|
||||
| Counter | `counter` |
|
||||
| Specimen | `containerdef`, `specimen`, `specimenstatus`, `specimencollection`, `specimenprep`, `specimenlog` |
|
||||
| OrderTest | `ordertest`, `ordercom`, `orderatt`, `orderstatus` |
|
||||
| Test | `testdef`, `testdefsite`, `testdeftech`, `testdefcal`, `testgrp`, `testmap` |
|
||||
| RefRange | `refnum`, `refthold`, `refvset`, `reftxt` |
|
||||
| CRMOrganizations | `account`, `site` |
|
||||
| Organization | `discipline`, `department`, `workstation` |
|
||||
| Equipment | `equipmentlist`, `comparameters`, `devicelist` |
|
||||
| AreaGeo | `areageo` |
|
||||
| PatRes | `patres`, `patresflag`, `patrestatus`, `flagdef` |
|
||||
|
||||
---
|
||||
|
||||
## Process Improvement: Database Design Ownership
|
||||
|
||||
### Current Challenge
|
||||
|
||||
The issues identified in this report share a common theme: **disconnect between database structure and API consumption patterns**. Many design decisions optimize for theoretical flexibility rather than practical developer workflow.
|
||||
|
||||
This is not a critique of intent — the design shows careful thought about multi-site configurability. However, when database schemas are designed in isolation from the developers who build APIs on top of them, friction inevitably occurs.
|
||||
|
||||
### Industry Best Practice
|
||||
|
||||
Modern software development teams typically follow this ownership model:
|
||||
|
||||
| Role | Responsibility |
|
||||
|------|---------------|
|
||||
| **Product/Business** | Define what data needs to exist (requirements) |
|
||||
| **Backend Developers** | Design how data is structured (schema design) |
|
||||
| **Backend Developers** | Implement APIs that consume the schema |
|
||||
| **DBA (if applicable)** | Optimize performance, manage infrastructure |
|
||||
|
||||
The rationale is simple: **those who consume the schema daily are best positioned to design it**.
|
||||
|
||||
### Benefits of Developer-Owned Schema Design
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **API-First Thinking** | Tables designed with JSON output in mind |
|
||||
| **Faster Iterations** | Schema changes driven by real implementation needs |
|
||||
| **Reduced Friction** | No translation layer between "what was designed" and "what we need" |
|
||||
| **Better Documentation** | Developers document what they build |
|
||||
| **Ownership & Accountability** | Single team owns the full stack |
|
||||
|
||||
### Recommendation
|
||||
|
||||
Consider transitioning database schema design ownership to the backend development team for future modules. This would involve:
|
||||
|
||||
1. **Requirements Gathering** — Business/product defines data needs
|
||||
2. **Schema Proposal** — Backend team designs tables based on API requirements
|
||||
3. **Review** — Technical review with stakeholders before implementation
|
||||
4. **Implementation** — Backend team executes migrations and builds APIs
|
||||
|
||||
This approach aligns with how most modern development teams operate and would prevent the types of issues found in this review.
|
||||
|
||||
> [!NOTE]
|
||||
> This recommendation is not about past decisions, but about optimizing future development velocity. The backend team's daily work with queries, JOINs, and API responses gives them unique insight into practical schema design.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The test definition table structure is the most immediate blocker for development. Without a clear relationship between `testdef` and `testdefsite`, creating coherent API responses is not feasible. This should be prioritized in Sprint 1.
|
||||
|
||||
The broader issues (missing FKs, type mismatches) represent significant technical debt that will compound over time. Investment in database refactoring now prevents costly incidents later.
|
||||
|
||||
---
|
||||
|
||||
*Report generated from migration file analysis in `app/Database/Migrations/`*
|
||||
@ -1,432 +0,0 @@
|
||||
# Database Schema Redesign Proposal: Test, OrderTest & RefRange Modules
|
||||
|
||||
**Date:** 2025-12-16
|
||||
**Status:** Draft / Proposal
|
||||
**Author:** Development Team
|
||||
**Purpose:** Propose cleaner, more maintainable table structure
|
||||
|
||||
---
|
||||
|
||||
## The Problem: Current Design Issues
|
||||
|
||||
### 1. Test Module - Confusing Table Split
|
||||
|
||||
**Current Structure:**
|
||||
```
|
||||
testdefsite → Basic test info (code, name, description)
|
||||
testdeftech → Technical info (result type, units, specimen)
|
||||
testdefcal → Calculation formula
|
||||
testgrp → Test grouping/panels
|
||||
testmap → External system mapping
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
| Problem | Description |
|
||||
|:--------|:------------|
|
||||
| ❌ Artificial separation | `testdefsite` and `testdeftech` are 1:1 relationship - why separate them? |
|
||||
| ❌ Confusing naming | "def" prefix is redundant, "site" suffix is misleading |
|
||||
| ❌ Redundant columns | `SiteID`, `DisciplineID`, `DepartmentID` duplicated across tables |
|
||||
| ❌ Hard to query | Need multiple JOINs just to get basic test info |
|
||||
|
||||
---
|
||||
|
||||
### 2. OrderTest Module - Unnecessary Normalization
|
||||
|
||||
**Current Structure:**
|
||||
```
|
||||
ordertest → Main order
|
||||
ordercom → Comments (separate table)
|
||||
orderatt → Attachments (separate table)
|
||||
orderstatus → Status history (separate table)
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
| Problem | Description |
|
||||
|:--------|:------------|
|
||||
| ❌ Over-normalized | Comments/attachments could be JSON or simpler structure |
|
||||
| ❌ Status as separate table | If you only need current status, this adds complexity |
|
||||
| ❌ Missing link | No link between order and actual tests ordered |
|
||||
|
||||
---
|
||||
|
||||
### 3. RefRange Module - Too Many Similar Tables
|
||||
|
||||
**Current Structure:**
|
||||
```
|
||||
refnum → Numeric ranges (Low, High, Critical)
|
||||
refthold → Threshold (single cutoff value)
|
||||
reftxt → Text reference
|
||||
refvset → Value set reference
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
| Problem | Description |
|
||||
|:--------|:------------|
|
||||
| ❌ 4 tables for same concept | All are "reference ranges" with slight variations |
|
||||
| ❌ Duplicated columns | Same columns repeated: TestSiteID, SpcType, Sex, AgeStart, AgeEnd |
|
||||
| ❌ Hard to maintain | Adding a new field means updating 4 tables |
|
||||
|
||||
---
|
||||
|
||||
## Proposed Redesign
|
||||
|
||||
### Part A: Test Module - Consolidated Design
|
||||
|
||||
**BEFORE (5 tables):**
|
||||
```
|
||||
testdefsite + testdeftech + testdefcal + testgrp + testmap
|
||||
```
|
||||
|
||||
**AFTER (3 tables):**
|
||||
```
|
||||
tests → All test definition in ONE table
|
||||
test_panels → Panel/group membership
|
||||
test_mappings → External system mapping
|
||||
```
|
||||
|
||||
#### A1. `tests` (Consolidated Test Definition)
|
||||
|
||||
Merge `testdefsite`, `testdeftech`, and `testdefcal` into ONE table:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ tests │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id INT UNSIGNED PK AUTO_INCREMENT │
|
||||
│ site_id INT UNSIGNED -- Which lab site │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Basic Info (from testdefsite) -- │
|
||||
│ code VARCHAR(10) -- Test code │
|
||||
│ name VARCHAR(100) -- Test name │
|
||||
│ description VARCHAR(255) │
|
||||
│ test_type ENUM('single','panel','calculated') │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Technical Info (from testdeftech) -- │
|
||||
│ discipline_id INT UNSIGNED -- Chemistry, Hematology │
|
||||
│ department_id INT UNSIGNED │
|
||||
│ result_type ENUM('numeric','text','coded') │
|
||||
│ specimen_type VARCHAR(20) │
|
||||
│ specimen_qty DECIMAL(10,2) │
|
||||
│ specimen_unit VARCHAR(20) │
|
||||
│ unit VARCHAR(20) -- Result unit │
|
||||
│ decimal_places TINYINT │
|
||||
│ method VARCHAR(100) │
|
||||
│ expected_tat INT -- Turnaround time (mins) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Calculated Test Info (from testdefcal) -- │
|
||||
│ formula TEXT -- NULL if not calculated │
|
||||
│ formula_inputs JSON -- List of input test IDs │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Display Order -- │
|
||||
│ sort_order_screen INT │
|
||||
│ sort_order_report INT │
|
||||
│ visible_screen BOOLEAN DEFAULT 1 │
|
||||
│ visible_report BOOLEAN DEFAULT 1 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Audit -- │
|
||||
│ created_at DATETIME │
|
||||
│ updated_at DATETIME │
|
||||
│ deleted_at DATETIME -- Soft delete │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ One query to get all test info
|
||||
- ✅ No redundant columns
|
||||
- ✅ Clear naming
|
||||
- ✅ `test_type` tells you if it's a panel or calculated test
|
||||
|
||||
---
|
||||
|
||||
#### A2. `test_panels` (Panel Membership)
|
||||
|
||||
For tests that are panels (groups of other tests):
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ test_panels │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id INT UNSIGNED PK AUTO_INCREMENT │
|
||||
│ panel_test_id INT UNSIGNED FK → tests.id -- The panel │
|
||||
│ member_test_id INT UNSIGNED FK → tests.id -- Member test │
|
||||
│ sort_order INT │
|
||||
│ created_at DATETIME │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Example:** CBC panel contains: WBC, RBC, HGB, HCT, PLT
|
||||
```
|
||||
panel_test_id=1 (CBC), member_test_id=2 (WBC)
|
||||
panel_test_id=1 (CBC), member_test_id=3 (RBC)
|
||||
panel_test_id=1 (CBC), member_test_id=4 (HGB)
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### A3. `test_mappings` (External System Mapping)
|
||||
|
||||
Keep this separate (good design):
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ test_mappings │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id INT UNSIGNED PK AUTO_INCREMENT │
|
||||
│ test_id INT UNSIGNED FK → tests.id │
|
||||
│ external_system VARCHAR(50) -- 'LIS', 'HIS', 'Analyzer'│
|
||||
│ external_code VARCHAR(50) -- Code in that system │
|
||||
│ external_name VARCHAR(100) │
|
||||
│ connection_id INT UNSIGNED -- Which connection/device │
|
||||
│ direction ENUM('inbound','outbound','both') │
|
||||
│ created_at DATETIME │
|
||||
│ updated_at DATETIME │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Part B: Reference Range - Unified Design
|
||||
|
||||
**BEFORE (4 tables):**
|
||||
```
|
||||
refnum + refthold + refvset + reftxt
|
||||
```
|
||||
|
||||
**AFTER (1 table):**
|
||||
```
|
||||
reference_ranges → All reference types in ONE table
|
||||
```
|
||||
|
||||
#### B1. `reference_ranges` (Unified)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ reference_ranges │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id INT UNSIGNED PK AUTO_INCREMENT │
|
||||
│ test_id INT UNSIGNED FK → tests.id │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Criteria (same across all old tables) -- │
|
||||
│ specimen_type VARCHAR(20) │
|
||||
│ sex ENUM('M','F','A') -- A = All/Any │
|
||||
│ age_min INT -- In days for precision │
|
||||
│ age_max INT -- In days │
|
||||
│ age_unit ENUM('days','months','years') │
|
||||
│ criteria VARCHAR(100) -- Additional criteria │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Reference Type -- │
|
||||
│ ref_type ENUM('numeric','threshold','text','coded') │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Numeric Range (when ref_type = 'numeric') -- │
|
||||
│ critical_low DECIMAL(15,4) │
|
||||
│ normal_low DECIMAL(15,4) │
|
||||
│ normal_high DECIMAL(15,4) │
|
||||
│ critical_high DECIMAL(15,4) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Threshold (when ref_type = 'threshold') -- │
|
||||
│ threshold_value DECIMAL(15,4) │
|
||||
│ threshold_operator ENUM('<','<=','>','>=','=') │
|
||||
│ below_text VARCHAR(50) -- "Negative", "Normal" │
|
||||
│ above_text VARCHAR(50) -- "Positive", "Abnormal" │
|
||||
│ gray_zone_low DECIMAL(15,4) │
|
||||
│ gray_zone_high DECIMAL(15,4) │
|
||||
│ gray_zone_text VARCHAR(50) -- "Equivocal" │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Text/Coded (when ref_type = 'text' or 'coded') -- │
|
||||
│ reference_text TEXT -- Expected values or desc │
|
||||
│ value_set JSON -- For coded: list of valid│
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Audit -- │
|
||||
│ created_at DATETIME │
|
||||
│ updated_at DATETIME │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ One table instead of 4
|
||||
- ✅ Easy to add new reference types
|
||||
- ✅ Single query with `ref_type` filter
|
||||
- ✅ No duplicated criteria columns
|
||||
|
||||
---
|
||||
|
||||
### Part C: OrderTest - Cleaner Design
|
||||
|
||||
**BEFORE (4 tables):**
|
||||
```
|
||||
ordertest + ordercom + orderatt + orderstatus
|
||||
```
|
||||
|
||||
**AFTER (3 tables):**
|
||||
```
|
||||
orders → Main order with current status
|
||||
order_tests → Individual tests in the order (MISSING before!)
|
||||
order_history → Status changes + comments combined
|
||||
```
|
||||
|
||||
#### C1. `orders` (Main Order)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ orders │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id INT UNSIGNED PK AUTO_INCREMENT │
|
||||
│ order_number VARCHAR(30) UNIQUE -- Display order ID │
|
||||
│ accession_number VARCHAR(30) -- Lab accession │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Patient & Visit -- │
|
||||
│ patient_id INT UNSIGNED FK → patients.id │
|
||||
│ visit_id INT UNSIGNED FK → visits.id │
|
||||
│ site_id INT UNSIGNED │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Order Details -- │
|
||||
│ priority ENUM('routine','urgent','stat') │
|
||||
│ status ENUM('pending','collected','received', │
|
||||
│ 'in_progress','completed','cancelled') │
|
||||
│ ordered_by INT UNSIGNED -- Doctor/User ID │
|
||||
│ ordered_at DATETIME │
|
||||
│ collected_at DATETIME │
|
||||
│ received_at DATETIME │
|
||||
│ completed_at DATETIME │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ -- Audit -- │
|
||||
│ created_at DATETIME │
|
||||
│ updated_at DATETIME │
|
||||
│ deleted_at DATETIME │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### C2. `order_tests` (Tests in Order) — **NEW TABLE!**
|
||||
|
||||
**This was MISSING in original design!** How do you know what tests are in an order?
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ order_tests │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id INT UNSIGNED PK AUTO_INCREMENT │
|
||||
│ order_id INT UNSIGNED FK → orders.id │
|
||||
│ test_id INT UNSIGNED FK → tests.id │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ status ENUM('ordered','in_progress','resulted', │
|
||||
│ 'verified','cancelled') │
|
||||
│ result_value VARCHAR(255) -- The actual result │
|
||||
│ result_flag ENUM('N','L','H','LL','HH','A') -- Normal/Abn│
|
||||
│ result_comment TEXT │
|
||||
│ resulted_by INT UNSIGNED -- Tech who entered result │
|
||||
│ resulted_at DATETIME │
|
||||
│ verified_by INT UNSIGNED -- Supervisor who verified │
|
||||
│ verified_at DATETIME │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ created_at DATETIME │
|
||||
│ updated_at DATETIME │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### C3. `order_history` (Combined Audit Trail)
|
||||
|
||||
Combine `ordercom`, `orderatt`, `orderstatus` into one audit table:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ order_history │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ id INT UNSIGNED PK AUTO_INCREMENT │
|
||||
│ order_id INT UNSIGNED FK → orders.id │
|
||||
│ order_test_id INT UNSIGNED FK → order_tests.id (nullable) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ event_type ENUM('status_change','comment','attachment', │
|
||||
│ 'result_edit','verification') │
|
||||
│ old_value TEXT │
|
||||
│ new_value TEXT │
|
||||
│ comment TEXT │
|
||||
│ attachment_path VARCHAR(255) -- For attachments │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ created_by INT UNSIGNED │
|
||||
│ created_at DATETIME │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary: Before vs After
|
||||
|
||||
| Module | Before | After | Change |
|
||||
|:-------|:-------|:------|:-------|
|
||||
| **Test** | 5 tables | 3 tables | -2 tables |
|
||||
| **RefRange** | 4 tables | 1 table | -3 tables |
|
||||
| **OrderTest** | 4 tables | 3 tables | -1 table, +1 essential table |
|
||||
| **Total** | 13 tables | 7 tables | **-6 tables** |
|
||||
|
||||
### New ERD
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ PROPOSED ERD │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ tests │◄────────│ test_panels │ │ test_mappings │ │
|
||||
│ │ (All tests) │ │ (Panel→Test) │ │ (Ext. systems) │ │
|
||||
│ └──────┬──────┘ └──────────────┘ └─────────────────┘ │
|
||||
│ │ │
|
||||
│ │ 1:N │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ reference_ranges │ (All ref types in one table) │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
│ │
|
||||
│ ┌──────────┐ 1:N ┌─────────────┐ 1:N ┌───────────────┐ │
|
||||
│ │ patients │◄──────────│ orders │◄──────────│ order_history │ │
|
||||
│ └──────────┘ └──────┬──────┘ └───────────────┘ │
|
||||
│ │ │
|
||||
│ │ 1:N │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ order_tests │ (What tests are in order) │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ │ N:1 │
|
||||
│ ▼ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ tests │ │
|
||||
│ └─────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
Since this is a major restructure:
|
||||
|
||||
1. **Create new migration files** (don't modify old ones)
|
||||
2. **Write data migration script** to move data from old to new tables
|
||||
3. **Update Models, Controllers, Views** to use new table names
|
||||
4. **Test thoroughly** before dropping old tables
|
||||
|
||||
---
|
||||
|
||||
## Questions for Discussion
|
||||
|
||||
1. Is storing `formula` as TEXT acceptable, or need a more structured approach?
|
||||
2. Should `order_history` store ALL changes, or just important ones?
|
||||
3. Any additional fields needed that I missed?
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Review and approve this proposal
|
||||
2. 🔲 Create new migration files
|
||||
3. 🔲 Write data migration scripts
|
||||
4. 🔲 Update Models to use new tables
|
||||
5. 🔲 Update Controllers and Services
|
||||
6. 🔲 Deprecate old tables
|
||||
@ -1,370 +0,0 @@
|
||||
# JWT Authentication Implementation
|
||||
|
||||
## Date: 2025-12-30
|
||||
|
||||
## Overview
|
||||
|
||||
Implemented complete JWT (JSON Web Token) authentication system for CLQMS using HTTP-only cookies for secure token storage.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Browser │ ◄─────► │ Server │ ◄─────► │ Database │
|
||||
└─────────┘ └──────────┘ └──────────┘
|
||||
│ │ │
|
||||
│ 1. POST /login │ │
|
||||
├───────────────────►│ │
|
||||
│ │ 2. Verify user │
|
||||
│ ├────────────────────►│
|
||||
│ │◄────────────────────┤
|
||||
│ │ 3. Generate JWT │
|
||||
│ 4. Set cookie │ │
|
||||
│◄───────────────────┤ │
|
||||
│ │ │
|
||||
│ 5. Access page │ │
|
||||
├───────────────────►│ │
|
||||
│ │ 6. Verify JWT │
|
||||
│ 7. Return page │ │
|
||||
│◄───────────────────┤ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Auth Controller (`app/Controllers/Auth.php`)
|
||||
|
||||
**Endpoints:**
|
||||
|
||||
| Method | Route | Description |
|
||||
|--------|-------|-------------|
|
||||
| POST | `/api/auth/login` | Login with username/password |
|
||||
| POST | `/api/auth/register` | Register new user |
|
||||
| GET | `/api/auth/check` | Check authentication status |
|
||||
| POST | `/api/auth/logout` | Logout and clear token |
|
||||
|
||||
**Key Features:**
|
||||
- JWT token generation using `firebase/php-jwt`
|
||||
- Password hashing with `password_hash()`
|
||||
- HTTP-only cookie storage for security
|
||||
- 10-day token expiration
|
||||
- Secure cookie handling (HTTPS/HTTP aware)
|
||||
|
||||
---
|
||||
|
||||
### 2. Auth Filter (`app/Filters/AuthFilter.php`)
|
||||
|
||||
**Purpose:** Protect routes from unauthorized access
|
||||
|
||||
**Behavior:**
|
||||
- Checks for JWT token in cookies
|
||||
- Validates token signature and expiration
|
||||
- Differentiates between API and page requests
|
||||
- **API requests**: Returns 401 JSON response
|
||||
- **Page requests**: Redirects to `/login`
|
||||
|
||||
**Protected Routes:**
|
||||
- `/` (Dashboard)
|
||||
- `/patients`
|
||||
- `/requests`
|
||||
- `/settings`
|
||||
|
||||
---
|
||||
|
||||
### 3. Login Page (`app/Views/auth/login.php`)
|
||||
|
||||
**Features:**
|
||||
- ✅ Beautiful animated gradient background
|
||||
- ✅ Username/password form
|
||||
- ✅ Password visibility toggle
|
||||
- ✅ Remember me checkbox
|
||||
- ✅ Registration modal
|
||||
- ✅ Error/success message display
|
||||
- ✅ Loading states
|
||||
- ✅ Responsive design
|
||||
- ✅ Alpine.js for reactivity
|
||||
|
||||
**Design:**
|
||||
- Animated gradient background
|
||||
- Glass morphism card design
|
||||
- FontAwesome icons
|
||||
- DaisyUI 5 components
|
||||
|
||||
---
|
||||
|
||||
### 4. Routes Configuration (`app/Config/Routes.php`)
|
||||
|
||||
```php
|
||||
// Public Routes (no auth)
|
||||
$routes->get('/login', 'PagesController::login');
|
||||
|
||||
// Auth API Routes
|
||||
$routes->post('api/auth/login', 'Auth::login');
|
||||
$routes->post('api/auth/register', 'Auth::register');
|
||||
$routes->get('api/auth/check', 'Auth::checkAuth');
|
||||
$routes->post('api/auth/logout', 'Auth::logout');
|
||||
|
||||
// Protected Page Routes (requires auth filter)
|
||||
$routes->group('', ['filter' => 'auth'], function ($routes) {
|
||||
$routes->get('/', 'PagesController::dashboard');
|
||||
$routes->get('/patients', 'PagesController::patients');
|
||||
$routes->get('/requests', 'PagesController::requests');
|
||||
$routes->get('/settings', 'PagesController::settings');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Features
|
||||
|
||||
### 1. **HTTP-Only Cookies**
|
||||
- Token stored in HTTP-only cookie
|
||||
- Not accessible via JavaScript
|
||||
- Prevents XSS attacks
|
||||
|
||||
### 2. **Secure Cookie Flags**
|
||||
```php
|
||||
[
|
||||
'name' => 'token',
|
||||
'httponly' => true, // XSS protection
|
||||
'secure' => $isSecure, // HTTPS only (production)
|
||||
'samesite' => Cookie::SAMESITE_LAX, // CSRF protection
|
||||
'expire' => 864000 // 10 days
|
||||
]
|
||||
```
|
||||
|
||||
### 3. **Password Hashing**
|
||||
- Uses `password_hash()` with `PASSWORD_DEFAULT`
|
||||
- Bcrypt algorithm
|
||||
- Automatic salt generation
|
||||
|
||||
### 4. **JWT Signature**
|
||||
- HMAC-SHA256 algorithm
|
||||
- Secret key from `.env` file
|
||||
- Prevents token tampering
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Users Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role_id INT DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
Add to `.env` file:
|
||||
|
||||
```env
|
||||
JWT_SECRET=your-super-secret-key-here-change-in-production
|
||||
```
|
||||
|
||||
**⚠️ Important:**
|
||||
- Use a strong, random secret key in production
|
||||
- Never commit `.env` to version control
|
||||
- Minimum 32 characters recommended
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Frontend Login (Alpine.js)
|
||||
|
||||
```javascript
|
||||
async login() {
|
||||
const res = await fetch(`${BASEURL}api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
username: this.form.username,
|
||||
password: this.form.password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.status === 'success') {
|
||||
window.location.href = `${BASEURL}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend Logout (Alpine.js)
|
||||
|
||||
```javascript
|
||||
async logout() {
|
||||
const res = await fetch(`${BASEURL}api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.href = `${BASEURL}login`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Check Auth Status
|
||||
|
||||
```javascript
|
||||
const res = await fetch(`${BASEURL}api/auth/check`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
console.log('User:', data.data.username);
|
||||
console.log('Role:', data.data.roleid);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Response Format
|
||||
|
||||
### Success Response
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"code": 200,
|
||||
"message": "Login successful"
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response
|
||||
```json
|
||||
{
|
||||
"status": "failed",
|
||||
"code": 401,
|
||||
"message": "Invalid password"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
- [x] Login with valid credentials
|
||||
- [x] Login with invalid credentials
|
||||
- [x] Register new user
|
||||
- [x] Register duplicate username
|
||||
- [x] Access protected page without login (should redirect)
|
||||
- [x] Access protected page with valid token
|
||||
- [x] Logout functionality
|
||||
- [x] Token expiration (after 10 days)
|
||||
- [x] Theme persistence after login
|
||||
- [x] Responsive design on mobile
|
||||
|
||||
### Test Users
|
||||
|
||||
Create test users via registration or SQL:
|
||||
|
||||
```sql
|
||||
INSERT INTO users (username, password, role_id)
|
||||
VALUES ('admin', '$2y$10$...hashed_password...', 1);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Modified/Created
|
||||
|
||||
### Created
|
||||
1. `app/Views/auth/login.php` - Login page with registration modal
|
||||
2. `docs/JWT_AUTH_IMPLEMENTATION.md` - This documentation
|
||||
|
||||
### Modified
|
||||
1. `app/Filters/AuthFilter.php` - Updated redirect path to `/login`
|
||||
2. `app/Config/Routes.php` - Added auth filter to protected routes
|
||||
3. `app/Views/layout/main_layout.php` - Added logout functionality
|
||||
|
||||
### Existing (No changes needed)
|
||||
1. `app/Controllers/Auth.php` - Already implemented
|
||||
2. `app/Config/Filters.php` - Already configured
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "Token not found" on protected pages
|
||||
|
||||
**Solution:** Check if login is setting the cookie correctly
|
||||
```php
|
||||
// In browser console
|
||||
document.cookie
|
||||
```
|
||||
|
||||
### Issue: "Invalid token signature"
|
||||
|
||||
**Solution:** Verify `JWT_SECRET` in `.env` matches between login and verification
|
||||
|
||||
### Issue: Redirect loop
|
||||
|
||||
**Solution:** Ensure `/login` route is NOT protected by auth filter
|
||||
|
||||
### Issue: CORS errors
|
||||
|
||||
**Solution:** Check CORS filter configuration in `app/Filters/Cors.php`
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Password Reset** - Email-based password recovery
|
||||
2. **Two-Factor Authentication** - TOTP/SMS verification
|
||||
3. **Session Management** - View and revoke active sessions
|
||||
4. **Role-Based Access Control** - Different permissions per role
|
||||
5. **OAuth Integration** - Google/Microsoft login
|
||||
6. **Refresh Tokens** - Automatic token renewal
|
||||
7. **Account Lockout** - After failed login attempts
|
||||
8. **Audit Logging** - Track login/logout events
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
✅ **Implemented:**
|
||||
- HTTP-only cookies
|
||||
- Password hashing
|
||||
- JWT signature verification
|
||||
- HTTPS support
|
||||
- SameSite cookie protection
|
||||
|
||||
⚠️ **Recommended for Production:**
|
||||
- Rate limiting on login endpoint
|
||||
- CAPTCHA after failed attempts
|
||||
- IP-based blocking
|
||||
- Security headers (CSP, HSTS)
|
||||
- Regular security audits
|
||||
- Token rotation
|
||||
- Shorter token expiration (1-2 hours with refresh token)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Firebase PHP-JWT Library](https://github.com/firebase/php-jwt)
|
||||
- [CodeIgniter 4 Authentication](https://codeigniter.com/user_guide/libraries/authentication.html)
|
||||
- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
||||
- [JWT Best Practices](https://tools.ietf.org/html/rfc8725)
|
||||
|
||||
---
|
||||
|
||||
**Implementation completed by:** AI Assistant
|
||||
**Date:** 2025-12-30
|
||||
**Status:** ✅ Production Ready
|
||||
@ -1,55 +0,0 @@
|
||||
# CLQMS Technical Documentation Index
|
||||
|
||||
This repository serves as the central knowledge base for the CLQMS Backend. It contains architectural proposals, API contracts, and design reviews that guide the development of the clinical laboratory management system.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architectural Redesign (Manager's Proposal)
|
||||
|
||||
The system is currently transitioning to a consolidated database schema to enhance performance and maintainability. This is a critical initiative aimed at reducing schema complexity.
|
||||
|
||||
- **[Detailed Redesign Proposal](./20251216002-Test_OrderTest_RefRange_schema_redesign_proposal.md)**
|
||||
- *Focus:* Consolidating 13 legacy tables into 7 optimized tables.
|
||||
- *Modules Impacted:* Test Definition, Reference Ranges, and Order Management.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Functional Modules
|
||||
|
||||
### 1. Test Management
|
||||
Handles the definition of laboratory tests, including technical specifications, calculation formulas, and external system mappings.
|
||||
- See: `tests` table and `test_panels` in the redesign proposal.
|
||||
|
||||
### 2. Patient & Order Workflow
|
||||
Manages the lifecycle of a laboratory order:
|
||||
- **[Patient Registration API Contract](./api_contract_patient_registration.md)**: Specifications for patient intake and data validation.
|
||||
- **Order Tracking**: From collection and receipt to technical verification.
|
||||
|
||||
### 3. Reference Range Engine
|
||||
A unified logic for determining normal and critical flags across various test types.
|
||||
- *Types supported:* Numeric, Threshold (Cut-off), Textual, and Coded (Value Sets).
|
||||
|
||||
---
|
||||
|
||||
## 📊 Design Reviews & Legacy Reference
|
||||
|
||||
Documentation regarding the initial assessments and legacy structures:
|
||||
- **[Database Design Review - Sonnet](./20251212001-database_design_review_sonnet.md)**: Comprehensive analysis of legacy table relationships and bottlenecks.
|
||||
- **[Database Design Review - Opus](./20251212002-database_design_review_opus.md)**: Additional perspectives on the initial system architecture.
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Development Standards
|
||||
|
||||
All contributions to the CLQMS backend must adhere to the following:
|
||||
1. **Data Integrity:** All database migrations must include data validation scripts.
|
||||
2. **Auditability:** Status changes in the `orders` module must be logged in `order_history`.
|
||||
3. **Security:** Every endpoint requires JWT authentication.
|
||||
|
||||
---
|
||||
|
||||
### 📜 Administration
|
||||
Documentation maintained by the **5Panda Team**.
|
||||
|
||||
---
|
||||
*Built with ❤️ by the 5Panda Team.*
|
||||
@ -1,121 +0,0 @@
|
||||
# CLQMS UI Fixes - Implementation Summary
|
||||
|
||||
## Date: 2025-12-30
|
||||
|
||||
### Issues Fixed
|
||||
|
||||
#### 1. ✅ Dark/Light Theme Toggle Not Working
|
||||
**Problem**: Using incompatible CDN versions (DaisyUI 5 beta with Tailwind CSS 3)
|
||||
|
||||
**Solution**:
|
||||
- Updated to DaisyUI 5 stable: `https://cdn.jsdelivr.net/npm/daisyui@5`
|
||||
- Updated to Tailwind CSS 4: `https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4`
|
||||
- These versions are compatible and properly support theme switching
|
||||
|
||||
**Result**: Theme toggle now works correctly between `corporate` (light) and `business` (dark) themes
|
||||
|
||||
---
|
||||
|
||||
#### 2. ✅ Sidebar Hover/Active States Not Aesthetic
|
||||
**Problem**: Custom hover classes weren't rendering well, poor visual distinction
|
||||
|
||||
**Solution**:
|
||||
- Removed custom `:class` bindings with inline color classes
|
||||
- Used DaisyUI's native `menu` component with `active` class
|
||||
- Added CSS enhancement for active states using DaisyUI color variables:
|
||||
```css
|
||||
.menu li > *:not(.menu-title):not(.btn).active {
|
||||
background-color: oklch(var(--p));
|
||||
color: oklch(var(--pc));
|
||||
}
|
||||
```
|
||||
- Increased spacing between menu items from `space-y-1` to `space-y-2`
|
||||
- Adjusted padding for better collapsed state appearance
|
||||
|
||||
**Result**: Clean, professional sidebar with proper active highlighting and smooth hover effects
|
||||
|
||||
---
|
||||
|
||||
#### 3. ✅ Welcome Message Not Visible
|
||||
**Problem**: Gradient background with `text-primary-content` had poor contrast
|
||||
|
||||
**Solution**:
|
||||
- Changed from gradient (`bg-gradient-to-r from-primary to-secondary`) to solid primary color
|
||||
- Restructured layout with icon badge and better typography hierarchy
|
||||
- Added larger padding (`py-8`)
|
||||
- Created icon container with semi-transparent background for visual interest
|
||||
- Improved text sizing and spacing
|
||||
|
||||
**Result**: Welcome message is now clearly visible in both light and dark themes
|
||||
|
||||
---
|
||||
|
||||
### Additional Improvements
|
||||
|
||||
#### 4. ✅ Smooth Theme Transitions
|
||||
Added global CSS transition for smooth theme switching:
|
||||
```css
|
||||
* {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
```
|
||||
|
||||
**Result**: Smooth, professional theme transitions instead of jarring instant changes
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. **`app/Views/layout/main_layout.php`**
|
||||
- Updated CDN links
|
||||
- Improved sidebar menu styling
|
||||
- Added smooth transitions
|
||||
- Enhanced active state styling
|
||||
|
||||
2. **`app/Views/dashboard/dashboard_index.php`**
|
||||
- Redesigned welcome banner
|
||||
- Better contrast and visibility
|
||||
- Improved layout structure
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [x] Light theme loads correctly
|
||||
- [x] Dark theme loads correctly
|
||||
- [x] Theme toggle switches between themes
|
||||
- [x] Theme preference persists in localStorage
|
||||
- [x] Sidebar active states show correctly
|
||||
- [x] Sidebar hover states work properly
|
||||
- [x] Welcome message is visible in both themes
|
||||
- [x] Smooth transitions between themes
|
||||
- [x] Responsive design works on mobile
|
||||
- [x] Burger menu toggles sidebar
|
||||
|
||||
---
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
✅ Chrome/Edge (Chromium)
|
||||
✅ Firefox
|
||||
✅ Safari
|
||||
✅ Mobile browsers
|
||||
|
||||
---
|
||||
|
||||
## Next Steps (Optional Enhancements)
|
||||
|
||||
1. Add more menu items (Specimens, Tests, Reports)
|
||||
2. Implement user profile functionality
|
||||
3. Add notifications/alerts system
|
||||
4. Create settings page for theme customization
|
||||
5. Add loading states for async operations
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Using DaisyUI 5 stable ensures long-term compatibility
|
||||
- Tailwind CSS 4 provides better performance and features
|
||||
- All changes follow DaisyUI 5 best practices from `llms.txt`
|
||||
- Color system uses OKLCH for better color consistency across themes
|
||||
@ -1,160 +0,0 @@
|
||||
# V2 Routes Migration Summary
|
||||
|
||||
## Date: 2025-12-30
|
||||
|
||||
## Overview
|
||||
|
||||
All new UI views have been moved to the `/v2/` prefix to avoid conflicts with existing frontend engineer's work.
|
||||
|
||||
---
|
||||
|
||||
## Route Changes
|
||||
|
||||
### Before (Conflicting with existing work)
|
||||
```
|
||||
/ → Dashboard
|
||||
/login → Login page
|
||||
/patients → Patients list
|
||||
/requests → Lab requests
|
||||
/settings → Settings
|
||||
```
|
||||
|
||||
### After (V2 namespace - No conflicts)
|
||||
```
|
||||
/v2/ → Dashboard
|
||||
/v2/login → Login page
|
||||
/v2/patients → Patients list
|
||||
/v2/requests → Lab requests
|
||||
/v2/settings → Settings
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
### 1. Routes Configuration
|
||||
**File:** `app/Config/Routes.php`
|
||||
|
||||
```php
|
||||
// Public Routes
|
||||
$routes->get('/v2/login', 'PagesController::login');
|
||||
|
||||
// Protected Page Routes - V2
|
||||
$routes->group('v2', ['filter' => 'auth'], function ($routes) {
|
||||
$routes->get('/', 'PagesController::dashboard');
|
||||
$routes->get('dashboard', 'PagesController::dashboard');
|
||||
$routes->get('patients', 'PagesController::patients');
|
||||
$routes->get('requests', 'PagesController::requests');
|
||||
$routes->get('settings', 'PagesController::settings');
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Auth Filter
|
||||
**File:** `app/Filters/AuthFilter.php`
|
||||
|
||||
- Redirects to `/v2/login` when unauthorized
|
||||
|
||||
### 3. Login Page
|
||||
**File:** `app/Views/auth/login.php`
|
||||
|
||||
- Redirects to `/v2/` after successful login
|
||||
|
||||
### 4. Main Layout
|
||||
**File:** `app/Views/layout/main_layout.php`
|
||||
|
||||
- All navigation links updated to `/v2/*`
|
||||
- Logout redirects to `/v2/login`
|
||||
|
||||
### 5. Dashboard
|
||||
**File:** `app/Views/dashboard/dashboard_index.php`
|
||||
|
||||
- Quick action links updated to `/v2/*`
|
||||
|
||||
---
|
||||
|
||||
## URL Mapping
|
||||
|
||||
| Feature | URL | Auth Required |
|
||||
|---------|-----|---------------|
|
||||
| **Login** | `/v2/login` | ❌ No |
|
||||
| **Dashboard** | `/v2/` or `/v2/dashboard` | ✅ Yes |
|
||||
| **Patients** | `/v2/patients` | ✅ Yes |
|
||||
| **Lab Requests** | `/v2/requests` | ✅ Yes |
|
||||
| **Settings** | `/v2/settings` | ✅ Yes |
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints (Unchanged)
|
||||
|
||||
API routes remain the same - no `/v2/` prefix needed:
|
||||
|
||||
```
|
||||
POST /api/auth/login
|
||||
POST /api/auth/register
|
||||
GET /api/auth/check
|
||||
POST /api/auth/logout
|
||||
GET /api/patient
|
||||
POST /api/patient
|
||||
...etc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing URLs
|
||||
|
||||
### Development Server
|
||||
```
|
||||
Login: http://localhost:8080/v2/login
|
||||
Dashboard: http://localhost:8080/v2/
|
||||
Patients: http://localhost:8080/v2/patients
|
||||
Requests: http://localhost:8080/v2/requests
|
||||
Settings: http://localhost:8080/v2/settings
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Engineer's Work
|
||||
|
||||
✅ **Protected:** All existing routes remain untouched
|
||||
- Root `/` is available for frontend engineer
|
||||
- `/patients`, `/requests`, etc. are available
|
||||
- No conflicts with new V2 UI
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
- [x] Updated routes to use `/v2/` prefix
|
||||
- [x] Updated AuthFilter redirects
|
||||
- [x] Updated login page redirect
|
||||
- [x] Updated main layout navigation links
|
||||
- [x] Updated dashboard quick action links
|
||||
- [x] Updated logout redirect
|
||||
- [x] API routes remain unchanged
|
||||
- [x] Documentation created
|
||||
|
||||
---
|
||||
|
||||
## Notes for Team
|
||||
|
||||
1. **New UI is at `/v2/`** - Share this URL with testers
|
||||
2. **Old routes are free** - Frontend engineer can use root paths
|
||||
3. **API unchanged** - Both UIs can use the same API endpoints
|
||||
4. **Auth works for both** - JWT authentication applies to both V1 and V2
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
When ready to migrate fully to V2:
|
||||
|
||||
1. Remove `/v2/` prefix from routes
|
||||
2. Archive or remove old frontend files
|
||||
3. Update documentation
|
||||
4. Update any bookmarks/links
|
||||
|
||||
Or keep both versions running side-by-side if needed.
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Complete - No conflicts with existing work
|
||||
@ -1,222 +0,0 @@
|
||||
# Patient Registration API Contract
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Base URL:** `/api/v1`
|
||||
|
||||
---
|
||||
|
||||
## 1. Patients (`/patients`)
|
||||
|
||||
### GET /patients
|
||||
Retrieve list of patients.
|
||||
|
||||
**Query Parameters:**
|
||||
- `page` (int, default: 1)
|
||||
- `limit` (int, default: 20)
|
||||
- `search` (string) - Search by name, MRN (PatientID), or phone.
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"InternalPID": 1001,
|
||||
"PatientID": "MRN-2025-0001",
|
||||
"NameFirst": "John",
|
||||
"NameLast": "Doe",
|
||||
"Gender": 1,
|
||||
"Birthdate": "1990-05-15",
|
||||
"MobilePhone": "+1234567890",
|
||||
"EmailAddress1": "john.doe@example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### POST /patients
|
||||
Register a new patient.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"PatientID": "MRN-2025-0002",
|
||||
"AlternatePID": "ALT-123", // Optional
|
||||
"Prefix": "Mr.", // Optional
|
||||
"NameFirst": "Jane",
|
||||
"NameMiddle": "Marie", // Optional
|
||||
"NameLast": "Doe",
|
||||
"NameMaiden": null, // Optional
|
||||
"Suffix": null, // Optional
|
||||
"Gender": 2, // 1=Male, 2=Female (example)
|
||||
"Birthdate": "1992-08-20",
|
||||
"PlaceOfBirth": "New York", // Optional
|
||||
"Street_1": "123 Main St",
|
||||
"Street_2": "Apt 4B", // Optional
|
||||
"City": "Metropolis",
|
||||
"Province": "NY",
|
||||
"ZIP": "10001",
|
||||
"Phone": "555-0100", // Optional
|
||||
"MobilePhone": "555-0199",
|
||||
"EmailAddress1": "jane.doe@example.com",
|
||||
"MaritalStatus": 1, // 1=Single, 2=Married, etc.
|
||||
"Religion": 1, // Optional ID
|
||||
"Race": 1, // Optional ID
|
||||
"Citizenship": "USA" // Optional
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"message": "Patient created successfully",
|
||||
"data": {
|
||||
"InternalPID": 1002,
|
||||
"PatientID": "MRN-2025-0002",
|
||||
"CreateDate": "2025-12-16T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GET /patients/{id}
|
||||
Get full details of a specific patient.
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"InternalPID": 1001,
|
||||
"PatientID": "MRN-2025-0001",
|
||||
"NameFirst": "John",
|
||||
"NameLast": "Doe",
|
||||
"identifiers": [
|
||||
{
|
||||
"PatIdtID": 5,
|
||||
"IdentifierType": "Passport",
|
||||
"Identifier": "A12345678"
|
||||
}
|
||||
],
|
||||
"relations": [],
|
||||
"comments": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PUT /patients/{id}
|
||||
Update patient demographics.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"NameLast": "Smith",
|
||||
"MobilePhone": "555-9999",
|
||||
"EmailAddress1": "john.smith@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"message": "Patient updated successfully",
|
||||
"data": { "InternalPID": 1001 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Patient Identifiers (`/patients/{id}/identifiers`)
|
||||
|
||||
### POST /patients/{id}/identifiers
|
||||
Add an identifier (SSN, Passport, Driver's License) to a patient.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"IdentifierType": "SSN",
|
||||
"Identifier": "000-11-2222",
|
||||
"EffectiveDate": "2020-01-01", // Optional
|
||||
"ExpirationDate": "2030-01-01" // Optional
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"message": "Identifier added",
|
||||
"data": { "PatIdtID": 15 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Patient Comments (`/patients/{id}/comments`)
|
||||
|
||||
### POST /patients/{id}/comments
|
||||
Add a comment to a patient record.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"Comment": "Patient requests wheelchair assistance upon arrival."
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"message": "Comment added",
|
||||
"data": { "PatComID": 42 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Patient Relations (`/patients/{id}/relations`)
|
||||
*Note: Pending Schema Update (Currently `patrelation` is missing columns)*
|
||||
|
||||
### POST /patients/{id}/relations
|
||||
Add a family member or emergency contact.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"RelatedPID": 1050, // If relation is also a patient
|
||||
"RelationType": "Spouse", // Requires schema update to store this
|
||||
"IsEmergency": true // Requires schema update
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"message": "Relation added",
|
||||
"data": { "PatRelID": 8 }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Patient Attachments (`/patients/{id}/attachments`)
|
||||
|
||||
### POST /patients/{id}/attachments
|
||||
Upload a file for a patient (insurance card, ID scan).
|
||||
|
||||
**Request (Multipart/Form-Data):**
|
||||
- `file`: (Binary File)
|
||||
- `Address`: (string, optional description or file path reference)
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"message": "File uploaded",
|
||||
"data": {
|
||||
"PatAttID": 99,
|
||||
"Address": "/uploads/patients/1001/scan_id.pdf"
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -1,157 +0,0 @@
|
||||
---
|
||||
title: "Project Pandaria: Next-Gen LIS Architecture"
|
||||
description: "An offline-first, event-driven architecture concept for the CLQMS."
|
||||
date: 2025-12-19
|
||||
order: 6
|
||||
tags:
|
||||
- posts
|
||||
- clqms
|
||||
layout: clqms-post.njk
|
||||
---
|
||||
|
||||
## 1. 💀 Pain vs. 🛡️ Solution
|
||||
|
||||
### 🚩 Problem 1: "The Server is Dead!"
|
||||
> **The Pain:** When the internet cuts or the server crashes, the entire lab stops. Patients wait, doctors get angry.
|
||||
|
||||
**🛡️ The Solution: "Offline-First Mode"**
|
||||
The workstation keeps working 100% offline. It has a local brain (database). Patients never know the internet is down.
|
||||
|
||||
---
|
||||
|
||||
### 🚩 Problem 2: "Data Vanished?"
|
||||
> **The Pain:** We pushed data, the network blinked, and the sample disappeared. We have to re-scan manually.
|
||||
|
||||
**🛡️ The Solution: "The Outbox Guarantee"**
|
||||
Data is treated like Registered Mail. It stays in a safe SQL "Outbox" until the workstation signs a receipt (ACK) confirming it is saved.
|
||||
|
||||
---
|
||||
|
||||
### 🚩 Problem 3: "Spaghetti Code"
|
||||
> **The Pain:** Adding a new machine (like Mindray) means hacking the core LIS code with endless `if-else` statements.
|
||||
|
||||
**🛡️ The Solution: "Universal Adapters"**
|
||||
Every machine gets a simple plugin (Driver). The Core System stays clean, modular, and untouched.
|
||||
|
||||
---
|
||||
|
||||
### 🚩 Problem 4: "Inconsistent Results"
|
||||
> **The Pain:** One machine says `WBC`, another says `Leukocytes`. The Database is a mess of different codes.
|
||||
|
||||
**🛡️ The Solution: "The Translator"**
|
||||
A built-in dictionary auto-translates everything to Standard English (e.g., `WBC`) before it ever touches the database.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🏗️ System Architecture: The "Edge" Concept
|
||||
|
||||
We are moving from a **Dependent** model (dumb terminal) to an **Empowered** model (Edge Computing).
|
||||
|
||||
### The "Core" (Central Server)
|
||||
* **Role:** The "Hippocampus" (Long-term Memory).
|
||||
* **Stack:** CodeIgniter 4 + MySQL.
|
||||
* **Responsibilities:**
|
||||
* Billing & Financials (Single Source of Truth).
|
||||
* Permanent Patient History.
|
||||
* API Gateway for external apps (Mobile, Website).
|
||||
* Administrator Dashboard.
|
||||
|
||||
### The "Edge" (Smart Workstation)
|
||||
* **Role:** The "Cortex" (Immediate Processing).
|
||||
* **Stack:** Node.js (Electron) + SQLite.
|
||||
* **Responsibilities:**
|
||||
* **Hardware I/O:** Speaking directly to RS232/TCP ports.
|
||||
* **Hot Caching:** Keeping the last 7 days of active orders locally.
|
||||
* **Logic Engine:** Validating results against reference ranges *before* syncing.
|
||||
|
||||
> **Key Difference:** The Workstation no longer asks "Can I work?" It assumes it can work. It treats the server as a "Sync Partner," not a "Master." If the internet dies, the Edge keeps processing samples, printing labels, and validating results without a hiccup.
|
||||
|
||||
---
|
||||
|
||||
## 3. 🔌 The "Universal Adapter" (Hardware Layer)
|
||||
|
||||
We use the **Adapter Design Pattern** to isolate hardware chaos from our clean business logic.
|
||||
|
||||
### The Problem: "The Tower of Babel"
|
||||
Every manufacturer speaks a proprietary dialect.
|
||||
* **Sysmex:** Uses ASTM protocols with checksums.
|
||||
* **Roche:** Uses custom HL7 variants.
|
||||
* **Mindray:** Often uses raw hex streams.
|
||||
|
||||
### The Fix: "Drivers as Plugins"
|
||||
The Workstation loads a specific `.js` file (The Driver) for each connected machine. This driver has one job: **Normalization.**
|
||||
|
||||
#### Example: ASTM to JSON
|
||||
**Raw Input (Alien Language):**
|
||||
`P|1||12345||Smith^John||19800101|M|||||`
|
||||
`R|1|^^^WBC|10.5|10^3/uL|4.0-11.0|N||F||`
|
||||
|
||||
**Normalized Output (clean JSON):**
|
||||
```json
|
||||
{
|
||||
"test_code": "WBC",
|
||||
"value": 10.5,
|
||||
"unit": "10^3/uL",
|
||||
"flag": "Normal",
|
||||
"timestamp": "2025-12-19T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Benefit: "Hot-Swappable Labs"
|
||||
Buying a new machine? You don't need to obscurely patch the `LISSender.exe`. You just drop in `driver-sysmex-xn1000.js` into the `plugins/` folder, and the Edge Workstation instantly learns how to speak Sysmex.
|
||||
|
||||
---
|
||||
|
||||
## 4. 🗣️ The "Translator" (Data Mapping)
|
||||
|
||||
Machines are stubborn. They send whatever test codes they want (`WBC`, `Leukocytes`, `W.B.C`, `White_Cells`). If we save these directly, our database becomes a swamp.
|
||||
|
||||
### The Solution: "Local Dictionary & Rules Engine"
|
||||
Before data is saved to SQLite, it passes through the **Translator**.
|
||||
|
||||
1. **Alias Matching:**
|
||||
* The dictionary knows that `W.B.C` coming from *Machine A* actually means `WBC_TOTAL`.
|
||||
* It renames the key instantly.
|
||||
|
||||
2. **Unit Conversion (Math Layer):**
|
||||
* *Machine A* sends Hemoglobin in `g/dL` (e.g., 14.5).
|
||||
* *Our Standard* is `g/L` (e.g., 145).
|
||||
* **The Rule:** `Apply: Value * 10`.
|
||||
* The translator automatically mathematical normalized the result.
|
||||
|
||||
This ensures that our Analytics Dashboard sees **clean, comparable data** regardless of whether it came from a 10-year-old machine or a brand new one.
|
||||
|
||||
---
|
||||
|
||||
## 5. 📨 The "Registered Mail" Sync (Redis + Outbox)
|
||||
|
||||
We are banning the word "Polling" (checking every 5 seconds). It's inefficient and scary. We are switching to **Events** using **Redis**.
|
||||
|
||||
### 🤔 What is Redis?
|
||||
Think of **MySQL** as a filing cabinet (safe, permanent, but slow to open).
|
||||
Think of **Redis** as a **loudspeaker system** (instant, in-memory, very fast).
|
||||
|
||||
We use Redis specifically for its **Pub/Sub (Publish/Subscribe)** feature. It lets us "broadcast" a message to all connected workstations instantly without writing to a disk.
|
||||
|
||||
### 🔄 How the Flow Works:
|
||||
|
||||
1. **👨⚕️ Order Created:** The Doctor saves an order on the Server.
|
||||
2. **📮 The Outbox:** The server puts a copy of the order in a special SQL table called `outbox_queue`.
|
||||
3. **🔔 The Bell (Redis):** The server "shouts" into the Redis loudspeaker: *"New mail for Lab 1!"*.
|
||||
4. **📥 Delivery:** The Workstation (listening to Redis) hears the shout instantly. It then goes to the SQL Outbox to download the actual heavy data.
|
||||
5. **✍️ The Signature (ACK):** The Workstation sends a digital signature back: *"I have received and saved Order #123."*
|
||||
6. **✅ Done:** Only *then* does the server delete the message from the Outbox.
|
||||
|
||||
**Safety Net & Self-Healing:**
|
||||
* **Redis is just the doorbell:** If the workstation is offline and misses the shout, it doesn't matter.
|
||||
* **SQL is the mailbox:** The message sits safely in the `outbox_queue` table indefinitely.
|
||||
* **Recovery:** When the Workstation turns back on, it automatically asks: *"Did I miss anything?"* and downloads all pending items from the SQL Outbox. **Zero data loss, even if the notification is lost.**
|
||||
|
||||
---
|
||||
|
||||
## 6. 🏆 Summary: Why We Win
|
||||
|
||||
* **Reliability:** 🛡️ 100% Uptime for the Lab.
|
||||
* **Speed:** ⚡ Instant response times (Local Database is faster than Cloud).
|
||||
* **Sanity:** 🧘 No more panic attacks when the internet provider fails.
|
||||
* **Future Proof:** 🚀 Ready for any new machine connection in the future.
|
||||
@ -1,432 +0,0 @@
|
||||
---
|
||||
title: "Edge Workstation: SQLite Database Schema"
|
||||
description: "Database design for the offline-first smart workstation."
|
||||
date: 2025-12-19
|
||||
order: 7
|
||||
tags:
|
||||
- posts
|
||||
- clqms
|
||||
- database
|
||||
layout: clqms-post.njk
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the **SQLite database schema** for the Edge Workstation — the local "brain" that enables **100% offline operation** for lab technicians.
|
||||
|
||||
> **Stack:** Node.js (Electron) + SQLite
|
||||
> **Role:** The "Cortex" — Immediate Processing
|
||||
|
||||
---
|
||||
|
||||
## 📊 Entity Relationship Diagram
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────┐
|
||||
│ orders │────────<│ order_tests │
|
||||
└─────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌──────────────┐
|
||||
│ machines │────────<│ results │
|
||||
└─────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ test_dictionary │ (The Translator)
|
||||
└─────────────────┘
|
||||
|
||||
┌───────────────┐ ┌───────────────┐
|
||||
│ outbox_queue │ │ inbox_queue │
|
||||
└───────────────┘ └───────────────┘
|
||||
(Push to Server) (Pull from Server)
|
||||
|
||||
┌───────────────┐ ┌───────────────┐
|
||||
│ sync_log │ │ config │
|
||||
└───────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Table Definitions
|
||||
|
||||
### 1. `orders` — Cached Patient Orders
|
||||
|
||||
Orders downloaded from the Core Server. Keeps the **last 7 days** for offline processing.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER | Primary key (local) |
|
||||
| `server_order_id` | TEXT | Original ID from Core Server |
|
||||
| `patient_id` | TEXT | Patient identifier |
|
||||
| `patient_name` | TEXT | Patient full name |
|
||||
| `patient_dob` | DATE | Date of birth |
|
||||
| `patient_gender` | TEXT | M, F, or O |
|
||||
| `order_date` | DATETIME | When order was created |
|
||||
| `priority` | TEXT | `stat`, `routine`, `urgent` |
|
||||
| `status` | TEXT | `pending`, `in_progress`, `completed`, `cancelled` |
|
||||
| `barcode` | TEXT | Sample barcode |
|
||||
| `synced_at` | DATETIME | Last sync timestamp |
|
||||
|
||||
```sql
|
||||
CREATE TABLE orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
server_order_id TEXT UNIQUE NOT NULL,
|
||||
patient_id TEXT NOT NULL,
|
||||
patient_name TEXT NOT NULL,
|
||||
patient_dob DATE,
|
||||
patient_gender TEXT CHECK(patient_gender IN ('M', 'F', 'O')),
|
||||
order_date DATETIME NOT NULL,
|
||||
priority TEXT DEFAULT 'routine' CHECK(priority IN ('stat', 'routine', 'urgent')),
|
||||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'cancelled')),
|
||||
barcode TEXT,
|
||||
notes TEXT,
|
||||
synced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `order_tests` — Requested Tests per Order
|
||||
|
||||
Each order can have multiple tests (CBC, Urinalysis, etc.)
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER | Primary key |
|
||||
| `order_id` | INTEGER | FK to orders |
|
||||
| `test_code` | TEXT | Standardized code (e.g., `WBC_TOTAL`) |
|
||||
| `test_name` | TEXT | Display name |
|
||||
| `status` | TEXT | `pending`, `processing`, `completed`, `failed` |
|
||||
|
||||
```sql
|
||||
CREATE TABLE order_tests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL,
|
||||
test_code TEXT NOT NULL,
|
||||
test_name TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `results` — Machine Output (Normalized)
|
||||
|
||||
Results from lab machines, **already translated** to standard format by The Translator.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER | Primary key |
|
||||
| `order_test_id` | INTEGER | FK to order_tests |
|
||||
| `machine_id` | INTEGER | FK to machines |
|
||||
| `test_code` | TEXT | Standardized test code |
|
||||
| `value` | REAL | Numeric result |
|
||||
| `unit` | TEXT | Standardized unit |
|
||||
| `flag` | TEXT | `L`, `N`, `H`, `LL`, `HH`, `A` |
|
||||
| `raw_value` | TEXT | Original value from machine |
|
||||
| `raw_unit` | TEXT | Original unit from machine |
|
||||
| `raw_test_code` | TEXT | Original code before translation |
|
||||
| `validated` | BOOLEAN | Has been reviewed by tech? |
|
||||
|
||||
```sql
|
||||
CREATE TABLE results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_test_id INTEGER,
|
||||
machine_id INTEGER,
|
||||
test_code TEXT NOT NULL,
|
||||
value REAL NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
reference_low REAL,
|
||||
reference_high REAL,
|
||||
flag TEXT CHECK(flag IN ('L', 'N', 'H', 'LL', 'HH', 'A')),
|
||||
raw_value TEXT,
|
||||
raw_unit TEXT,
|
||||
raw_test_code TEXT,
|
||||
validated BOOLEAN DEFAULT 0,
|
||||
validated_by TEXT,
|
||||
validated_at DATETIME,
|
||||
machine_timestamp DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_test_id) REFERENCES order_tests(id),
|
||||
FOREIGN KEY (machine_id) REFERENCES machines(id)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. `outbox_queue` — The Registered Mail 📮
|
||||
|
||||
Data waits here until the Core Server sends an **ACK (acknowledgment)**. This is the heart of our **zero data loss** guarantee.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER | Primary key |
|
||||
| `event_type` | TEXT | `result_created`, `result_validated`, etc. |
|
||||
| `payload` | TEXT | JSON data to sync |
|
||||
| `target_entity` | TEXT | `results`, `orders`, etc. |
|
||||
| `priority` | INTEGER | 1 = highest, 10 = lowest |
|
||||
| `retry_count` | INTEGER | Number of failed attempts |
|
||||
| `status` | TEXT | `pending`, `processing`, `sent`, `acked`, `failed` |
|
||||
| `acked_at` | DATETIME | When server confirmed receipt |
|
||||
|
||||
```sql
|
||||
CREATE TABLE outbox_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_type TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
target_entity TEXT,
|
||||
target_id INTEGER,
|
||||
priority INTEGER DEFAULT 5,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
max_retries INTEGER DEFAULT 5,
|
||||
last_error TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
sent_at DATETIME,
|
||||
acked_at DATETIME
|
||||
);
|
||||
```
|
||||
|
||||
> **Flow:** Data enters as `pending` → moves to `sent` when transmitted → becomes `acked` when server confirms → deleted after cleanup.
|
||||
|
||||
---
|
||||
|
||||
### 5. `inbox_queue` — Messages from Server 📥
|
||||
|
||||
Incoming orders/updates from Core Server waiting to be processed locally.
|
||||
|
||||
```sql
|
||||
CREATE TABLE inbox_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
server_message_id TEXT UNIQUE NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
error_message TEXT,
|
||||
received_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at DATETIME
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. `machines` — Connected Lab Equipment 🔌
|
||||
|
||||
Registry of all connected analyzers.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | INTEGER | Primary key |
|
||||
| `name` | TEXT | "Sysmex XN-1000" |
|
||||
| `driver_file` | TEXT | "driver-sysmex-xn1000.js" |
|
||||
| `connection_type` | TEXT | `RS232`, `TCP`, `USB`, `FILE` |
|
||||
| `connection_config` | TEXT | JSON config |
|
||||
|
||||
```sql
|
||||
CREATE TABLE machines (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
manufacturer TEXT,
|
||||
model TEXT,
|
||||
serial_number TEXT,
|
||||
driver_file TEXT NOT NULL,
|
||||
connection_type TEXT CHECK(connection_type IN ('RS232', 'TCP', 'USB', 'FILE')),
|
||||
connection_config TEXT,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
last_communication DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**Example config:**
|
||||
```json
|
||||
{
|
||||
"port": "COM3",
|
||||
"baudRate": 9600,
|
||||
"dataBits": 8,
|
||||
"parity": "none"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. `test_dictionary` — The Translator 📖
|
||||
|
||||
This table solves the **"WBC vs Leukocytes"** problem. It maps machine-specific codes to our standard codes.
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `machine_id` | INTEGER | FK to machines (NULL = universal) |
|
||||
| `raw_code` | TEXT | What machine sends: `W.B.C`, `Leukocytes` |
|
||||
| `standard_code` | TEXT | Our standard: `WBC_TOTAL` |
|
||||
| `unit_conversion_factor` | REAL | Math conversion (e.g., 10 for g/dL → g/L) |
|
||||
|
||||
```sql
|
||||
CREATE TABLE test_dictionary (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
machine_id INTEGER,
|
||||
raw_code TEXT NOT NULL,
|
||||
standard_code TEXT NOT NULL,
|
||||
standard_name TEXT NOT NULL,
|
||||
unit_conversion_factor REAL DEFAULT 1.0,
|
||||
raw_unit TEXT,
|
||||
standard_unit TEXT,
|
||||
reference_low REAL,
|
||||
reference_high REAL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (machine_id) REFERENCES machines(id),
|
||||
UNIQUE(machine_id, raw_code)
|
||||
);
|
||||
```
|
||||
|
||||
**Translation Example:**
|
||||
|
||||
| Machine | Raw Code | Standard Code | Conversion |
|
||||
|---------|----------|---------------|------------|
|
||||
| Sysmex | `WBC` | `WBC_TOTAL` | × 1.0 |
|
||||
| Mindray | `Leukocytes` | `WBC_TOTAL` | × 1.0 |
|
||||
| Sysmex | `HGB` (g/dL) | `HGB` (g/L) | × 10 |
|
||||
| Universal | `W.B.C` | `WBC_TOTAL` | × 1.0 |
|
||||
|
||||
---
|
||||
|
||||
### 8. `sync_log` — Audit Trail 📜
|
||||
|
||||
Track all sync activities for debugging and recovery.
|
||||
|
||||
```sql
|
||||
CREATE TABLE sync_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
direction TEXT CHECK(direction IN ('push', 'pull')),
|
||||
event_type TEXT NOT NULL,
|
||||
entity_type TEXT,
|
||||
entity_id INTEGER,
|
||||
server_response_code INTEGER,
|
||||
success BOOLEAN,
|
||||
duration_ms INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. `config` — Local Settings ⚙️
|
||||
|
||||
Key-value store for workstation-specific settings.
|
||||
|
||||
```sql
|
||||
CREATE TABLE config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
description TEXT,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
**Default values:**
|
||||
|
||||
| Key | Value | Description |
|
||||
|-----|-------|-------------|
|
||||
| `workstation_id` | `LAB-WS-001` | Unique identifier |
|
||||
| `server_url` | `https://api.clqms.com` | Core Server endpoint |
|
||||
| `cache_days` | `7` | Days to keep cached orders |
|
||||
| `auto_validate` | `false` | Auto-validate normal results |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 How the Sync Works
|
||||
|
||||
### Outbox Pattern (Push)
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Lab Result │
|
||||
│ Generated │
|
||||
└────────┬────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Save to SQLite │
|
||||
│ + Outbox │
|
||||
└────────┬────────┘
|
||||
▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ Send to Server │────>│ Core Server │
|
||||
└────────┬────────┘ └────────┬────────┘
|
||||
│ │
|
||||
│ ◄──── ACK ─────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Mark as 'acked' │
|
||||
│ in Outbox │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### Self-Healing Recovery
|
||||
|
||||
If the workstation was offline and missed Redis notifications:
|
||||
|
||||
```javascript
|
||||
// On startup, ask: "Did I miss anything?"
|
||||
async function recoverMissedMessages() {
|
||||
const lastSync = await db.get("SELECT value FROM config WHERE key = 'last_sync'");
|
||||
const missed = await api.get(`/outbox/pending?since=${lastSync}`);
|
||||
|
||||
for (const message of missed) {
|
||||
await inbox.insert(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Sample Data
|
||||
|
||||
### Sample Machine Registration
|
||||
|
||||
```sql
|
||||
INSERT INTO machines (name, manufacturer, driver_file, connection_type, connection_config)
|
||||
VALUES ('Sysmex XN-1000', 'Sysmex', 'driver-sysmex-xn1000.js', 'RS232',
|
||||
'{"port": "COM3", "baudRate": 9600}');
|
||||
```
|
||||
|
||||
### Sample Dictionary Entry
|
||||
|
||||
```sql
|
||||
-- Mindray calls WBC "Leukocytes" — we translate it!
|
||||
INSERT INTO test_dictionary (machine_id, raw_code, standard_code, standard_name, raw_unit, standard_unit)
|
||||
VALUES (2, 'Leukocytes', 'WBC_TOTAL', 'White Blood Cell Count', 'x10^9/L', '10^3/uL');
|
||||
```
|
||||
|
||||
### Sample Result with Translation
|
||||
|
||||
```sql
|
||||
-- Machine sent: { code: "Leukocytes", value: 8.5, unit: "x10^9/L" }
|
||||
-- After translation:
|
||||
INSERT INTO results (test_code, value, unit, flag, raw_test_code, raw_value, raw_unit)
|
||||
VALUES ('WBC_TOTAL', 8.5, '10^3/uL', 'N', 'Leukocytes', '8.5', 'x10^9/L');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏆 Key Benefits
|
||||
|
||||
| Feature | Benefit |
|
||||
|---------|---------|
|
||||
| **Offline-First** | Lab never stops, even without internet |
|
||||
| **Outbox Queue** | Zero data loss guarantee |
|
||||
| **Test Dictionary** | Clean, standardized data from any machine |
|
||||
| **Inbox Queue** | Never miss orders, even if offline |
|
||||
| **Sync Log** | Full audit trail for debugging |
|
||||
|
||||
---
|
||||
|
||||
## 📁 Full SQL Migration
|
||||
|
||||
The complete SQL migration file is available at:
|
||||
📄 [`docs/examples/edge_workstation.sql`](/docs/examples/edge_workstation.sql)
|
||||
@ -1,32 +0,0 @@
|
||||
# 001. Database Design Constraints
|
||||
|
||||
Date: 2025-12-18
|
||||
Status: Accepted
|
||||
|
||||
## Context
|
||||
The database schema and relationship design for the CLQMS system were established by management and external stakeholders. The backend engineering team was brought in after the core data structure was finalized.
|
||||
|
||||
The development team has identified potential challenges regarding:
|
||||
- Normalization levels in specific tables.
|
||||
- Naming conventions differ from standard framework defaults.
|
||||
- Specific relationship structures that may impact query performance or data integrity.
|
||||
|
||||
## Decision
|
||||
The backend team will implement the application logic based on the provided database schema. Significant structural changes to the database (refactoring tables, altering core relationships) are out of scope for the current development phase unless explicitly approved by management.
|
||||
|
||||
The team will:
|
||||
1. Map application entities to the existing table structures.
|
||||
2. Handle necessary data integrity and consistency checks within the Application Layer (Models/Services) where the database constraints are insufficient.
|
||||
3. Document any workarounds required to bridge the gap between the schema and the application framework (CodeIgniter 4).
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Development can proceed immediately without spending time on database redesign discussions.
|
||||
- Alignment with the manager's initial vision and requirements.
|
||||
|
||||
### Negative
|
||||
- **Technical Debt**: Potential accumulation of "glue code" to make modern framework features work with the non-standard schema.
|
||||
- **Maintainability**: Future developers may find the data model unintuitive if it deviates significantly from standard practices.
|
||||
- **Performance**: Sub-optimal schema designs may require complex queries or application-side processing that impacts performance at scale.
|
||||
- **Responsibility**: The backend team explicitly notes that issues arising directly from the inherent database structure (e.g., anomalies, scaling bottlenecks related to schema) are consequences of this design constraint.
|
||||
@ -1,254 +0,0 @@
|
||||
-- ============================================================
|
||||
-- 🖥️ CLQMS Edge Workstation - SQLite Database Schema
|
||||
-- Project Pandaria: Offline-First LIS Architecture
|
||||
-- ============================================================
|
||||
-- This is the LOCAL database for each Smart Workstation.
|
||||
-- Stack: Node.js (Electron) + SQLite
|
||||
-- Role: "The Cortex" - Immediate Processing
|
||||
-- ============================================================
|
||||
|
||||
-- 🔧 Enable foreign keys (SQLite needs this explicitly)
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 📋 CACHED ORDERS (Hot Cache - Last 7 Days)
|
||||
-- ============================================================
|
||||
-- Orders downloaded from the Core Server for local processing.
|
||||
-- Workstation can work 100% offline with this data.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
server_order_id TEXT UNIQUE NOT NULL, -- Original ID from Core Server
|
||||
patient_id TEXT NOT NULL,
|
||||
patient_name TEXT NOT NULL,
|
||||
patient_dob DATE,
|
||||
patient_gender TEXT CHECK(patient_gender IN ('M', 'F', 'O')),
|
||||
order_date DATETIME NOT NULL,
|
||||
priority TEXT DEFAULT 'routine' CHECK(priority IN ('stat', 'routine', 'urgent')),
|
||||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'cancelled')),
|
||||
barcode TEXT,
|
||||
notes TEXT,
|
||||
synced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_orders_barcode ON orders(barcode);
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
CREATE INDEX idx_orders_patient ON orders(patient_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 🔬 ORDER TESTS (What tests are requested?)
|
||||
-- ============================================================
|
||||
-- Each order can have multiple tests (CBC, Urinalysis, etc.)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS order_tests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL,
|
||||
test_code TEXT NOT NULL, -- Standardized code (e.g., 'WBC_TOTAL')
|
||||
test_name TEXT NOT NULL, -- Display name (e.g., 'White Blood Cell Count')
|
||||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'processing', 'completed', 'failed')),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_order_tests_order ON order_tests(order_id);
|
||||
CREATE INDEX idx_order_tests_code ON order_tests(test_code);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 📊 RESULTS (Machine Output - Normalized)
|
||||
-- ============================================================
|
||||
-- Results from lab machines, already translated to standard format.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_test_id INTEGER,
|
||||
machine_id INTEGER,
|
||||
test_code TEXT NOT NULL, -- Standardized test code
|
||||
value REAL NOT NULL, -- Numeric result
|
||||
unit TEXT NOT NULL, -- Standardized unit
|
||||
reference_low REAL,
|
||||
reference_high REAL,
|
||||
flag TEXT CHECK(flag IN ('L', 'N', 'H', 'LL', 'HH', 'A')), -- Low, Normal, High, Critical Low/High, Abnormal
|
||||
raw_value TEXT, -- Original value from machine
|
||||
raw_unit TEXT, -- Original unit from machine
|
||||
raw_test_code TEXT, -- Original code from machine (before translation)
|
||||
validated BOOLEAN DEFAULT 0,
|
||||
validated_by TEXT,
|
||||
validated_at DATETIME,
|
||||
machine_timestamp DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (order_test_id) REFERENCES order_tests(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (machine_id) REFERENCES machines(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_results_order_test ON results(order_test_id);
|
||||
CREATE INDEX idx_results_test_code ON results(test_code);
|
||||
CREATE INDEX idx_results_validated ON results(validated);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. 📮 OUTBOX QUEUE (Registered Mail Pattern)
|
||||
-- ============================================================
|
||||
-- Data waits here until the Core Server confirms receipt (ACK).
|
||||
-- Zero data loss, even if network blinks!
|
||||
|
||||
CREATE TABLE IF NOT EXISTS outbox_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_type TEXT NOT NULL, -- 'result_created', 'result_validated', 'order_updated'
|
||||
payload TEXT NOT NULL, -- JSON data to sync
|
||||
target_entity TEXT, -- 'results', 'orders', etc.
|
||||
target_id INTEGER, -- ID of the record
|
||||
priority INTEGER DEFAULT 5, -- 1 = highest, 10 = lowest
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
max_retries INTEGER DEFAULT 5,
|
||||
last_error TEXT,
|
||||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'processing', 'sent', 'acked', 'failed')),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
sent_at DATETIME,
|
||||
acked_at DATETIME
|
||||
);
|
||||
|
||||
CREATE INDEX idx_outbox_status ON outbox_queue(status);
|
||||
CREATE INDEX idx_outbox_priority ON outbox_queue(priority, created_at);
|
||||
|
||||
-- ============================================================
|
||||
-- 5. 📥 INBOX QUEUE (Messages from Server)
|
||||
-- ============================================================
|
||||
-- Incoming messages/orders from Core Server waiting to be processed.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inbox_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
server_message_id TEXT UNIQUE NOT NULL, -- ID from server for deduplication
|
||||
event_type TEXT NOT NULL, -- 'new_order', 'order_cancelled', 'config_update'
|
||||
payload TEXT NOT NULL, -- JSON data
|
||||
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'processing', 'completed', 'failed')),
|
||||
error_message TEXT,
|
||||
received_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at DATETIME
|
||||
);
|
||||
|
||||
CREATE INDEX idx_inbox_status ON inbox_queue(status);
|
||||
|
||||
-- ============================================================
|
||||
-- 6. 🔌 MACHINES (Connected Lab Equipment)
|
||||
-- ============================================================
|
||||
-- Registry of connected machines/analyzers.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS machines (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL, -- 'Sysmex XN-1000', 'Mindray BC-6800'
|
||||
manufacturer TEXT,
|
||||
model TEXT,
|
||||
serial_number TEXT,
|
||||
driver_file TEXT NOT NULL, -- 'driver-sysmex-xn1000.js'
|
||||
connection_type TEXT CHECK(connection_type IN ('RS232', 'TCP', 'USB', 'FILE')),
|
||||
connection_config TEXT, -- JSON: {"port": "COM3", "baudRate": 9600}
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
last_communication DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 7. 📖 TEST DICTIONARY (The Translator)
|
||||
-- ============================================================
|
||||
-- Maps machine-specific codes to standard codes.
|
||||
-- Solves the "WBC vs Leukocytes" problem!
|
||||
|
||||
CREATE TABLE IF NOT EXISTS test_dictionary (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
machine_id INTEGER, -- NULL = universal mapping
|
||||
raw_code TEXT NOT NULL, -- What the machine sends: 'W.B.C', 'Leukocytes'
|
||||
standard_code TEXT NOT NULL, -- Our standard: 'WBC_TOTAL'
|
||||
standard_name TEXT NOT NULL, -- 'White Blood Cell Count'
|
||||
unit_conversion_factor REAL DEFAULT 1.0, -- Multiply raw value by this (e.g., 10 for g/dL to g/L)
|
||||
raw_unit TEXT, -- Unit machine sends
|
||||
standard_unit TEXT, -- Our standard unit
|
||||
reference_low REAL,
|
||||
reference_high REAL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (machine_id) REFERENCES machines(id) ON DELETE CASCADE,
|
||||
UNIQUE(machine_id, raw_code)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_dictionary_lookup ON test_dictionary(machine_id, raw_code);
|
||||
|
||||
-- ============================================================
|
||||
-- 8. 📜 SYNC LOG (Audit Trail)
|
||||
-- ============================================================
|
||||
-- Track all sync activities for debugging & recovery.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sync_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
direction TEXT CHECK(direction IN ('push', 'pull')),
|
||||
event_type TEXT NOT NULL,
|
||||
entity_type TEXT,
|
||||
entity_id INTEGER,
|
||||
server_response_code INTEGER,
|
||||
server_message TEXT,
|
||||
success BOOLEAN,
|
||||
duration_ms INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sync_log_created ON sync_log(created_at DESC);
|
||||
|
||||
-- ============================================================
|
||||
-- 9. ⚙️ LOCAL CONFIG (Workstation Settings)
|
||||
-- ============================================================
|
||||
-- Key-value store for workstation-specific settings.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
description TEXT,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 📦 SAMPLE DATA: Machines & Dictionary
|
||||
-- ============================================================
|
||||
|
||||
-- Sample Machines
|
||||
INSERT INTO machines (name, manufacturer, model, driver_file, connection_type, connection_config) VALUES
|
||||
('Sysmex XN-1000', 'Sysmex', 'XN-1000', 'driver-sysmex-xn1000.js', 'RS232', '{"port": "COM3", "baudRate": 9600}'),
|
||||
('Mindray BC-6800', 'Mindray', 'BC-6800', 'driver-mindray-bc6800.js', 'TCP', '{"host": "192.168.1.50", "port": 5000}');
|
||||
|
||||
-- Sample Test Dictionary (The Translator)
|
||||
INSERT INTO test_dictionary (machine_id, raw_code, standard_code, standard_name, raw_unit, standard_unit, unit_conversion_factor, reference_low, reference_high) VALUES
|
||||
-- Sysmex mappings (machine_id = 1)
|
||||
(1, 'WBC', 'WBC_TOTAL', 'White Blood Cell Count', '10^3/uL', '10^3/uL', 1.0, 4.0, 11.0),
|
||||
(1, 'RBC', 'RBC_TOTAL', 'Red Blood Cell Count', '10^6/uL', '10^6/uL', 1.0, 4.5, 5.5),
|
||||
(1, 'HGB', 'HGB', 'Hemoglobin', 'g/dL', 'g/L', 10.0, 120, 170),
|
||||
(1, 'PLT', 'PLT_TOTAL', 'Platelet Count', '10^3/uL', '10^3/uL', 1.0, 150, 400),
|
||||
-- Mindray mappings (machine_id = 2) - Different naming!
|
||||
(2, 'Leukocytes', 'WBC_TOTAL', 'White Blood Cell Count', 'x10^9/L', '10^3/uL', 1.0, 4.0, 11.0),
|
||||
(2, 'Erythrocytes', 'RBC_TOTAL', 'Red Blood Cell Count', 'x10^12/L', '10^6/uL', 1.0, 4.5, 5.5),
|
||||
(2, 'Hb', 'HGB', 'Hemoglobin', 'g/L', 'g/L', 1.0, 120, 170),
|
||||
(2, 'Thrombocytes', 'PLT_TOTAL', 'Platelet Count', 'x10^9/L', '10^3/uL', 1.0, 150, 400),
|
||||
-- Universal mappings (machine_id = NULL)
|
||||
(NULL, 'W.B.C', 'WBC_TOTAL', 'White Blood Cell Count', NULL, '10^3/uL', 1.0, 4.0, 11.0),
|
||||
(NULL, 'White_Cells', 'WBC_TOTAL', 'White Blood Cell Count', NULL, '10^3/uL', 1.0, 4.0, 11.0);
|
||||
|
||||
-- Sample Config
|
||||
INSERT INTO config (key, value, description) VALUES
|
||||
('workstation_id', 'LAB-WS-001', 'Unique identifier for this workstation'),
|
||||
('workstation_name', 'Hematology Station 1', 'Human-readable name'),
|
||||
('server_url', 'https://clqms-core.example.com/api', 'Core Server API endpoint'),
|
||||
('cache_days', '7', 'Number of days to keep cached orders'),
|
||||
('auto_validate', 'false', 'Auto-validate results within normal range'),
|
||||
('last_sync', NULL, 'Timestamp of last successful sync');
|
||||
|
||||
-- Sample Order (for testing)
|
||||
INSERT INTO orders (server_order_id, patient_id, patient_name, patient_dob, patient_gender, order_date, priority, barcode) VALUES
|
||||
('ORD-2025-001234', 'PAT-00001', 'John Smith', '1980-01-15', 'M', '2025-12-19 08:00:00', 'routine', 'LAB2025001234');
|
||||
|
||||
INSERT INTO order_tests (order_id, test_code, test_name) VALUES
|
||||
(1, 'WBC_TOTAL', 'White Blood Cell Count'),
|
||||
(1, 'RBC_TOTAL', 'Red Blood Cell Count'),
|
||||
(1, 'HGB', 'Hemoglobin'),
|
||||
(1, 'PLT_TOTAL', 'Platelet Count');
|
||||
|
||||
-- ============================================================
|
||||
-- ✅ DONE! Your Edge Workstation database is ready.
|
||||
-- ============================================================
|
||||
Binary file not shown.
689
plans/ref_range_multiple_support_plan.md
Normal file
689
plans/ref_range_multiple_support_plan.md
Normal file
@ -0,0 +1,689 @@
|
||||
# Plan: Multiple Reference Ranges with Advanced Dialog
|
||||
|
||||
## Overview
|
||||
Refactor the "Reff" tab to support multiple reference ranges using the existing `refnum` table schema.
|
||||
|
||||
## Existing Database Schema (refnum table)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| RefNumID | INT AUTO_INCREMENT | Primary key |
|
||||
| SiteID | INT | Site identifier |
|
||||
| TestSiteID | INT | Links to test |
|
||||
| SpcType | INT | Specimen type |
|
||||
| Sex | INT | Gender (from valueset) |
|
||||
| Criteria | VARCHAR(100) | Additional criteria |
|
||||
| AgeStart | INT | Age range start |
|
||||
| AgeEnd | INT | Age range end |
|
||||
| **NumRefType** | INT | **Input format: 1=NMRC, 2=TH, 3=TEXT, 4=LIST** |
|
||||
| **RangeType** | INT | **Result category: 1=REF, 2=CRTC, 3=VAL, 4=RERUN** |
|
||||
| LowSign | INT | Low operator: 1='<', 2='<=', 3='>=', 4='>', 5='<>' |
|
||||
| Low | INT | Low value |
|
||||
| HighSign | INT | High operator |
|
||||
| High | INT | High value |
|
||||
| Display | INT | Display order |
|
||||
| **Flag** | VARCHAR(10) | **Like Label (e.g., "Negative", "Borderline")** |
|
||||
| Interpretation | VARCHAR(255) | Interpretation text |
|
||||
| Notes | VARCHAR(255) | Notes |
|
||||
| CreateDate | Datetime | Creation timestamp |
|
||||
| StartDate | Datetime | Start date |
|
||||
| EndDate | Datetime | Soft delete |
|
||||
|
||||
---
|
||||
|
||||
## Key Concept: NumRefType vs RangeType
|
||||
|
||||
| Aspect | NumRefType | RangeType |
|
||||
|--------|------------|-----------|
|
||||
| **Location** | Main Reff Tab + Advanced Dialog | Advanced Dialog |
|
||||
| **Purpose** | Input format | Result categorization |
|
||||
| **Values** | 1=NMRC, 2=TH, 3=TEXT, 4=LIST | 1=REF, 2=CRTC, 3=VAL, 4=RERUN |
|
||||
| **Database Field** | NumRefType | RangeType |
|
||||
|
||||
---
|
||||
|
||||
## UI Design
|
||||
|
||||
### Main Reff Tab (Simple)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Reference Ranges │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Ref Type: [ Numeric (NMRC) ▼ ] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ For Numeric (with operators > < <= >=): ││
|
||||
│ │ Ref Low: [0.00 ] Ref High: [100.00 ] ││
|
||||
│ │ Crit Low: [<55.00 ] Crit High: [>115.00 ] ││
|
||||
│ │ ││
|
||||
│ │ Examples: 0-100, <50, >=100, <>0 (not equal to 0) ││
|
||||
│ └─────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐│
|
||||
│ │ For Threshold: ││
|
||||
│ │ Below Text: [Below Normal] Below Value: [<] [50] ││
|
||||
│ │ Above Text: [Above Normal] Above Value: [>] [150] ││
|
||||
│ └─────────────────────────────────────────────────────────────┘│
|
||||
│ │
|
||||
│ [Advanced Settings ▼] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Advanced Dialog (Multiple Reference Ranges)
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Advanced Reference Ranges [X]Close│
|
||||
├───────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [Add RefType ▼] [Add Button] │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ RefType │ Flag/Label │ RangeType │ Sex │ Age │ Low │ High │ [×] │ │
|
||||
│ │─────────┼────────────┼───────────┼─────┼─────┼────────┼────────┼───────│ │
|
||||
│ │ NMRC │ Negative │ REF (1) │ All │ 0-150│ 0 │ 25 │ [×] │ │
|
||||
│ │ NMRC │ Borderline │ REF (1) │ All │ 0-150│ 25 │ 50 │ [×] │ │
|
||||
│ │ NMRC │ Positive │ REF (1) │ All │ 0-150│ 50 │ │ [×] │ │
|
||||
│ │ TEXT │ Negative │ REF (1) │ All │ 0-150│ │ │ [×] │ │
|
||||
│ │ TH │ Low │ REF (1) │ All │ 0-150│ <50 │ │ [×] │ │
|
||||
│ │ NMRC │ Critical │ CRTC (2) │ All │ 0-150│ <55 │ >115 │ [×] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Cancel] [Save Advanced Ranges] │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- **RefType** column: NMRC (1), TH (2), TEXT (3), LIST (4)
|
||||
- **RangeType** column: REF (1), CRTC (2), VAL (3), RERUN (4)
|
||||
- **Flag** column: Display label for the result (e.g., "Negative", "Borderline")
|
||||
- **Low/High** columns: Support operators via LowSign/HighSign fields
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Backend Changes (Tests.php Controller)
|
||||
|
||||
#### 1.1 Add RefType and RangeType constants
|
||||
```php
|
||||
// At top of Tests.php
|
||||
const REFTYPE_NMRC = 1;
|
||||
const REFTYPE_TH = 2;
|
||||
const REFTYPE_TEXT = 3;
|
||||
const REFTYPE_LIST = 4;
|
||||
|
||||
const RANGETYPE_REF = 1;
|
||||
const RANGETYPE_CRTC = 2;
|
||||
const RANGETYPE_VAL = 3;
|
||||
const RANGETYPE_RERUN = 4;
|
||||
|
||||
const LOWSIGN_LT = 1;
|
||||
const LOWSIGN_LTE = 2;
|
||||
const LOWSIGN_GTE = 3;
|
||||
const LOWSIGN_GT = 4;
|
||||
const LOWSIGN_NE = 5;
|
||||
```
|
||||
|
||||
#### 1.2 Update `show()` method to load refnum data
|
||||
```php
|
||||
// Add after loading testdeftech/testdefcal
|
||||
$row['refnum'] = $this->RefNumModel->where('TestSiteID', $id)
|
||||
->where('EndDate IS NULL')
|
||||
->orderBy('Display', 'ASC')
|
||||
->findAll();
|
||||
```
|
||||
|
||||
#### 1.3 Update `saveRefNum()` helper method
|
||||
```php
|
||||
private function saveRefNum($testSiteID, $refRanges, $action, $siteID = 1) {
|
||||
if ($action === 'update') {
|
||||
// Soft delete existing refnums
|
||||
$this->RefNumModel->where('TestSiteID', $testSiteID)
|
||||
->set('EndDate', date('Y-m-d H:i:s'))
|
||||
->update();
|
||||
}
|
||||
|
||||
foreach ($refRanges as $index => $ref) {
|
||||
$refData = [
|
||||
'TestSiteID' => $testSiteID,
|
||||
'SiteID' => $siteID,
|
||||
'NumRefType' => $ref['RefType'] ?? self::REFTYPE_NMRC,
|
||||
'RangeType' => $ref['RangeType'] ?? self::RANGETYPE_REF,
|
||||
'Flag' => $ref['Flag'] ?? null, // Label for display
|
||||
'Sex' => $ref['Sex'] ?? 0, // 0=All, 1=M, 2=F (from valueset)
|
||||
'AgeStart' => $ref['AgeStart'] ?? 0,
|
||||
'AgeEnd' => $ref['AgeEnd'] ?? 150,
|
||||
'LowSign' => $this->parseSign($ref['Low'] ?? ''),
|
||||
'Low' => $this->parseValue($ref['Low'] ?? ''),
|
||||
'HighSign' => $this->parseSign($ref['High'] ?? ''),
|
||||
'High' => $this->parseValue($ref['High'] ?? ''),
|
||||
'Display' => $index,
|
||||
'CreateDate' => date('Y-m-d H:i:s')
|
||||
];
|
||||
$this->RefNumModel->insert($refData);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to extract operator from value like "<=50"
|
||||
private function parseSign($value) {
|
||||
if (str_starts_with($value, '<>')) return self::LOWSIGN_NE;
|
||||
if (str_starts_with($value, '<=')) return self::LOWSIGN_LTE;
|
||||
if (str_starts_with($value, '<')) return self::LOWSIGN_LT;
|
||||
if (str_starts_with($value, '>=')) return self::LOWSIGN_GTE;
|
||||
if (str_starts_with($value, '>')) return self::LOWSIGN_GT;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper to extract numeric value from operator-prefixed string
|
||||
private function parseValue($value) {
|
||||
return preg_replace('/^[<>=<>]+/', '', $value) ?: null;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 Update `handleDetails()` to save refnum
|
||||
```php
|
||||
// Add in handleDetails method, after saving tech/calc details
|
||||
if (isset($input['refnum']) && is_array($input['refnum'])) {
|
||||
$this->saveRefNum($testSiteID, $input['refnum'], $action, $input['SiteID'] ?? 1);
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.5 Update `delete()` to soft delete refnum
|
||||
```php
|
||||
// Add in delete method
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$this->RefNumModel->where('TestSiteID', $id)
|
||||
->set('EndDate', $now)
|
||||
->update();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Frontend Changes (tests_index.php)
|
||||
|
||||
#### 2.1 Update form state to include advanced ref ranges
|
||||
```javascript
|
||||
form: {
|
||||
// ... existing fields ...
|
||||
// Advanced ranges
|
||||
refRanges: [], // Array of advanced reference range objects
|
||||
// Dialog states
|
||||
showAdvancedRefModal: false,
|
||||
advancedRefRanges: [],
|
||||
newRefType: 1 // Default: NMRC
|
||||
}
|
||||
|
||||
// RefType options for select
|
||||
refTypeOptions: [
|
||||
{ value: 1, label: 'Numeric (NMRC)' },
|
||||
{ value: 2, label: 'Threshold (TH)' },
|
||||
{ value: 3, label: 'Text (TEXT)' },
|
||||
{ value: 4, label: 'Value Set (LIST)' }
|
||||
]
|
||||
|
||||
// RangeType options
|
||||
rangeTypeOptions: [
|
||||
{ value: 1, label: 'REF' },
|
||||
{ value: 2, label: 'CRTC' },
|
||||
{ value: 3, label: 'VAL' },
|
||||
{ value: 4, label: 'RERUN' }
|
||||
]
|
||||
|
||||
// Sex options
|
||||
sexOptions: [
|
||||
{ value: 0, label: 'All' },
|
||||
{ value: 1, label: 'Male' },
|
||||
{ value: 2, label: 'Female' }
|
||||
]
|
||||
```
|
||||
|
||||
#### 2.2 Update `editTest()` to load refnum data
|
||||
```javascript
|
||||
if (testData.refnum && testData.refnum.length > 0) {
|
||||
this.form.refRanges = testData.refnum.map(r => ({
|
||||
RefNumID: r.RefNumID,
|
||||
RefType: r.NumRefType || 1,
|
||||
RangeType: r.RangeType || 1,
|
||||
Flag: r.Flag || '',
|
||||
Sex: r.Sex || 0,
|
||||
AgeStart: r.AgeStart || 0,
|
||||
AgeEnd: r.AgeEnd || 150,
|
||||
Low: this.formatValueWithSign(r.LowSign, r.Low),
|
||||
High: this.formatValueWithSign(r.HighSign, r.High)
|
||||
}));
|
||||
} else {
|
||||
this.form.refRanges = [];
|
||||
}
|
||||
|
||||
// Format value with operator sign for display
|
||||
formatValueWithSign(sign, value) {
|
||||
if (!value && value !== 0) return '';
|
||||
const signs = {
|
||||
1: '<', 2: '<=', 3: '>=', 4: '>', 5: '<>'
|
||||
};
|
||||
return (signs[sign] || '') + value;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 Update `save()` to include refnum in payload
|
||||
```javascript
|
||||
if (this.form.refRanges && this.form.refRanges.length > 0) {
|
||||
payload.refnum = this.form.refRanges.map(r => ({
|
||||
RefType: r.RefType,
|
||||
RangeType: r.RangeType,
|
||||
Flag: r.Flag,
|
||||
Sex: r.Sex,
|
||||
AgeStart: r.AgeStart,
|
||||
AgeEnd: r.AgeEnd,
|
||||
Low: r.Low,
|
||||
High: r.High
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.4 Add helper methods for advanced ref ranges
|
||||
```javascript
|
||||
// Open advanced dialog
|
||||
openAdvancedRefDialog() {
|
||||
this.advancedRefRanges = this.form.refRanges.length > 0
|
||||
? [...this.form.refRanges]
|
||||
: [{
|
||||
RefNumID: null,
|
||||
RefType: this.form.RefType || 1,
|
||||
RangeType: 1,
|
||||
Flag: '',
|
||||
Sex: 0,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 150,
|
||||
Low: this.form.RefLow || '',
|
||||
High: this.form.RefHigh || ''
|
||||
}];
|
||||
|
||||
// Add CRTC if critical values exist
|
||||
if ((this.form.CritLow || this.form.CritHigh) &&
|
||||
!this.advancedRefRanges.some(r => r.RangeType === 2)) {
|
||||
this.advancedRefRanges.push({
|
||||
RefNumID: null,
|
||||
RefType: 1,
|
||||
RangeType: 2,
|
||||
Flag: 'Critical',
|
||||
Sex: 0,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 150,
|
||||
Low: this.form.CritLow || '',
|
||||
High: this.form.CritHigh || ''
|
||||
});
|
||||
}
|
||||
|
||||
this.showAdvancedRefModal = true;
|
||||
},
|
||||
|
||||
// Add new advanced range
|
||||
addAdvancedRefRange() {
|
||||
this.advancedRefRanges.push({
|
||||
RefNumID: null,
|
||||
RefType: this.newRefType,
|
||||
RangeType: 1,
|
||||
Flag: '',
|
||||
Sex: 0,
|
||||
AgeStart: 0,
|
||||
AgeEnd: 150,
|
||||
Low: '',
|
||||
High: ''
|
||||
});
|
||||
},
|
||||
|
||||
// Remove advanced range
|
||||
removeAdvancedRefRange(index) {
|
||||
this.advancedRefRanges.splice(index, 1);
|
||||
},
|
||||
|
||||
// Save advanced ranges and close
|
||||
saveAdvancedRefRanges() {
|
||||
this.form.refRanges = [...this.advancedRefRanges];
|
||||
this.showAdvancedRefModal = false;
|
||||
},
|
||||
|
||||
// Cancel advanced dialog
|
||||
cancelAdvancedRefDialog() {
|
||||
this.showAdvancedRefModal = false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: UI Changes (test_dialog.php)
|
||||
|
||||
#### 3.1 Keep main Reff tab with RefType selector
|
||||
```html
|
||||
<!-- Reff Tab - Main (Simple) -->
|
||||
<div x-show="form.dialogTab === 'reff'" class="space-y-4" x-cloak>
|
||||
|
||||
<!-- RefType Selector -->
|
||||
<div>
|
||||
<label class="label"><span class="label-text font-medium">Reference Type</span></label>
|
||||
<select class="select w-full" x-model="form.RefType">
|
||||
<option value="1">Numeric Range (NMRC)</option>
|
||||
<option value="2">Threshold (TH)</option>
|
||||
<option value="3">Text Result (TEXT)</option>
|
||||
<option value="4">Value Set (LIST)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Numeric Range Fields -->
|
||||
<template x-if="form.RefType == '1'">
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Ref Low</span></label>
|
||||
<input type="text" class="input" x-model="form.RefLow" placeholder="0.00" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Ref High</span></label>
|
||||
<input type="text" class="input" x-model="form.RefHigh" placeholder="10.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text text-error">Crit Low</span></label>
|
||||
<input type="text" class="input border-error/30" x-model="form.CritLow" placeholder="0.00" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text text-error">Crit High</span></label>
|
||||
<input type="text" class="input border-error/30" x-model="form.CritHigh" placeholder="20.00" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Unit</span></label>
|
||||
<input type="text" class="input" x-model="form.Unit1" placeholder="mg/dL" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Decimals</span></label>
|
||||
<input type="number" class="input" x-model="form.Decimal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Threshold Fields -->
|
||||
<template x-if="form.RefType == '2'">
|
||||
<div class="space-y-4">
|
||||
<div class="p-4 bg-info/10 border border-info/20 rounded-lg">
|
||||
<p class="text-sm mb-3"><strong>Below Threshold:</strong></p>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Below Text</span></label>
|
||||
<input type="text" class="input" x-model="form.RefText" placeholder="Below Normal" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Operator</span></label>
|
||||
<select class="select" x-model="form.BelowOp">
|
||||
<option value="<"><</option>
|
||||
<option value="<="><=</option>
|
||||
<option value="<>"><></option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Value</span></label>
|
||||
<input type="text" class="input" x-model="form.BelowVal" placeholder="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-warning/10 border border-warning/20 rounded-lg">
|
||||
<p class="text-sm mb-3"><strong>Above Threshold:</strong></p>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Above Text</span></label>
|
||||
<input type="text" class="input" x-model="form.AboveText" placeholder="Above Normal" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Operator</span></label>
|
||||
<select class="select" x-model="form.AboveOp">
|
||||
<option value=">">></option>
|
||||
<option value=">=">>=</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Value</span></label>
|
||||
<input type="text" class="input" x-model="form.AboveVal" placeholder="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Text Result Fields -->
|
||||
<template x-if="form.RefType == '3'">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Default Text</span></label>
|
||||
<input type="text" class="input" x-model="form.RefText" placeholder="e.g., Negative" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Value Set Fields -->
|
||||
<template x-if="form.RefType == '4'">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="label"><span class="label-text">Value Set</span></label>
|
||||
<select class="select w-full" x-model="form.VSetDefID">
|
||||
<option value="">Select Value Set...</option>
|
||||
<template x-for="v in vsetDefsList" :key="v.VSetDefID">
|
||||
<option :value="v.VSetDefID" x-text="v.VSDesc"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Advanced Button -->
|
||||
<div class="mt-4 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-outline btn-sm" @click="openAdvancedRefDialog()">
|
||||
<i class="fa-solid fa-gear mr-1"></i>
|
||||
Advanced Settings
|
||||
</button>
|
||||
<span class="ml-2 text-xs opacity-60" x-show="form.refRanges.length > 0">
|
||||
<i class="fa-solid fa-check text-success mr-1"></i>
|
||||
<span x-text="form.refRanges.length + ' advanced ranges configured'"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 3.2 Add Advanced RefRanges Modal
|
||||
```html
|
||||
<!-- Advanced Reference Ranges Modal -->
|
||||
<div x-show="showAdvancedRefModal" x-cloak class="modal-overlay" @click.self="cancelAdvancedRefDialog()">
|
||||
<div class="modal-content p-6 max-w-5xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg">Advanced Reference Ranges</h3>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="cancelAdvancedRefDialog()">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Row Controls -->
|
||||
<div class="flex gap-2 mb-4 p-3 bg-base-200 rounded-lg">
|
||||
<select class="select select-sm" x-model="newRefType">
|
||||
<option :value="1">Numeric (NMRC)</option>
|
||||
<option :value="2">Threshold (TH)</option>
|
||||
<option :value="3">Text (TEXT)</option>
|
||||
<option :value="4">Value Set (LIST)</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline" @click="addAdvancedRefRange()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add Range
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Ranges Table -->
|
||||
<div class="overflow-x-auto mb-4">
|
||||
<table class="table table-sm table-compact w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80px;">RefType</th>
|
||||
<th style="width: 120px;">Flag/Label</th>
|
||||
<th style="width: 80px;">RangeType</th>
|
||||
<th style="width: 60px;">Sex</th>
|
||||
<th style="width: 70px;">Age From</th>
|
||||
<th style="width: 70px;">Age To</th>
|
||||
<th style="width: 100px;">Low</th>
|
||||
<th style="width: 100px;">High</th>
|
||||
<th style="width: 40px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="(ref, index) in advancedRefRanges" :key="index">
|
||||
<tr :class="{ 'bg-error/5': ref.RangeType == 2 }">
|
||||
<!-- RefType -->
|
||||
<td>
|
||||
<select
|
||||
class="select select-xs w-full"
|
||||
x-model="ref.RefType"
|
||||
>
|
||||
<option :value="1">NMRC</option>
|
||||
<option :value="2">TH</option>
|
||||
<option :value="3">TEXT</option>
|
||||
<option :value="4">LIST</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<!-- Flag/Label -->
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-xs w-full"
|
||||
x-model="ref.Flag"
|
||||
placeholder="e.g., Negative"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- RangeType -->
|
||||
<td>
|
||||
<select
|
||||
class="select select-xs w-full"
|
||||
:class="{ 'border-error/30 bg-error/10': ref.RangeType == 2 }"
|
||||
x-model="ref.RangeType"
|
||||
>
|
||||
<option :value="1">REF</option>
|
||||
<option :value="2">CRTC</option>
|
||||
<option :value="3">VAL</option>
|
||||
<option :value="4">RERUN</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<!-- Sex -->
|
||||
<td>
|
||||
<select class="select select-xs w-full" x-model="ref.Sex">
|
||||
<option :value="0">All</option>
|
||||
<option :value="1">M</option>
|
||||
<option :value="2">F</option>
|
||||
</select>
|
||||
</td>
|
||||
|
||||
<!-- Age From -->
|
||||
<td>
|
||||
<input type="number" class="input input-xs w-full text-center" x-model="ref.AgeStart" />
|
||||
</td>
|
||||
|
||||
<!-- Age To -->
|
||||
<td>
|
||||
<input type="number" class="input input-xs w-full text-center" x-model="ref.AgeEnd" />
|
||||
</td>
|
||||
|
||||
<!-- Low -->
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-xs w-full text-center"
|
||||
:class="{ 'border-error/30': ref.RangeType == 2 }"
|
||||
x-model="ref.Low"
|
||||
placeholder="0.00 or <=10"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- High -->
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-xs w-full text-center"
|
||||
:class="{ 'border-error/30': ref.RangeType == 2 }"
|
||||
x-model="ref.High"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- Delete -->
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error" @click="removeAdvancedRefRange(index)">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Empty State -->
|
||||
<template x-if="advancedRefRanges.length === 0">
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-8 text-base-400">
|
||||
<i class="fa-solid fa-layer-group mr-2"></i>
|
||||
No advanced ranges. Click "Add Range" to create one.
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="text-xs opacity-60 p-2 border rounded bg-base-200 flex gap-4 mb-4">
|
||||
<span><strong>1</strong>=NMRC</span>
|
||||
<span><strong>2</strong>=TH</span>
|
||||
<span><strong>3</strong>=TEXT</span>
|
||||
<span><strong>4</strong>=LIST</span>
|
||||
<span class="ml-4"><strong>1</strong>=REF</span>
|
||||
<span><strong>2</strong>=CRTC</span>
|
||||
<span><strong>3</strong>=VAL</span>
|
||||
<span><strong>4</strong>=RERUN</span>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 pt-4 border-t" style="border-color: rgb(var(--color-border));">
|
||||
<button class="btn btn-ghost flex-1" @click="cancelAdvancedRefDialog()">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="saveAdvancedRefRanges()">
|
||||
<i class="fa-solid fa-check mr-1"></i> Save Advanced Ranges
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `app/Controllers/Tests.php` | Add constants, refnum loading/save helper, delete update |
|
||||
| `app/Models/RefRange/RefNumModel.php` | Ensure allowedFields includes all needed fields |
|
||||
| `app/Views/v2/master/tests/tests_index.php` | Add refRanges state, helper methods, modal state |
|
||||
| `app/Views/v2/master/tests/test_dialog.php` | Update Reff tab with numeric RefType, add Advanced modal |
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Basic users** - Use global RefLow/RefHigh fields on main tab
|
||||
2. **Advanced users** - Click "Advanced Settings" to open modal
|
||||
3. **Modal** - Add/edit/remove multiple ranges with criteria
|
||||
4. **Save** - Advanced ranges saved to refnum table, global fields saved to testdeftech
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review this plan
|
||||
2. Provide feedback or request changes
|
||||
3. Once approved, switch to Code mode for implementation
|
||||
Loading…
x
Reference in New Issue
Block a user