# 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 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 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 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 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