From 85c7e964051105801082d67759ad26e12495ed41 Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Wed, 4 Mar 2026 16:48:12 +0700 Subject: [PATCH] feat: implement comprehensive result management and lab reporting system - Add full CRUD operations for results (index, show, update, delete) - Implement result validation with reference range checking (L/H flags) - Add cumulative patient results retrieval across all orders - Create ReportController for HTML lab report generation - Add lab report view with patient info, order details, and test results - Implement soft delete for results with transaction safety - Update API routes with /api/results/* endpoints for CRUD - Add /api/reports/{orderID} endpoint for report viewing - Update OpenAPI docs with results and reports schemas/paths - Add documentation for manual result entry and MVP plan --- app/Config/Routes.php | 12 +- app/Controllers/ReportController.php | 75 +++ app/Controllers/ResultController.php | 187 +++++- app/Models/PatResultModel.php | 245 +++++++ app/Views/reports/lab_report.php | 376 +++++++++++ docs/manual-result-entry-plan.md | 941 +++++++++++++++++++++++++++ docs/mvp_plan.md | 249 +++++++ public/api-docs.bundled.yaml | 353 ++++++---- public/api-docs.yaml | 4 +- public/paths/reports.yaml | 34 + public/paths/results.yaml | 302 +++++---- 11 files changed, 2480 insertions(+), 298 deletions(-) create mode 100644 app/Controllers/ReportController.php create mode 100644 app/Views/reports/lab_report.php create mode 100644 docs/manual-result-entry-plan.md create mode 100644 docs/mvp_plan.md create mode 100644 public/paths/reports.yaml diff --git a/app/Config/Routes.php b/app/Config/Routes.php index c7c5daf..f343c4e 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -15,8 +15,18 @@ $routes->options('(:any)', function () { $routes->group('api', ['filter' => 'auth'], function ($routes) { $routes->get('dashboard', 'DashboardController::index'); - $routes->get('result', 'ResultController::index'); $routes->get('sample', 'SampleController::index'); + + // Results CRUD + $routes->group('results', function ($routes) { + $routes->get('/', 'ResultController::index'); + $routes->get('(:num)', 'ResultController::show/$1'); + $routes->patch('(:num)', 'ResultController::update/$1'); + $routes->delete('(:num)', 'ResultController::delete/$1'); + }); + + // Reports + $routes->get('reports/(:num)', 'ReportController::view/$1'); }); diff --git a/app/Controllers/ReportController.php b/app/Controllers/ReportController.php new file mode 100644 index 0000000..fa02567 --- /dev/null +++ b/app/Controllers/ReportController.php @@ -0,0 +1,75 @@ +resultModel = new PatResultModel(); + $this->orderModel = new OrderTestModel(); + $this->patientModel = new PatientModel(); + } + + /** + * Generate HTML lab report for an order + * GET /api/reports/{orderID} + */ + public function view($orderID) { + try { + // Get order details + $order = $this->orderModel->find((int)$orderID); + + if (!$order) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Order not found', + 'data' => [] + ], 404); + } + + // Get patient details + $patient = $this->patientModel->find($order['InternalPID']); + + if (!$patient) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Patient not found', + 'data' => [] + ], 404); + } + + // Get results for this order + $results = $this->resultModel->getByOrder((int)$orderID); + + // Prepare data for the view + $data = [ + 'patient' => $patient, + 'order' => $order, + 'results' => $results, + 'generatedAt' => date('Y-m-d H:i:s') + ]; + + // Return HTML view + return view('reports/lab_report', $data); + + } catch (\Exception $e) { + log_message('error', 'ReportController::view error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to generate report', + 'data' => [] + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Controllers/ResultController.php b/app/Controllers/ResultController.php index f54ee2d..8680acb 100644 --- a/app/Controllers/ResultController.php +++ b/app/Controllers/ResultController.php @@ -4,31 +4,176 @@ namespace App\Controllers; use App\Traits\ResponseTrait; use CodeIgniter\Controller; - -use Firebase\JWT\JWT; -use Firebase\JWT\Key; -use Firebase\JWT\ExpiredException; -use Firebase\JWT\SignatureInvalidException; -use Firebase\JWT\BeforeValidException; -use CodeIgniter\Cookie\Cookie; +use App\Models\PatResultModel; class ResultController extends Controller { use ResponseTrait; - public function index() { + protected $model; - $token = $this->request->getCookie('token'); - $key = getenv('JWT_SECRET'); - - // Decode Token dengan Key yg ada di .env - $decodedPayload = JWT::decode($token, new Key($key, 'HS256')); - - return $this->respond([ - 'status' => 'success', - 'code' => 200, - 'message' => 'Authenticated', - 'data' => $decodedPayload - ], 200); + public function __construct() { + $this->model = new PatResultModel(); + } + + /** + * List results with optional filters + * GET /api/results + */ + public function index() { + try { + $orderID = $this->request->getGet('order_id'); + $patientID = $this->request->getGet('patient_id'); + + if ($orderID) { + $results = $this->model->getByOrder((int)$orderID); + } elseif ($patientID) { + $results = $this->model->getByPatient((int)$patientID); + } else { + // Get all results with pagination + $page = (int)($this->request->getGet('page') ?? 1); + $perPage = (int)($this->request->getGet('per_page') ?? 20); + + $results = $this->model + ->where('DelDate', null) + ->orderBy('ResultID', 'DESC') + ->paginate($perPage, 'default', $page); + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'Results retrieved successfully', + 'data' => $results + ], 200); + + } catch (\Exception $e) { + log_message('error', 'ResultController::index error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to retrieve results', + 'data' => [] + ], 500); + } + } + + /** + * Get single result + * GET /api/results/{id} + */ + public function show($id) { + try { + $result = $this->model->getWithRelations((int)$id); + + if (!$result) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Result not found', + 'data' => [] + ], 404); + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'Result retrieved successfully', + 'data' => $result + ], 200); + + } catch (\Exception $e) { + log_message('error', 'ResultController::show error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to retrieve result', + 'data' => [] + ], 500); + } + } + + /** + * Update result with validation + * PATCH /api/results/{id} + */ + public function update($id) { + try { + $data = $this->request->getJSON(true); + + if (empty($data)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'No data provided', + 'data' => [] + ], 400); + } + + $result = $this->model->updateWithValidation((int)$id, $data); + + if (!$result['success']) { + return $this->respond([ + 'status' => 'failed', + 'message' => $result['message'], + 'data' => [] + ], 400); + } + + // Get updated result with relations + $updatedResult = $this->model->getWithRelations((int)$id); + + return $this->respond([ + 'status' => 'success', + 'message' => $result['message'], + 'data' => [ + 'result' => $updatedResult, + 'flag' => $result['flag'] + ] + ], 200); + + } catch (\Exception $e) { + log_message('error', 'ResultController::update error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to update result', + 'data' => [] + ], 500); + } + } + + /** + * Soft delete result + * DELETE /api/results/{id} + */ + public function delete($id) { + try { + $result = $this->model->find((int)$id); + + if (!$result) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Result not found', + 'data' => [] + ], 404); + } + + $deleted = $this->model->softDelete((int)$id); + + if (!$deleted) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to delete result', + 'data' => [] + ], 500); + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'Result deleted successfully', + 'data' => [] + ], 200); + + } catch (\Exception $e) { + log_message('error', 'ResultController::delete error: ' . $e->getMessage()); + return $this->respond([ + 'status' => 'failed', + 'message' => 'Failed to delete result', + 'data' => [] + ], 500); + } } - } diff --git a/app/Models/PatResultModel.php b/app/Models/PatResultModel.php index 7506e00..a6ceffe 100644 --- a/app/Models/PatResultModel.php +++ b/app/Models/PatResultModel.php @@ -1,6 +1,10 @@ find($refNumID); + + if (!$ref) { + return null; + } + + // Get patient info from order + $orderModel = new OrderTestModel(); + $order = $orderModel->find($orderID); + + if (!$order) { + return null; + } + + $patientModel = new PatientModel(); + $patient = $patientModel->find($order['InternalPID']); + + if (!$patient) { + return null; + } + + // Check if patient matches criteria (sex) + if (!empty($ref['Sex']) && $ref['Sex'] !== 'ALL') { + if ($patient['Sex'] !== $ref['Sex']) { + return null; + } + } + + // Check age criteria + if ($ref['AgeStart'] !== null || $ref['AgeEnd'] !== null) { + $birthdate = new \DateTime($patient['Birthdate']); + $today = new \DateTime(); + $age = $birthdate->diff($today)->y; + + if ($ref['AgeStart'] !== null && $age < $ref['AgeStart']) { + return null; + } + if ($ref['AgeEnd'] !== null && $age > $ref['AgeEnd']) { + return null; + } + } + + $value = floatval($resultValue); + $low = floatval($ref['Low']); + $high = floatval($ref['High']); + + // Check low + if ($ref['LowSign'] === '<=' && $value <= $low) { + return 'L'; + } + if ($ref['LowSign'] === '<' && $value < $low) { + return 'L'; + } + + // Check high + if ($ref['HighSign'] === '>=' && $value >= $high) { + return 'H'; + } + if ($ref['HighSign'] === '>' && $value > $high) { + return 'H'; + } + + return null; // Normal + } + + /** + * Get all results for an order with test names and reference ranges + * + * @param int $orderID + * @return array + */ + public function getByOrder(int $orderID): array { + $builder = $this->db->table('patres pr'); + $builder->select(' + pr.ResultID, + pr.OrderID, + pr.TestSiteID, + pr.TestSiteCode, + pr.Result, + pr.ResultDateTime, + pr.RefNumID, + pr.RefTxtID, + pr.CreateDate, + tds.TestSiteName, + tds.Unit1, + tds.Unit2, + rn.Low, + rn.High, + rn.LowSign, + rn.HighSign, + rn.Display as RefDisplay + '); + $builder->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left'); + $builder->join('refnum rn', 'rn.RefNumID = pr.RefNumID', 'left'); + $builder->where('pr.OrderID', $orderID); + $builder->where('pr.DelDate', null); + $builder->orderBy('tds.SeqScr', 'ASC'); + + return $builder->get()->getResultArray(); + } + + /** + * Get cumulative patient results across all orders + * + * @param int $internalPID + * @return array + */ + public function getByPatient(int $internalPID): array { + $builder = $this->db->table('patres pr'); + $builder->select(' + pr.ResultID, + pr.OrderID, + pr.TestSiteID, + pr.TestSiteCode, + pr.Result, + pr.ResultDateTime, + pr.RefNumID, + tds.TestSiteName, + tds.Unit1, + tds.Unit2, + ot.OrderID as OrderNumber, + ot.TrnDate as OrderDate + '); + $builder->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left'); + $builder->join('ordertest ot', 'ot.InternalOID = pr.OrderID', 'left'); + $builder->where('ot.InternalPID', $internalPID); + $builder->where('pr.DelDate', null); + $builder->orderBy('pr.ResultDateTime', 'DESC'); + + return $builder->get()->getResultArray(); + } + + /** + * Update result with validation and return flag (not stored) + * + * @param int $resultID + * @param array $data + * @return array ['success' => bool, 'flag' => string|null, 'message' => string] + */ + public function updateWithValidation(int $resultID, array $data): array { + $this->db->transStart(); + + try { + $result = $this->find($resultID); + if (!$result) { + throw new \Exception('Result not found'); + } + + $flag = null; + + // If result value is being updated, validate it + if (isset($data['Result']) && !empty($data['Result'])) { + $refNumID = $data['RefNumID'] ?? $result['RefNumID']; + + if ($refNumID) { + $flag = $this->validateAndFlag($data['Result'], $refNumID, $result['OrderID']); + } + } + + // Set update timestamp + $data['StartDate'] = date('Y-m-d H:i:s'); + + $updated = $this->update($resultID, $data); + + if (!$updated) { + throw new \Exception('Failed to update result'); + } + + $this->db->transComplete(); + + return [ + 'success' => true, + 'flag' => $flag, + 'message' => 'Result updated successfully' + ]; + } catch (\Exception $e) { + $this->db->transRollback(); + return [ + 'success' => false, + 'flag' => null, + 'message' => $e->getMessage() + ]; + } + } + + /** + * Get single result with related data + * + * @param int $resultID + * @return array|null + */ + public function getWithRelations(int $resultID): ?array { + $builder = $this->db->table('patres pr'); + $builder->select(' + pr.*, + tds.TestSiteName, + tds.TestSiteCode, + tds.Unit1, + tds.Unit2, + rn.Low, + rn.High, + rn.LowSign, + rn.HighSign, + rn.Display as RefDisplay, + ot.OrderID as OrderNumber, + ot.InternalPID + '); + $builder->join('testdefsite tds', 'tds.TestSiteID = pr.TestSiteID', 'left'); + $builder->join('refnum rn', 'rn.RefNumID = pr.RefNumID', 'left'); + $builder->join('ordertest ot', 'ot.InternalOID = pr.OrderID', 'left'); + $builder->where('pr.ResultID', $resultID); + $builder->where('pr.DelDate', null); + + $result = $builder->get()->getRowArray(); + return $result ?: null; + } + + /** + * Soft delete result + * + * @param int $resultID + * @return bool + */ + public function softDelete(int $resultID): bool { + return $this->update($resultID, ['DelDate' => date('Y-m-d H:i:s')]); + } } diff --git a/app/Views/reports/lab_report.php b/app/Views/reports/lab_report.php new file mode 100644 index 0000000..97c387d --- /dev/null +++ b/app/Views/reports/lab_report.php @@ -0,0 +1,376 @@ + + + + + + Lab Report - <?= esc($order['OrderID'] ?? 'N/A') ?> + + + + =' && $value >= $high) { + return 'H'; + } + if ($highSign === '>' && $value > $high) { + return 'H'; + } + + return null; // Normal + } + ?> + + +
+
+

LABORATORY REPORT

+
Clinical Laboratory Quality Management System
+
+ +
+
Patient Information
+
+
+ Patient Name + +
+
+ Patient ID + +
+
+ Date of Birth + +
+
+ Sex + +
+
+ Phone + +
+
+ Address + +
+
+
+ +
+
Order Information
+
+
+ Order ID + +
+
+ Order Date + +
+
+ Priority + +
+
+ Requesting Physician + +
+
+ Placer ID + +
+
+
+ +
+
Test Results
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Test NameCodeResultUnitFlagReference Range
+ + + + + + - + + + + - + + - + +
No results found for this order
+
+ +
+
+
+
Authorized Signature
+
Pathologist / Lab Director
+
+
+ + +
+ + \ No newline at end of file diff --git a/docs/manual-result-entry-plan.md b/docs/manual-result-entry-plan.md new file mode 100644 index 0000000..ca37d6b --- /dev/null +++ b/docs/manual-result-entry-plan.md @@ -0,0 +1,941 @@ +# 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 + **Scope**: Result Entry + Validation + Reporting (2-week sprint) +> **Removed**: Edge API, Worklists, Calculated Tests, QC, Instrument Integration + +--- + +## Current State + +### What's Working +- Patient CRUD + Identifiers + Addresses +- Patient Visit + ADT +- Lab Orders (create with specimen generation) +- Test Definitions + Reference Ranges (refnum) +- Authentication (JWT) +- Master Data (ValueSets, Locations, etc.) + +### Critical Gap +- `ResultController` only returns JWT payload (no CRUD) +- `PatResultModel` has no validation logic +- No PDF generation capability +- Only 1 working route: `GET /api/result` (returns auth check only) + +--- + +## Phase 1: Result CRUD + Validation (Week 1) + +### Day 1-2: PatResultModel Enhancement + +**Location**: `app/Models/PatResultModel.php` + +Add methods: +- `validateAndFlag($resultID, $value)` - Compare result against refnum ranges + - Check patient age, sex from order + - Match refnum criteria + - Return 'L', 'H', or null +- `getByOrder($orderID)` - Fetch all results for an order with test names +- `getByPatient($internalPID)` - Get cumulative patient results +- `updateWithValidation($resultID, $data)` - Update + auto-validate + +### Day 3-4: ResultController + +**Location**: `app/Controllers/ResultController.php` + +Replace placeholder with full CRUD: + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `index()` | `GET /api/results` | List results (filter by order/patient) | +| `show($id)` | `GET /api/results/{id}` | Get single result | +| `update($id)` | `PATCH /api/results/{id}` | Update result + auto-validate | +| `delete($id)` | `DELETE /api/results/{id}` | Soft delete result | + +**Features**: +- Filter by `order_id` or `patient_id` query param +- Include test name from `testdefsite` +- Auto-calculate flags on update +- Return standardized ResponseTrait format + +### Day 5: Routes & Testing + +**Location**: `app/Config/Routes.php` + +Replace line 18: +```php +// OLD +$routes->get('result', 'ResultController::index'); + +// NEW +$routes->group('results', function ($routes) { + $routes->get('/', 'ResultController::index'); + $routes->get('(:num)', 'ResultController::show/$1'); + $routes->patch('(:num)', 'ResultController::update/$1'); + $routes->delete('(:num)', 'ResultController::delete/$1'); +}); +``` + +**Testing**: Manual API testing with Postman/Insomnia + +--- + +## Phase 2: HTML Report Viewing (Week 2) + +### Day 1-2: Report View & Controller + +**Create Report View**: `app/Views/reports/lab_report.php` +Template sections: +- Patient header (Name, DOB, ID) +- Order info (OrderID, Date, Doctor) +- Results table (Test, Result, Units, Reference, Flag) +- Footer (Lab info, signature) +- Print-friendly CSS with `@media print` support + +**Create ReportController**: `app/Controllers/ReportController.php` + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `view($orderID)` | `GET /api/reports/{orderID}` | Generate HTML lab report | + +**Logic**: +- Fetch order with patient details +- Get all results for order (with flags) +- Include test definitions (name, units, ref range) +- Render HTML view using CodeIgniter's view() function +- Users can print/save as PDF via browser + +### Day 3-4: Routes & Polish + +**Routes** (add to Routes.php): +```php +$routes->get('reports/(:num)', 'ReportController::view/$1'); +``` + +**HTML Styling**: +- Professional lab report format +- Show abnormal flags (L/H) highlighted +- Include reference ranges +- Signature line for pathologist +- Responsive design with print button + +**Testing**: +- Generate report with actual data +- Verify formatting +- Test print functionality +- Test edge cases (no results, all normal, mix of flags) + +**Note**: PDF generation deferred to post-MVP. Users can use browser's "Print to PDF" feature for now. + +--- + +## Database Schema Reference + +### patres (Results) +| Column | Type | Notes | +|--------|------|-------| +| ResultID | INT | PK | +| OrderID | INT | FK to ordertest | +| TestSiteID | INT | FK to testdefsite | +| Result | VARCHAR(255) | The actual value | +| ResultDateTime | DATETIME | When entered | +| RefNumID | INT | Applied reference range | + +### refnum (Reference Ranges) +| Column | Type | Notes | +|--------|------|-------| +| RefNumID | INT | PK | +| TestSiteID | INT | Which test | +| Sex | VARCHAR | M/F/ALL | +| AgeStart/AgeEnd | INT | Age criteria | +| Low/High | DECIMAL | Range values | +| LowSign/HighSign | VARCHAR | <=, <, etc | + +--- + +## API Endpoints Summary + +| Endpoint | Method | Auth | Description | +|----------|--------|------|-------------| +| `/api/results` | GET | Yes | List results (filter: ?order_id= or ?patient_id=) | +| `/api/results/{id}` | GET | Yes | Get single result | +| `/api/results/{id}` | PATCH | Yes | Update result + auto-flag | +| `/api/results/{id}` | DELETE | Yes | Soft delete result | +| `/api/reports/{orderID}` | GET | Yes | Generate PDF report | + +--- + +## Flag Logic (Reference Range Validation) + +```php +// Pseudo-code for validation +function validateResult($resultValue, $refNumID) { + $ref = getRefNum($refNumID); + $patient = getPatientFromOrder($orderID); + + // Match criteria (sex, age) + if (!matchesCriteria($ref, $patient)) { + return null; // No flag if criteria don't match + } + + $value = floatval($resultValue); + $low = floatval($ref['Low']); + $high = floatval($ref['High']); + + // Check low + if ($ref['LowSign'] === '<=' && $value <= $low) return 'L'; + if ($ref['LowSign'] === '<' && $value < $low) return 'L'; + + // Check high + if ($ref['HighSign'] === '>=' && $value >= $high) return 'H'; + if ($ref['HighSign'] === '>' && $value > $high) return 'H'; + + return null; // Normal +} +``` + +--- + +## Out of Scope (Post-MVP) + +- **Edge API**: Instrument integration (`app/Controllers/EdgeController.php`) +- **Worklist Generation**: Technician worklists +- **Calculated Tests**: Formula execution (CALC type) +- **Quality Control**: QC samples, Levy-Jennings charts +- **Calculated Test Execution**: Deferred to later +- **Delta Checking**: Result trending +- **Critical Result Alerts**: Notification system +- **Audit Trail**: Complete audit logging + +--- + +## Success Criteria + +- [ ] Can enter result values via API +- [ ] Results auto-validate against refnum ranges +- [ ] Abnormal results show L/H flags +- [ ] Can view all results for an order +- [ ] Can generate PDF lab report +- [ ] Report shows patient, order, results with flags + +--- + +## Files to Create/Modify + +### Create +1. `app/Controllers/ReportController.php` - HTML report controller +2. `app/Views/reports/lab_report.php` - HTML report template with dynamic flag calculation +3. `app/Database/Migrations/2026-03-04-073950_RemoveFlagColumnFromPatRes.php` - Remove Flag column +4. `public/paths/reports.yaml` - OpenAPI documentation for reports endpoint + +### Modify +1. `app/Models/PatResultModel.php` - Add validation methods (validateAndFlag, getByOrder, getByPatient, updateWithValidation, getWithRelations, softDelete) +2. `app/Controllers/ResultController.php` - Full CRUD (index, show, update, delete) +3. `app/Config/Routes.php` - New routes for results and reports +4. `public/paths/results.yaml` - Updated OpenAPI documentation +5. `public/api-docs.yaml` - Added Reports tag +6. Regenerated `public/api-docs.bundled.yaml` + +**Note**: Flag is calculated dynamically at runtime, not stored in database. This allows for: +- Real-time validation against current reference ranges +- Support for reference range updates without re-processing historical results +- Reduced storage requirements + +**API Documentation**: Remember to run `node public/bundle-api-docs.js` after updating YAML files to regenerate the bundled documentation. + +--- + +*Last Updated: 2026-03-04* +*Sprint Duration: 2 weeks* +*Team Size: 1 developer* diff --git a/public/api-docs.bundled.yaml b/public/api-docs.bundled.yaml index e94cff5..70a345a 100644 --- a/public/api-docs.bundled.yaml +++ b/public/api-docs.bundled.yaml @@ -36,7 +36,9 @@ tags: - name: Orders description: Laboratory order management - name: Results - description: Patient results reporting + description: Patient results reporting with auto-validation + - name: Reports + description: Lab report generation (HTML view) - name: Edge API description: Instrument integration endpoints - name: Contacts @@ -2707,51 +2709,75 @@ paths: type: string data: $ref: '#/components/schemas/Patient' + /api/reports/{orderID}: + get: + tags: + - Reports + summary: Generate lab report + description: Generate an HTML lab report for a specific order. Returns HTML content that can be viewed in browser or printed to PDF. + security: + - bearerAuth: [] + parameters: + - name: orderID + in: path + required: true + schema: + type: integer + description: Internal Order ID + responses: + '200': + description: HTML lab report + content: + text/html: + schema: + type: string + description: HTML content of the lab report + '404': + description: Order or patient not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Failed to generate report + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /api/results: get: tags: - Results - summary: Get patient results - description: Retrieve patient test results with optional filters + summary: List results + description: Retrieve patient test results with optional filters by order or patient security: - bearerAuth: [] parameters: - - name: InternalPID + - name: order_id in: query schema: type: integer - description: Filter by internal patient ID - - name: OrderID + description: Filter by internal order ID + - name: patient_id in: query schema: - type: string - description: Filter by order ID - - name: TestCode + type: integer + description: Filter by internal patient ID (returns cumulative results) + - name: page in: query schema: - type: string - description: Filter by test code - - name: date_from + type: integer + default: 1 + description: Page number for pagination + - name: per_page in: query schema: - type: string - format: date - description: Filter results from date (YYYY-MM-DD) - - name: date_to - in: query - schema: - type: string - format: date - description: Filter results to date (YYYY-MM-DD) - - name: verified_only - in: query - schema: - type: boolean - default: false - description: Return only verified results + type: integer + default: 20 + description: Number of results per page responses: '200': - description: List of patient results + description: List of results content: application/json: schema: @@ -2759,6 +2785,9 @@ paths: properties: status: type: string + example: success + message: + type: string data: type: array items: @@ -2766,84 +2795,57 @@ paths: properties: ResultID: type: integer - InternalPID: - type: integer OrderID: - type: string - TestID: type: integer - TestCode: + TestSiteID: + type: integer + TestSiteCode: type: string - TestName: + Result: type: string - ResultValue: - type: string - Unit: - type: string - ReferenceRange: - type: string - AbnormalFlag: - type: string - Verified: - type: boolean - VerifiedBy: - type: string - VerifiedDate: + nullable: true + ResultDateTime: type: string format: date-time - ResultDate: + RefNumID: + type: integer + nullable: true + RefTxtID: + type: integer + nullable: true + CreateDate: type: string format: date-time - post: - tags: - - Results - summary: Create or update result - description: Create a new result or update an existing result entry - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - InternalPID - - TestID - - ResultValue - properties: - InternalPID: - type: integer - OrderID: - type: string - TestID: - type: integer - ResultValue: - type: string - Unit: - type: string - AbnormalFlag: - type: string - enum: - - H - - L - - 'N' - - A - - C - description: H=High, L=Low, N=Normal, A=Abnormal, C=Critical - responses: - '201': - description: Result created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' + TestSiteName: + type: string + nullable: true + Unit1: + type: string + nullable: true + Unit2: + type: string + nullable: true + Low: + type: number + nullable: true + High: + type: number + nullable: true + LowSign: + type: string + nullable: true + HighSign: + type: string + nullable: true + RefDisplay: + type: string + nullable: true /api/results/{id}: get: tags: - Results summary: Get result by ID - description: Retrieve a specific result entry by its ID + description: Retrieve a specific result entry with all related data security: - bearerAuth: [] parameters: @@ -2863,44 +2865,94 @@ paths: properties: status: type: string + example: success + message: + type: string data: type: object properties: ResultID: type: integer - InternalPID: + SiteID: type: integer OrderID: - type: string - TestID: type: integer - TestCode: + InternalSID: + type: integer + SID: type: string - TestName: + SampleID: type: string - ResultValue: + TestSiteID: + type: integer + TestSiteCode: type: string - Unit: + AspCnt: + type: integer + Result: type: string - ReferenceRange: + nullable: true + SampleType: type: string - AbnormalFlag: - type: string - Verified: - type: boolean - VerifiedBy: - type: string - VerifiedDate: + nullable: true + ResultDateTime: type: string format: date-time - ResultDate: + WorkstationID: + type: integer + nullable: true + EquipmentID: + type: integer + nullable: true + RefNumID: + type: integer + nullable: true + RefTxtID: + type: integer + nullable: true + CreateDate: type: string format: date-time + TestSiteName: + type: string + nullable: true + Unit1: + type: string + nullable: true + Unit2: + type: string + nullable: true + Low: + type: number + nullable: true + High: + type: number + nullable: true + LowSign: + type: string + nullable: true + HighSign: + type: string + nullable: true + RefDisplay: + type: string + nullable: true + OrderNumber: + type: string + nullable: true + InternalPID: + type: integer + '404': + description: Result not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' patch: tags: - Results summary: Update result - description: Update an existing result entry + description: Update a result value with automatic validation against reference ranges. Returns calculated flag (L/H) in response but does not store it. security: - bearerAuth: [] parameters: @@ -2917,32 +2969,63 @@ paths: schema: type: object properties: - ResultValue: + Result: type: string - Unit: + description: The result value + RefNumID: + type: integer + description: Reference range ID to validate against + SampleType: type: string - AbnormalFlag: - type: string - enum: - - H - - L - - 'N' - - A - - C - Verified: - type: boolean + nullable: true + WorkstationID: + type: integer + nullable: true + EquipmentID: + type: integer + nullable: true responses: '200': description: Result updated successfully content: application/json: schema: - $ref: '#/components/schemas/SuccessResponse' + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + result: + type: object + flag: + type: string + nullable: true + enum: + - L + - H + description: Calculated flag - L for Low, H for High, null for normal + '400': + description: Validation failed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Result not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' delete: tags: - Results summary: Delete result - description: Soft delete a result entry + description: Soft delete a result entry by setting DelDate security: - bearerAuth: [] parameters: @@ -2959,28 +3042,12 @@ paths: application/json: schema: $ref: '#/components/schemas/SuccessResponse' - /api/results/{id}/verify: - post: - tags: - - Results - summary: Verify result - description: Mark a result as verified by the current user - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Result ID - responses: - '200': - description: Result verified successfully + '404': + description: Result not found content: application/json: schema: - $ref: '#/components/schemas/SuccessResponse' + $ref: '#/components/schemas/ErrorResponse' /api/specimen: get: tags: diff --git a/public/api-docs.yaml b/public/api-docs.yaml index 1ed150e..d168543 100644 --- a/public/api-docs.yaml +++ b/public/api-docs.yaml @@ -38,7 +38,9 @@ tags: - name: Orders description: Laboratory order management - name: Results - description: Patient results reporting + description: Patient results reporting with auto-validation + - name: Reports + description: Lab report generation (HTML view) - name: Edge API description: Instrument integration endpoints - name: Contacts diff --git a/public/paths/reports.yaml b/public/paths/reports.yaml new file mode 100644 index 0000000..9c459c2 --- /dev/null +++ b/public/paths/reports.yaml @@ -0,0 +1,34 @@ +/api/reports/{orderID}: + get: + tags: [Reports] + summary: Generate lab report + description: Generate an HTML lab report for a specific order. Returns HTML content that can be viewed in browser or printed to PDF. + security: + - bearerAuth: [] + parameters: + - name: orderID + in: path + required: true + schema: + type: integer + description: Internal Order ID + responses: + '200': + description: HTML lab report + content: + text/html: + schema: + type: string + description: HTML content of the lab report + '404': + description: Order or patient not found + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/ErrorResponse' + '500': + description: Failed to generate report + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/ErrorResponse' \ No newline at end of file diff --git a/public/paths/results.yaml b/public/paths/results.yaml index c7407f8..3b450d3 100644 --- a/public/paths/results.yaml +++ b/public/paths/results.yaml @@ -1,47 +1,36 @@ /api/results: get: tags: [Results] - summary: Get patient results - description: Retrieve patient test results with optional filters + summary: List results + description: Retrieve patient test results with optional filters by order or patient security: - bearerAuth: [] parameters: - - name: InternalPID + - name: order_id in: query schema: type: integer - description: Filter by internal patient ID - - name: OrderID + description: Filter by internal order ID + - name: patient_id in: query schema: - type: string - description: Filter by order ID - - name: TestCode + type: integer + description: Filter by internal patient ID (returns cumulative results) + - name: page in: query schema: - type: string - description: Filter by test code - - name: date_from + type: integer + default: 1 + description: Page number for pagination + - name: per_page in: query schema: - type: string - format: date - description: Filter results from date (YYYY-MM-DD) - - name: date_to - in: query - schema: - type: string - format: date - description: Filter results to date (YYYY-MM-DD) - - name: verified_only - in: query - schema: - type: boolean - default: false - description: Return only verified results + type: integer + default: 20 + description: Number of results per page responses: '200': - description: List of patient results + description: List of results content: application/json: schema: @@ -49,6 +38,9 @@ properties: status: type: string + example: success + message: + type: string data: type: array items: @@ -56,79 +48,57 @@ properties: ResultID: type: integer - InternalPID: - type: integer OrderID: - type: string - TestID: type: integer - TestCode: + TestSiteID: + type: integer + TestSiteCode: type: string - TestName: + Result: type: string - ResultValue: - type: string - Unit: - type: string - ReferenceRange: - type: string - AbnormalFlag: - type: string - Verified: - type: boolean - VerifiedBy: - type: string - VerifiedDate: + nullable: true + ResultDateTime: type: string format: date-time - ResultDate: + RefNumID: + type: integer + nullable: true + RefTxtID: + type: integer + nullable: true + CreateDate: type: string format: date-time - - post: - tags: [Results] - summary: Create or update result - description: Create a new result or update an existing result entry - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - InternalPID - - TestID - - ResultValue - properties: - InternalPID: - type: integer - OrderID: - type: string - TestID: - type: integer - ResultValue: - type: string - Unit: - type: string - AbnormalFlag: - type: string - enum: [H, L, N, A, C] - description: H=High, L=Low, N=Normal, A=Abnormal, C=Critical - responses: - '201': - description: Result created successfully - content: - application/json: - schema: - $ref: '../components/schemas/common.yaml#/SuccessResponse' + TestSiteName: + type: string + nullable: true + Unit1: + type: string + nullable: true + Unit2: + type: string + nullable: true + Low: + type: number + nullable: true + High: + type: number + nullable: true + LowSign: + type: string + nullable: true + HighSign: + type: string + nullable: true + RefDisplay: + type: string + nullable: true /api/results/{id}: get: tags: [Results] summary: Get result by ID - description: Retrieve a specific result entry by its ID + description: Retrieve a specific result entry with all related data security: - bearerAuth: [] parameters: @@ -148,44 +118,94 @@ properties: status: type: string + example: success + message: + type: string data: type: object properties: ResultID: type: integer - InternalPID: + SiteID: type: integer OrderID: - type: string - TestID: type: integer - TestCode: + InternalSID: + type: integer + SID: type: string - TestName: + SampleID: type: string - ResultValue: + TestSiteID: + type: integer + TestSiteCode: type: string - Unit: + AspCnt: + type: integer + Result: type: string - ReferenceRange: + nullable: true + SampleType: type: string - AbnormalFlag: - type: string - Verified: - type: boolean - VerifiedBy: - type: string - VerifiedDate: + nullable: true + ResultDateTime: type: string format: date-time - ResultDate: + WorkstationID: + type: integer + nullable: true + EquipmentID: + type: integer + nullable: true + RefNumID: + type: integer + nullable: true + RefTxtID: + type: integer + nullable: true + CreateDate: type: string format: date-time + TestSiteName: + type: string + nullable: true + Unit1: + type: string + nullable: true + Unit2: + type: string + nullable: true + Low: + type: number + nullable: true + High: + type: number + nullable: true + LowSign: + type: string + nullable: true + HighSign: + type: string + nullable: true + RefDisplay: + type: string + nullable: true + OrderNumber: + type: string + nullable: true + InternalPID: + type: integer + '404': + description: Result not found + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/ErrorResponse' patch: tags: [Results] summary: Update result - description: Update an existing result entry + description: Update a result value with automatic validation against reference ranges. Returns calculated flag (L/H) in response but does not store it. security: - bearerAuth: [] parameters: @@ -202,27 +222,61 @@ schema: type: object properties: - ResultValue: + Result: type: string - Unit: + description: The result value + RefNumID: + type: integer + description: Reference range ID to validate against + SampleType: type: string - AbnormalFlag: - type: string - enum: [H, L, N, A, C] - Verified: - type: boolean + nullable: true + WorkstationID: + type: integer + nullable: true + EquipmentID: + type: integer + nullable: true responses: '200': description: Result updated successfully content: application/json: schema: - $ref: '../components/schemas/common.yaml#/SuccessResponse' + type: object + properties: + status: + type: string + example: success + message: + type: string + data: + type: object + properties: + result: + type: object + flag: + type: string + nullable: true + enum: [L, H] + description: Calculated flag - L for Low, H for High, null for normal + '400': + description: Validation failed + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/ErrorResponse' + '404': + description: Result not found + content: + application/json: + schema: + $ref: '../components/schemas/common.yaml#/ErrorResponse' delete: tags: [Results] summary: Delete result - description: Soft delete a result entry + description: Soft delete a result entry by setting DelDate security: - bearerAuth: [] parameters: @@ -239,25 +293,9 @@ application/json: schema: $ref: '../components/schemas/common.yaml#/SuccessResponse' - -/api/results/{id}/verify: - post: - tags: [Results] - summary: Verify result - description: Mark a result as verified by the current user - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - schema: - type: integer - description: Result ID - responses: - '200': - description: Result verified successfully + '404': + description: Result not found content: application/json: schema: - $ref: '../components/schemas/common.yaml#/SuccessResponse' + $ref: '../components/schemas/common.yaml#/ErrorResponse' \ No newline at end of file