clqms-be/docs/manual-result-entry-plan.md

942 lines
22 KiB
Markdown
Raw Normal View History

# Manual Result Entry Implementation Plan
## Overview
This document outlines the implementation plan for manual laboratory result entry functionality in CLQMS. The system already creates empty `patres` records when orders are placed. This plan covers the complete workflow for entering, validating, and verifying test results.
**Current State:** Empty `patres` records exist for all ordered tests
**Target State:** Full result entry with reference range validation, abnormal flag calculation, and verification workflow
---
## Phase 1: Core Result Management (Priority: HIGH)
### 1.1 Extend PatResultModel
**File:** `app/Models/PatResultModel.php`
#### New Methods to Add:
```php
/**
* Get results with filtering and pagination
*
* @param array $filters Available filters:
* - InternalPID: int - Filter by patient
* - OrderID: string - Filter by order
* - ResultStatus: string - PEN, PRE, FIN, AMD
* - TestSiteID: int - Filter by test
* - date_from: string - YYYY-MM-DD
* - date_to: string - YYYY-MM-DD
* - WorkstationID: int - Filter by workstation
* @param int $page
* @param int $perPage
* @return array
*/
public function getResults(array $filters = [], int $page = 1, int $perPage = 20): array
/**
* Get single result with full details
* Includes: patient demographics, test info, specimen info, reference ranges
*
* @param int $resultID
* @return array|null
*/
public function getResultWithDetails(int $resultID): ?array
/**
* Update result with validation
*
* @param int $resultID
* @param array $data
* - Result: string - The result value
* - Unit: string - Unit of measurement (optional)
* - AbnormalFlag: string - H, L, N, A, C (optional, auto-calculated)
* - Comment: string - Result comment (optional)
* - ResultStatus: string - Status update (optional)
* @return bool
*/
public function updateResult(int $resultID, array $data): bool
/**
* Get pending results for a workstation (worklist)
*
* @param int $workstationID
* @param array $filters Additional filters
* @return array
*/
public function getPendingByWorkstation(int $workstationID, array $filters = []): array
/**
* Get all results for an order
*
* @param string $orderID
* @return array
*/
public function getByOrder(string $orderID): array
/**
* Verify a result
*
* @param int $resultID
* @param int $userID - ID of verifying user
* @param string|null $comment - Optional verification comment
* @return bool
*/
public function verifyResult(int $resultID, int $userID, ?string $comment = null): bool
/**
* Unverify a result (amendment)
*
* @param int $resultID
* @param int $userID - ID of user amending
* @param string $reason - Required reason for amendment
* @return bool
*/
public function unverifyResult(int $resultID, int $userID, string $reason): bool
```
#### Fields to Add to `$allowedFields`:
```php
protected $allowedFields = [
'SiteID',
'OrderID',
'InternalSID',
'SID',
'SampleID',
'TestSiteID',
'TestSiteCode',
'AspCnt',
'Result',
'Unit', // NEW
'SampleType',
'ResultDateTime',
'WorkstationID',
'EquipmentID',
'RefNumID',
'RefTxtID',
'ResultStatus', // NEW: PEN, PRE, FIN, AMD
'Verified', // NEW: boolean
'VerifiedBy', // NEW: user ID
'VerifiedDate', // NEW: datetime
'EnteredBy', // NEW: user ID
'AbnormalFlag', // NEW: H, L, N, A, C
'Comment', // NEW
'CreateDate',
'EndDate',
'ArchiveDate',
'DelDate'
];
```
---
### 1.2 Create ResultEntryService
**File:** `app/Libraries/ResultEntryService.php`
This service handles all business logic for result entry.
```php
<?php
namespace App\Libraries;
use App\Models\PatResultModel;
use App\Models\Patient\PatientModel;
use App\Models\RefRange\RefNumModel;
use App\Models\RefRange\RefTxtModel;
use App\Models\Test\TestDefSiteModel;
class ResultEntryService
{
/**
* Validate result value based on test type
*
* @param string $value
* @param int $testSiteID
* @return array ['valid' => bool, 'error' => string|null]
*/
public function validateResult(string $value, int $testSiteID): array
/**
* Find applicable reference range
*
* @param int $testSiteID
* @param array $patient Demographics: age (months), sex, specimenType
* @return array|null Reference range data
*/
public function getApplicableRange(int $testSiteID, array $patient): ?array
/**
* Calculate abnormal flag based on value and range
*
* @param string|float $value
* @param array $range Reference range data
* @return string H, L, N, A, or C
*/
public function calculateAbnormalFlag($value, array $range): string
/**
* Format reference range for display
*
* @param array $range
* @return string Human-readable range (e.g., "10.0 - 20.0 mg/dL")
*/
public function formatDisplayRange(array $range): string
/**
* Check delta (compare with previous result)
*
* @param int $resultID Current result being edited
* @param string|float $newValue
* @return array ['hasPrevious' => bool, 'previousValue' => string|null, 'deltaPercent' => float|null, 'significant' => bool]
*/
public function checkDelta(int $resultID, $newValue): array
/**
* Process result entry
*
* @param int $resultID
* @param array $data
* @param int $userID User entering the result
* @return array ['success' => bool, 'result' => array|null, 'errors' => array]
*/
public function processEntry(int $resultID, array $data, int $userID): array
/**
* Update calculated tests after dependency changes
*
* @param string $orderID
* @param int $userID
* @return int Number of calculated results updated
*/
public function recalculateDependentResults(string $orderID, int $userID): int
/**
* Get worklist for workstation
*
* @param int $workstationID
* @param array $filters
* @return array
*/
public function getWorklist(int $workstationID, array $filters = []): array
}
```
---
### 1.3 Implement ResultController
**File:** `app/Controllers/ResultController.php`
Replace the placeholder controller with full implementation:
```php
<?php
namespace App\Controllers;
use App\Traits\ResponseTrait;
use App\Libraries\ResultEntryService;
use App\Models\PatResultModel;
use CodeIgniter\Controller;
class ResultController extends Controller
{
use ResponseTrait;
protected $resultModel;
protected $entryService;
public function __construct()
{
$this->resultModel = new PatResultModel();
$this->entryService = new ResultEntryService();
}
/**
* GET /api/results
* List results with filtering
*/
public function index()
/**
* GET /api/results/{id}
* Get single result with details
*/
public function show($id = null)
/**
* PATCH /api/results/{id}
* Update result value
*/
public function update($id = null)
/**
* POST /api/results/batch
* Batch update multiple results
*/
public function batchUpdate()
/**
* POST /api/results/{id}/verify
* Verify a result
*/
public function verify($id = null)
/**
* POST /api/results/{id}/unverify
* Unverify/amend a result
*/
public function unverify($id = null)
/**
* GET /api/results/worklist
* Get pending results for workstation
*/
public function worklist()
/**
* GET /api/results/order/{orderID}
* Get all results for an order
*/
public function byOrder($orderID = null)
}
```
---
## Phase 2: Database Schema (Priority: HIGH)
### 2.1 Migration for New Fields
**File:** `app/Database/Migrations/2025-03-04-000001_AddResultFields.php`
```php
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddResultFields extends Migration
{
public function up()
{
$this->forge->addColumn('patres', [
'ResultStatus' => [
'type' => 'VARCHAR',
'constraint' => 10,
'null' => true,
'comment' => 'PEN=Pending, PRE=Preliminary, FIN=Final, AMD=Amended',
'after' => 'RefTxtID'
],
'Verified' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'after' => 'ResultStatus'
],
'VerifiedBy' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'Verified'
],
'VerifiedDate' => [
'type' => 'DATETIME',
'null' => true,
'after' => 'VerifiedBy'
],
'EnteredBy' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'VerifiedDate'
],
'AbnormalFlag' => [
'type' => 'VARCHAR',
'constraint' => 1,
'null' => true,
'comment' => 'H=High, L=Low, N=Normal, A=Abnormal, C=Critical',
'after' => 'EnteredBy'
],
'Comment' => [
'type' => 'TEXT',
'null' => true,
'after' => 'AbnormalFlag'
],
'Unit' => [
'type' => 'VARCHAR',
'constraint' => 50,
'null' => true,
'after' => 'Result'
]
]);
}
public function down()
{
$this->forge->dropColumn('patres', [
'ResultStatus',
'Verified',
'VerifiedBy',
'VerifiedDate',
'EnteredBy',
'AbnormalFlag',
'Comment',
'Unit'
]);
}
}
```
### 2.2 Create Result History Table (Audit Trail)
**File:** `app/Database/Migrations/2025-03-04-000002_CreatePatResHistory.php`
```php
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreatePatResHistory extends Migration
{
public function up()
{
$this->forge->addField([
'HistoryID' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true
],
'ResultID' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true
],
'OrderID' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true
],
'TestSiteID' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true
],
'OldResult' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true
],
'NewResult' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true
],
'OldStatus' => [
'type' => 'VARCHAR',
'constraint' => 10,
'null' => true
],
'NewStatus' => [
'type' => 'VARCHAR',
'constraint' => 10,
'null' => true
],
'ChangedBy' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true
],
'ChangeReason' => [
'type' => 'TEXT',
'null' => true
],
'CreateDate' => [
'type' => 'DATETIME',
'null' => false
]
]);
$this->forge->addKey('HistoryID', true);
$this->forge->addKey('ResultID');
$this->forge->addKey('CreateDate');
$this->forge->createTable('patreshistory');
}
public function down()
{
$this->forge->dropTable('patreshistory');
}
}
```
---
## Phase 3: API Routes (Priority: HIGH)
### 3.1 Update Routes.php
Add to `app/Config/Routes.php` within the existing `api` group:
```php
// Results
$routes->group('results', function ($routes) {
$routes->get('/', 'ResultController::index');
$routes->get('worklist', 'ResultController::worklist');
$routes->get('order/(:any)', 'ResultController::byOrder/$1');
$routes->get('(:num)', 'ResultController::show/$1');
$routes->patch('(:num)', 'ResultController::update/$1');
$routes->post('batch', 'ResultController::batchUpdate');
$routes->post('(:num)/verify', 'ResultController::verify/$1');
$routes->post('(:num)/unverify', 'ResultController::unverify/$1');
});
```
---
## Phase 4: API Documentation (Priority: MEDIUM)
### 4.1 Create Results Schema
**File:** `public/components/schemas/results.yaml`
```yaml
Result:
type: object
properties:
ResultID:
type: integer
OrderID:
type: integer
InternalSID:
type: integer
nullable: true
TestSiteID:
type: integer
TestSiteCode:
type: string
TestSiteName:
type: string
nullable: true
SID:
type: string
SampleID:
type: string
Result:
type: string
nullable: true
Unit:
type: string
nullable: true
ResultStatus:
type: string
enum: [PEN, PRE, FIN, AMD]
nullable: true
Verified:
type: boolean
default: false
VerifiedBy:
type: integer
nullable: true
VerifiedDate:
type: string
format: date-time
nullable: true
EnteredBy:
type: integer
nullable: true
AbnormalFlag:
type: string
enum: [H, L, N, A, C]
nullable: true
Comment:
type: string
nullable: true
CreateDate:
type: string
format: date-time
ReferenceRange:
type: object
nullable: true
properties:
Low:
type: number
High:
type: number
Display:
type: string
Patient:
type: object
properties:
InternalPID:
type: integer
PatientID:
type: string
NameFirst:
type: string
NameLast:
type: string
Birthdate:
type: string
format: date
Sex:
type: string
ResultEntryRequest:
type: object
required:
- Result
properties:
Result:
type: string
description: The result value
Unit:
type: string
description: Unit override (optional)
AbnormalFlag:
type: string
enum: [H, L, N, A, C]
description: Override auto-calculated flag (optional)
Comment:
type: string
description: Result comment
ResultStatus:
type: string
enum: [PEN, PRE, FIN]
description: Set status (can't set to AMD via update)
ResultBatchRequest:
type: object
required:
- results
properties:
results:
type: array
items:
type: object
properties:
ResultID:
type: integer
Result:
type: string
Unit:
type: string
Comment:
type: string
ResultVerifyRequest:
type: object
properties:
comment:
type: string
description: Optional verification comment
ResultUnverifyRequest:
type: object
required:
- reason
properties:
reason:
type: string
description: Required reason for amendment
ResultWorklistResponse:
type: object
properties:
status:
type: string
message:
type: string
data:
type: array
items:
type: object
properties:
ResultID:
type: integer
PatientName:
type: string
PatientID:
type: string
OrderID:
type: string
TestCode:
type: string
TestName:
type: string
ResultStatus:
type: string
Priority:
type: string
OrderDate:
type: string
format: date-time
```
### 4.2 Create API Paths Documentation
**File:** `public/paths/results.yaml`
Document all endpoints (GET /api/results, GET /api/results/{id}, PATCH /api/results/{id}, POST /api/results/batch, POST /api/results/{id}/verify, POST /api/results/{id}/unverify, GET /api/results/worklist, GET /api/results/order/{orderID}) with:
- Summary and description
- Security (bearerAuth)
- Parameters (path, query)
- RequestBody schemas
- Response schemas
- Error responses
---
## Phase 5: Reference Range Integration (Priority: MEDIUM)
### 5.1 Create RefRangeService
**File:** `app/Libraries/RefRangeService.php`
```php
<?php
namespace App\Libraries;
use App\Models\RefRange\RefNumModel;
use App\Models\RefRange\RefTxtModel;
class RefRangeService
{
/**
* Get applicable reference range for a test and patient
*
* @param int $testSiteID
* @param array $patient Contains: age (in months), sex (M/F), specimenType
* @return array|null
*/
public function getApplicableRange(int $testSiteID, array $patient): ?array
/**
* Evaluate numeric result against range
*
* @param float $value
* @param array $range
* @return string H, L, N, A, C
*/
public function evaluateNumeric(float $value, array $range): string
/**
* Get text reference options
*
* @param int $refTxtID
* @return array
*/
public function getTextOptions(int $refTxtID): array
/**
* Format range for display
*
* @param array $range
* @return string
*/
public function formatDisplay(array $range): string
}
```
---
## Phase 6: Testing (Priority: MEDIUM)
### 6.1 Create Feature Tests
**File:** `tests/feature/Results/ResultEntryTest.php`
Test scenarios:
- Get results list with filters
- Get single result with details
- Update result value
- Validation errors (invalid value)
- Auto-calculate abnormal flag
- Delta check notification
- Batch update
**File:** `tests/feature/Results/ResultVerifyTest.php`
Test scenarios:
- Verify result successfully
- Unverify with reason
- Attempt to modify verified result
- Permission checks
- Amendment workflow
**File:** `tests/feature/Results/ResultWorklistTest.php`
Test scenarios:
- Get worklist by workstation
- Filter by priority
- Sort by order date
- Pagination
### 6.2 Create Unit Tests
**File:** `tests/unit/Libraries/ResultEntryServiceTest.php`
Test scenarios:
- Result validation
- Reference range matching
- Abnormal flag calculation
- Delta calculation
- Calculated test formulas
**File:** `tests/unit/Libraries/RefRangeServiceTest.php`
Test scenarios:
- Age-based range selection
- Sex-based range selection
- Specimen type matching
- Boundary value evaluation
---
## Data Flow Diagram
```
Order Creation
|
v
Empty patres records created
|
v
GET /api/results/worklist <-- Technician sees pending results
|
v
GET /api/results/{id} <-- Load result with patient info
|
v
PATCH /api/results/{id} <-- Enter result value
| |
| v
| ResultEntryService.validateResult()
| |
| v
| RefRangeService.getApplicableRange()
| |
| v
| Auto-calculate AbnormalFlag
| |
v v
Result updated in patres
|
v
POST /api/results/{id}/verify <-- Senior tech/pathologist
|
v
ResultStatus = FIN, Verified = true
```
---
## Status Definitions
| Status | Code | Description |
|--------|------|-------------|
| Pending | PEN | Order created, awaiting result entry |
| Preliminary | PRE | Result entered but not verified |
| Final | FIN | Result verified by authorized user |
| Amended | AMD | Previously final result modified |
## Abnormal Flag Definitions
| Flag | Meaning | Action Required |
|------|---------|-----------------|
| N | Normal | None |
| H | High | Review |
| L | Low | Review |
| A | Abnormal (text) | Review |
| C | Critical | Immediate notification |
---
## Questions for Implementation
Before starting implementation, clarify:
1. **Who can verify results?**
- Option A: Any authenticated user
- Option B: Users with specific role (senior tech, pathologist)
- Option C: Configure per test/discipline
2. **Can calculated tests be manually edited?**
- Option A: No, always auto-computed
- Option B: Yes, allow override with reason
- Option C: Configurable per test
3. **Audit trail requirements:**
- Option A: Full history (every change)
- Option B: Only amendments (verified→unverify→verify)
- Option C: No audit trail needed
4. **Critical results handling:**
- Option A: Flag only
- Option B: Flag + notification system
- Option C: Flag + mandatory acknowledgment
5. **Batch entry priority:**
- Must-have or nice-to-have?
- Support for templates/predefined sets?
6. **Delta check sensitivity:**
- Fixed percentage threshold (e.g., 20%)?
- Test-specific thresholds?
- Configurable?
---
## Implementation Checklist
- [ ] Phase 1.1: Extend PatResultModel with CRUD methods
- [ ] Phase 1.2: Create ResultEntryService with business logic
- [ ] Phase 1.3: Implement ResultController methods
- [ ] Phase 2.1: Create migration for new patres fields
- [ ] Phase 2.2: Create patreshistory table
- [ ] Phase 3.1: Add routes to Routes.php
- [ ] Phase 4.1: Create results.yaml schema
- [ ] Phase 4.2: Create results.yaml paths documentation
- [ ] Phase 4.3: Run `node public/bundle-api-docs.js`
- [ ] Phase 5.1: Create RefRangeService
- [ ] Phase 6.1: Create feature tests
- [ ] Phase 6.2: Create unit tests
- [ ] Run full test suite: `./vendor/bin/phpunit`
---
## Estimated Timeline
| Phase | Duration | Priority |
|-------|----------|----------|
| Phase 1: Core Management | 2-3 days | HIGH |
| Phase 2: Database Schema | 0.5 day | HIGH |
| Phase 3: API Routes | 0.5 day | HIGH |
| Phase 4: Documentation | 1 day | MEDIUM |
| Phase 5: Reference Ranges | 1-2 days | MEDIUM |
| Phase 6: Testing | 2-3 days | MEDIUM |
| **Total** | **7-10 days** | |
---
## Notes
- All dates stored in UTC, convert to local time for display
- Use transactions for all multi-table operations
- Follow existing code style (camelCase methods, snake_case properties)
- Update AGENTS.md if adding new commands or patterns
- Consider performance: worklist queries should be fast (< 500ms)
*Last Updated: 2025-03-04*