2025-09-19 15:22:25 +07:00
< ? php
namespace App\Controllers ;
2026-02-18 10:15:47 +07:00
use App\Traits\ResponseTrait ;
2025-09-19 15:22:25 +07:00
use CodeIgniter\Controller ;
2026-01-28 17:31:00 +07:00
use App\Libraries\ValueSet ;
2026-03-03 13:51:27 +07:00
use App\Models\OrderTest\OrderTestModel ;
2026-01-12 16:53:41 +07:00
use App\Models\Patient\PatientModel ;
2026-03-03 13:51:27 +07:00
use App\Models\PatVisit\PatVisitModel ;
2026-03-12 06:34:56 +07:00
use App\Services\RuleEngineService ;
2025-09-19 15:22:25 +07:00
2026-01-05 16:55:34 +07:00
class OrderTestController extends Controller {
2025-09-19 15:22:25 +07:00
use ResponseTrait ;
2026-01-12 16:53:41 +07:00
protected $db ;
protected $model ;
protected $patientModel ;
protected $visitModel ;
protected $rules ;
2025-09-19 15:22:25 +07:00
public function __construct () {
$this -> db = \Config\Database :: connect ();
2026-01-12 16:53:41 +07:00
$this -> model = new OrderTestModel ();
$this -> patientModel = new PatientModel ();
$this -> visitModel = new PatVisitModel ();
$this -> rules = [
'InternalPID' => 'required|is_natural'
2025-09-19 15:22:25 +07:00
];
}
public function index () {
2026-01-12 16:53:41 +07:00
$internalPID = $this -> request -> getVar ( 'InternalPID' );
2026-03-03 13:51:27 +07:00
$includeDetails = $this -> request -> getVar ( 'include' ) === 'details' ;
2026-01-12 16:53:41 +07:00
try {
if ( $internalPID ) {
$rows = $this -> model -> getOrdersByPatient ( $internalPID );
} else {
$rows = $this -> db -> table ( 'ordertest' )
-> where ( 'DelDate' , null )
feat(api): transition to headless architecture and enhance order management
This commit marks a significant architectural shift, transitioning the CLQMS backend to a fully headless REST API. All view-related components have been removed to focus solely on providing a robust, stateless API for clinical laboratory workflows.
### Architectural Changes
- **Headless API Transition:**
- Removed all view files (`app/Views/v2`), associated page controllers (`PagesController`), and routes (`Routes.php`). The application no longer serves a front-end UI.
- The root endpoint (`/`) now returns a simple "Backend Running" status message.
- **Developer Tooling & Guidance:**
- Replaced `CLAUDE.md` with `GEMINI.md` to provide updated context and instructional guidelines for Gemini agents.
- Updated `.serena/project.yml` with project configuration.
### Feature Enhancements
- **Advanced Order Management (`OrderTestModel`):**
- **Test Expansion:** The `createOrder` process now automatically expands `GROUP` (panel) tests into their individual components and recursively includes all parameter dependencies for `CALC` (calculated) tests.
- **Order Comments:** Added support for attaching comments to an order via the `ordercom` table.
- **Status Tracking:** Order status updates are now correctly recorded in the `orderstatus` table.
- **Schema Alignment:** Switched from `OrderID` to `InternalOID` as the primary key for internal operations.
- **Reference Range Refactor (`TestsController`):**
- Simplified reference range logic by consolidating `refthold` and `refvset` into the main `refnum` and `reftxt` tables.
- Standardized `RefType` handling to support `NMRC`, `TEXT`, `THOLD`, and `VSET` codes from the `reference_type` ValueSet.
### Other Changes
- **Documentation:**
- `PRD.md`, `README.md`, and `TODO.md` were updated to reflect the headless architecture, refined scope, and current project priorities.
- **Database:**
- Removed obsolete `RefTHoldID` and `RefVSetID` columns from the `patres` table migration.
- **Testing:**
- Added new feature tests for `ContactController`, `OrganizationController`, and `TestsController`.
2026-01-31 09:27:32 +07:00
-> orderBy ( 'TrnDate' , 'DESC' )
2026-01-12 16:53:41 +07:00
-> get ()
-> getResultArray ();
}
2025-09-19 15:22:25 +07:00
refactor(api): standardize ValueSet label transformation across controllers
Replace manual label lookup code with ValueSet::transformLabels() helper
for consistent API responses across all controllers.
Updated controllers:
- ContactController: Specialty, Occupation
- OrderTestController: Priority, OrderStatus
- PatientController: Sex
- ContainerDefController: ConCategory, CapColor, ConSize
- SpecimenCollectionController: CollectionMethod, Additive, SpecimenRole
- SpecimenController: SpecimenType, SpecimenStatus, BodySite
- SpecimenStatusController: Status, Activity
- DemoOrderController: Priority, OrderStatus
- TestMapController: HostType, ClientType
- TestsController: Reference range fields
Also updated api-docs.yaml field naming convention to PascalCase
2026-01-29 09:05:40 +07:00
$rows = ValueSet :: transformLabels ( $rows , [
'Priority' => 'order_priority' ,
'OrderStatus' => 'order_status' ,
]);
2026-01-28 17:31:00 +07:00
2026-03-03 13:51:27 +07:00
if ( $includeDetails && ! empty ( $rows )) {
foreach ( $rows as & $row ) {
$row [ 'Specimens' ] = $this -> getOrderSpecimens ( $row [ 'InternalOID' ]);
$row [ 'Tests' ] = $this -> getOrderTests ( $row [ 'InternalOID' ]);
}
}
2025-09-19 15:22:25 +07:00
return $this -> respond ([
2026-01-12 16:53:41 +07:00
'status' => 'success' ,
'message' => 'Data fetched successfully' ,
'data' => $rows
2025-09-19 15:22:25 +07:00
], 200 );
2026-01-12 16:53:41 +07:00
} catch ( \Exception $e ) {
return $this -> failServerError ( 'Something went wrong: ' . $e -> getMessage ());
2025-09-19 15:22:25 +07:00
}
}
2026-01-12 16:53:41 +07:00
public function show ( $orderID = null ) {
try {
$row = $this -> model -> getOrder ( $orderID );
if ( empty ( $row )) {
return $this -> respond ([
'status' => 'success' ,
'message' => 'Data not found.' ,
'data' => null
], 200 );
}
2026-01-28 17:31:00 +07:00
refactor(api): standardize ValueSet label transformation across controllers
Replace manual label lookup code with ValueSet::transformLabels() helper
for consistent API responses across all controllers.
Updated controllers:
- ContactController: Specialty, Occupation
- OrderTestController: Priority, OrderStatus
- PatientController: Sex
- ContainerDefController: ConCategory, CapColor, ConSize
- SpecimenCollectionController: CollectionMethod, Additive, SpecimenRole
- SpecimenController: SpecimenType, SpecimenStatus, BodySite
- SpecimenStatusController: Status, Activity
- DemoOrderController: Priority, OrderStatus
- TestMapController: HostType, ClientType
- TestsController: Reference range fields
Also updated api-docs.yaml field naming convention to PascalCase
2026-01-29 09:05:40 +07:00
$row = ValueSet :: transformLabels ([ $row ], [
'Priority' => 'order_priority' ,
'OrderStatus' => 'order_status' ,
])[ 0 ];
2026-01-28 17:31:00 +07:00
2026-03-03 13:51:27 +07:00
// Include specimens and tests
$row [ 'Specimens' ] = $this -> getOrderSpecimens ( $row [ 'InternalOID' ]);
$row [ 'Tests' ] = $this -> getOrderTests ( $row [ 'InternalOID' ]);
2025-09-19 15:22:25 +07:00
return $this -> respond ([
2026-01-12 16:53:41 +07:00
'status' => 'success' ,
'message' => 'Data fetched successfully' ,
'data' => $row
2025-09-19 15:22:25 +07:00
], 200 );
2026-01-12 16:53:41 +07:00
} catch ( \Exception $e ) {
return $this -> failServerError ( 'Something went wrong: ' . $e -> getMessage ());
2025-09-19 15:22:25 +07:00
}
}
2026-03-03 13:51:27 +07:00
private function getOrderSpecimens ( $internalOID ) {
$specimens = $this -> db -> table ( 'specimen s' )
-> select ( 's.*, cd.ConCode, cd.ConName' )
-> join ( 'containerdef cd' , 'cd.ConDefID = s.ConDefID' , 'left' )
-> where ( 's.OrderID' , $internalOID )
-> where ( 's.EndDate IS NULL' )
-> get ()
-> getResultArray ();
// Get status for each specimen
foreach ( $specimens as & $specimen ) {
$status = $this -> db -> table ( 'specimenstatus' )
-> where ( 'SID' , $specimen [ 'SID' ])
-> where ( 'EndDate IS NULL' )
-> orderBy ( 'CreateDate' , 'DESC' )
-> get ()
-> getRowArray ();
$specimen [ 'Status' ] = $status [ 'SpcStatus' ] ? ? 'PENDING' ;
}
return $specimens ;
}
private function getOrderTests ( $internalOID ) {
2026-03-12 16:55:03 +07:00
$tests = $this -> db -> table ( 'patres pr' )
-> select ( 'pr.*, tds.TestSiteCode, tds.TestSiteName, tds.TestType, tds.SeqScr AS TestSeqScr, tds.SeqRpt AS TestSeqRpt, tds.DisciplineID, d.DisciplineCode, d.DisciplineName, d.SeqScr AS DisciplineSeqScr, d.SeqRpt AS DisciplineSeqRpt' )
2026-03-03 13:51:27 +07:00
-> join ( 'testdefsite tds' , 'tds.TestSiteID = pr.TestSiteID' , 'left' )
2026-03-12 16:55:03 +07:00
-> join ( 'discipline d' , 'd.DisciplineID = tds.DisciplineID' , 'left' )
2026-03-03 13:51:27 +07:00
-> where ( 'pr.OrderID' , $internalOID )
-> where ( 'pr.DelDate IS NULL' )
2026-03-12 16:55:03 +07:00
-> orderBy ( 'COALESCE(d.SeqScr, 999999) ASC' )
-> orderBy ( 'COALESCE(d.SeqRpt, 999999) ASC' )
-> orderBy ( 'COALESCE(tds.SeqScr, 999999) ASC' )
-> orderBy ( 'COALESCE(tds.SeqRpt, 999999) ASC' )
-> orderBy ( 'pr.ResultID ASC' )
2026-03-03 13:51:27 +07:00
-> get ()
-> getResultArray ();
2026-03-12 16:55:03 +07:00
foreach ( $tests as & $test ) {
$discipline = [
'DisciplineID' => $test [ 'DisciplineID' ] ? ? null ,
'DisciplineCode' => $test [ 'DisciplineCode' ] ? ? null ,
'DisciplineName' => $test [ 'DisciplineName' ] ? ? null ,
'SeqScr' => $test [ 'DisciplineSeqScr' ] ? ? null ,
'SeqRpt' => $test [ 'DisciplineSeqRpt' ] ? ? null ,
];
$test [ 'Discipline' ] = $discipline ;
$test [ 'SeqScr' ] = $test [ 'TestSeqScr' ] ? ? null ;
$test [ 'SeqRpt' ] = $test [ 'TestSeqRpt' ] ? ? null ;
$test [ 'DisciplineID' ] = $discipline [ 'DisciplineID' ];
unset ( $test [ 'DisciplineCode' ], $test [ 'DisciplineName' ], $test [ 'DisciplineSeqScr' ], $test [ 'DisciplineSeqRpt' ], $test [ 'TestSeqScr' ], $test [ 'TestSeqRpt' ]);
}
unset ( $test );
return $tests ;
2026-03-03 13:51:27 +07:00
}
2025-09-19 15:22:25 +07:00
public function create () {
2026-01-12 16:53:41 +07:00
$input = $this -> request -> getJSON ( true );
2025-09-19 15:22:25 +07:00
2026-01-12 16:53:41 +07:00
if ( ! $this -> validateData ( $input , $this -> rules )) {
return $this -> failValidationErrors ( $this -> validator -> getErrors ());
}
2025-09-19 15:22:25 +07:00
2026-01-12 16:53:41 +07:00
try {
if ( ! $this -> patientModel -> find ( $input [ 'InternalPID' ])) {
return $this -> failValidationErrors ([ 'InternalPID' => 'Patient not found' ]);
2025-09-19 15:22:25 +07:00
}
2026-01-12 16:53:41 +07:00
if ( ! empty ( $input [ 'PatVisitID' ])) {
$visit = $this -> visitModel -> find ( $input [ 'PatVisitID' ]);
if ( ! $visit ) {
return $this -> failValidationErrors ([ 'PatVisitID' => 'Visit not found' ]);
}
2025-09-19 15:22:25 +07:00
}
2026-01-12 16:53:41 +07:00
$orderID = $this -> model -> createOrder ( $input );
2026-03-12 06:34:56 +07:00
2026-03-03 13:51:27 +07:00
// Fetch complete order details
$order = $this -> model -> getOrder ( $orderID );
$order [ 'Specimens' ] = $this -> getOrderSpecimens ( $order [ 'InternalOID' ]);
$order [ 'Tests' ] = $this -> getOrderTests ( $order [ 'InternalOID' ]);
2025-09-19 15:22:25 +07:00
2026-03-12 06:34:56 +07:00
// Run common rules for ORDER_CREATED (non-blocking)
try {
$ruleEngine = new RuleEngineService ();
$ruleEngine -> run ( 'ORDER_CREATED' , [
'order' => $order ,
'tests' => $order [ 'Tests' ],
'input' => $input ,
]);
// Refresh tests in case rules updated results
$order [ 'Tests' ] = $this -> getOrderTests ( $order [ 'InternalOID' ]);
} catch ( \Throwable $e ) {
log_message ( 'error' , 'OrderTestController::create rule engine error: ' . $e -> getMessage ());
}
2025-09-19 15:22:25 +07:00
return $this -> respondCreated ([
2026-01-12 16:53:41 +07:00
'status' => 'success' ,
'message' => 'Order created successfully' ,
2026-03-03 13:51:27 +07:00
'data' => $order
2025-09-19 15:22:25 +07:00
], 201 );
2026-01-12 16:53:41 +07:00
} catch ( \Exception $e ) {
2025-09-19 15:22:25 +07:00
return $this -> failServerError ( 'Something went wrong: ' . $e -> getMessage ());
}
}
public function update () {
2026-01-12 16:53:41 +07:00
$input = $this -> request -> getJSON ( true );
2025-09-19 15:22:25 +07:00
2026-01-12 16:53:41 +07:00
if ( empty ( $input [ 'OrderID' ])) {
return $this -> failValidationErrors ([ 'OrderID' => 'OrderID is required' ]);
}
2025-09-19 15:22:25 +07:00
2026-01-12 16:53:41 +07:00
try {
$order = $this -> model -> getOrder ( $input [ 'OrderID' ]);
if ( ! $order ) {
return $this -> failNotFound ( 'Order not found' );
2025-09-19 15:22:25 +07:00
}
2026-01-12 16:53:41 +07:00
$updateData = [];
if ( isset ( $input [ 'Priority' ])) $updateData [ 'Priority' ] = $input [ 'Priority' ];
if ( isset ( $input [ 'OrderStatus' ])) $updateData [ 'OrderStatus' ] = $input [ 'OrderStatus' ];
if ( isset ( $input [ 'OrderingProvider' ])) $updateData [ 'OrderingProvider' ] = $input [ 'OrderingProvider' ];
if ( isset ( $input [ 'DepartmentID' ])) $updateData [ 'DepartmentID' ] = $input [ 'DepartmentID' ];
if ( isset ( $input [ 'WorkstationID' ])) $updateData [ 'WorkstationID' ] = $input [ 'WorkstationID' ];
2025-09-19 15:22:25 +07:00
2026-01-12 16:53:41 +07:00
if ( ! empty ( $updateData )) {
feat(api): transition to headless architecture and enhance order management
This commit marks a significant architectural shift, transitioning the CLQMS backend to a fully headless REST API. All view-related components have been removed to focus solely on providing a robust, stateless API for clinical laboratory workflows.
### Architectural Changes
- **Headless API Transition:**
- Removed all view files (`app/Views/v2`), associated page controllers (`PagesController`), and routes (`Routes.php`). The application no longer serves a front-end UI.
- The root endpoint (`/`) now returns a simple "Backend Running" status message.
- **Developer Tooling & Guidance:**
- Replaced `CLAUDE.md` with `GEMINI.md` to provide updated context and instructional guidelines for Gemini agents.
- Updated `.serena/project.yml` with project configuration.
### Feature Enhancements
- **Advanced Order Management (`OrderTestModel`):**
- **Test Expansion:** The `createOrder` process now automatically expands `GROUP` (panel) tests into their individual components and recursively includes all parameter dependencies for `CALC` (calculated) tests.
- **Order Comments:** Added support for attaching comments to an order via the `ordercom` table.
- **Status Tracking:** Order status updates are now correctly recorded in the `orderstatus` table.
- **Schema Alignment:** Switched from `OrderID` to `InternalOID` as the primary key for internal operations.
- **Reference Range Refactor (`TestsController`):**
- Simplified reference range logic by consolidating `refthold` and `refvset` into the main `refnum` and `reftxt` tables.
- Standardized `RefType` handling to support `NMRC`, `TEXT`, `THOLD`, and `VSET` codes from the `reference_type` ValueSet.
### Other Changes
- **Documentation:**
- `PRD.md`, `README.md`, and `TODO.md` were updated to reflect the headless architecture, refined scope, and current project priorities.
- **Database:**
- Removed obsolete `RefTHoldID` and `RefVSetID` columns from the `patres` table migration.
- **Testing:**
- Added new feature tests for `ContactController`, `OrganizationController`, and `TestsController`.
2026-01-31 09:27:32 +07:00
$this -> model -> update ( $order [ 'InternalOID' ], $updateData );
2025-09-19 15:22:25 +07:00
}
2026-03-03 13:51:27 +07:00
$updatedOrder = $this -> model -> getOrder ( $input [ 'OrderID' ]);
$updatedOrder [ 'Specimens' ] = $this -> getOrderSpecimens ( $updatedOrder [ 'InternalOID' ]);
$updatedOrder [ 'Tests' ] = $this -> getOrderTests ( $updatedOrder [ 'InternalOID' ]);
2026-01-12 16:53:41 +07:00
return $this -> respond ([
'status' => 'success' ,
'message' => 'Order updated successfully' ,
2026-03-03 13:51:27 +07:00
'data' => $updatedOrder
2026-01-12 16:53:41 +07:00
], 200 );
} catch ( \Exception $e ) {
2025-09-19 15:22:25 +07:00
return $this -> failServerError ( 'Something went wrong: ' . $e -> getMessage ());
}
}
public function delete () {
2026-01-12 16:53:41 +07:00
$input = $this -> request -> getJSON ( true );
$orderID = $input [ 'OrderID' ] ? ? null ;
2025-09-19 15:22:25 +07:00
2026-01-12 16:53:41 +07:00
if ( empty ( $orderID )) {
return $this -> failValidationErrors ([ 'OrderID' => 'OrderID is required' ]);
}
try {
$order = $this -> model -> getOrder ( $orderID );
if ( ! $order ) {
return $this -> failNotFound ( 'Order not found' );
2025-09-19 15:22:25 +07:00
}
2026-01-12 16:53:41 +07:00
$this -> model -> softDelete ( $orderID );
2025-09-19 15:22:25 +07:00
return $this -> respondDeleted ([
'status' => 'success' ,
2026-01-12 16:53:41 +07:00
'message' => 'Order deleted successfully'
2025-09-19 15:22:25 +07:00
]);
2026-01-12 16:53:41 +07:00
} catch ( \Exception $e ) {
2025-09-19 15:22:25 +07:00
return $this -> failServerError ( 'Something went wrong: ' . $e -> getMessage ());
}
}
2026-01-12 16:53:41 +07:00
public function updateStatus () {
$input = $this -> request -> getJSON ( true );
if ( empty ( $input [ 'OrderID' ]) || empty ( $input [ 'OrderStatus' ])) {
return $this -> failValidationErrors ([ 'error' => 'OrderID and OrderStatus are required' ]);
2025-09-19 15:22:25 +07:00
}
2026-01-12 16:53:41 +07:00
$validStatuses = [ 'ORD' , 'SCH' , 'ANA' , 'VER' , 'REV' , 'REP' ];
if ( ! in_array ( $input [ 'OrderStatus' ], $validStatuses )) {
return $this -> failValidationErrors ([ 'OrderStatus' => 'Invalid status. Valid: ' . implode ( ', ' , $validStatuses )]);
}
2025-09-19 15:22:25 +07:00
2026-01-12 16:53:41 +07:00
try {
$order = $this -> model -> getOrder ( $input [ 'OrderID' ]);
if ( ! $order ) {
return $this -> failNotFound ( 'Order not found' );
}
2025-09-19 15:22:25 +07:00
2026-01-12 16:53:41 +07:00
$this -> model -> updateStatus ( $input [ 'OrderID' ], $input [ 'OrderStatus' ]);
2025-09-19 15:22:25 +07:00
2026-03-03 13:51:27 +07:00
$updatedOrder = $this -> model -> getOrder ( $input [ 'OrderID' ]);
$updatedOrder [ 'Specimens' ] = $this -> getOrderSpecimens ( $updatedOrder [ 'InternalOID' ]);
$updatedOrder [ 'Tests' ] = $this -> getOrderTests ( $updatedOrder [ 'InternalOID' ]);
2026-01-12 16:53:41 +07:00
return $this -> respond ([
'status' => 'success' ,
'message' => 'Order status updated successfully' ,
2026-03-03 13:51:27 +07:00
'data' => $updatedOrder
2026-01-12 16:53:41 +07:00
], 200 );
} catch ( \Exception $e ) {
return $this -> failServerError ( 'Something went wrong: ' . $e -> getMessage ());
}
2025-09-19 15:22:25 +07:00
}
2026-01-05 16:55:34 +07:00
}