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
+ }
+ ?>
+
+
+
+
+
+
+
Patient Information
+
+
+ Patient Name
+ = esc(($patient['NameFirst'] ?? '') . ' ' . ($patient['NameLast'] ?? '')) ?>
+
+
+ Patient ID
+ = esc($patient['PatientID'] ?? 'N/A') ?>
+
+
+ Date of Birth
+ = esc($patient['Birthdate'] ?? 'N/A') ?>
+
+
+ Sex
+ = esc($patient['Sex'] ?? 'N/A') ?>
+
+
+ Phone
+ = esc($patient['MobilePhone'] ?? $patient['Phone'] ?? 'N/A') ?>
+
+
+ Address
+ = esc(($patient['Street_1'] ?? '') . ' ' . ($patient['City'] ?? '')) ?>
+
+
+
+
+
+
Order Information
+
+
+ Order ID
+ = esc($order['OrderID'] ?? 'N/A') ?>
+
+
+ Order Date
+ = esc($order['TrnDate'] ?? 'N/A') ?>
+
+
+ Priority
+ = esc($order['Priority'] ?? 'N/A') ?>
+
+
+ Requesting Physician
+ = esc($order['ReqApp'] ?? 'N/A') ?>
+
+
+ Placer ID
+ = esc($order['PlacerID'] ?? 'N/A') ?>
+
+
+
+
+
+
Test Results
+
+
+
+ | Test Name |
+ Code |
+ Result |
+ Unit |
+ Flag |
+ Reference Range |
+
+
+
+
+
+
+ | = esc($result['TestSiteName'] ?? 'N/A') ?> |
+ = esc($result['TestSiteCode'] ?? 'N/A') ?> |
+ = esc($result['Result'] ?? 'Pending') ?> |
+ = esc($result['Unit1'] ?? '') ?> |
+
+
+
+ = $flag === 'H' ? 'HIGH' : 'LOW' ?>
+
+
+ -
+
+ |
+
+
+ = esc($result['LowSign'] ?? '') ?> = esc($result['Low']) ?> - = esc($result['HighSign'] ?? '') ?> = esc($result['High']) ?>
+
+ -
+
+ |
+
+
+
+
+ | 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