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
This commit is contained in:
parent
42006e1af9
commit
85c7e96405
@ -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');
|
||||
});
|
||||
|
||||
|
||||
|
||||
75
app/Controllers/ReportController.php
Normal file
75
app/Controllers/ReportController.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Traits\ResponseTrait;
|
||||
use CodeIgniter\Controller;
|
||||
use App\Models\PatResultModel;
|
||||
use App\Models\OrderTest\OrderTestModel;
|
||||
use App\Models\Patient\PatientModel;
|
||||
|
||||
class ReportController extends Controller {
|
||||
use ResponseTrait;
|
||||
|
||||
protected $resultModel;
|
||||
protected $orderModel;
|
||||
protected $patientModel;
|
||||
|
||||
public function __construct() {
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\RefRange\RefNumModel;
|
||||
use App\Models\OrderTest\OrderTestModel;
|
||||
use App\Models\Patient\PatientModel;
|
||||
|
||||
class PatResultModel extends BaseModel {
|
||||
protected $table = 'patres';
|
||||
protected $primaryKey = 'ResultID';
|
||||
@ -25,4 +29,245 @@ class PatResultModel extends BaseModel {
|
||||
'ArchiveDate',
|
||||
'DelDate'
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate result value against reference range and return flag
|
||||
*
|
||||
* @param float|string $resultValue The result value to validate
|
||||
* @param int $refNumID The reference range ID
|
||||
* @param int $orderID The order ID to get patient info
|
||||
* @return string|null 'L' for low, 'H' for high, or null for normal/no match
|
||||
*/
|
||||
public function validateAndFlag($resultValue, int $refNumID, int $orderID): ?string {
|
||||
if (!is_numeric($resultValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$refNumModel = new RefNumModel();
|
||||
$ref = $refNumModel->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')]);
|
||||
}
|
||||
}
|
||||
|
||||
376
app/Views/reports/lab_report.php
Normal file
376
app/Views/reports/lab_report.php
Normal file
@ -0,0 +1,376 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lab Report - <?= esc($order['OrderID'] ?? 'N/A') ?></title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.report-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid #ccc;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.header .subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.patient-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.order-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.results-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.results-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.results-table th,
|
||||
.results-table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.results-table th {
|
||||
background: #e0e0e0;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.results-table tr:nth-child(even) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.flag {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.flag-low {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffc107;
|
||||
}
|
||||
|
||||
.flag-high {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.reference-range {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ccc;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.signature-section {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.signature-box {
|
||||
text-align: center;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.signature-line {
|
||||
border-bottom: 1px solid #333;
|
||||
margin-top: 60px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.print-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 10px 20px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.print-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.report-container {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.print-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.results-table {
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
.results-table tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
// Helper function to calculate flag dynamically
|
||||
function calculateFlag($result) {
|
||||
if (empty($result['Result']) || !is_numeric($result['Result'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (empty($result['Low']) || empty($result['High'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = floatval($result['Result']);
|
||||
$low = floatval($result['Low']);
|
||||
$high = floatval($result['High']);
|
||||
$lowSign = $result['LowSign'] ?? '<=';
|
||||
$highSign = $result['HighSign'] ?? '<=';
|
||||
|
||||
// Check low
|
||||
if ($lowSign === '<=' && $value <= $low) {
|
||||
return 'L';
|
||||
}
|
||||
if ($lowSign === '<' && $value < $low) {
|
||||
return 'L';
|
||||
}
|
||||
|
||||
// Check high
|
||||
if ($highSign === '>=' && $value >= $high) {
|
||||
return 'H';
|
||||
}
|
||||
if ($highSign === '>' && $value > $high) {
|
||||
return 'H';
|
||||
}
|
||||
|
||||
return null; // Normal
|
||||
}
|
||||
?>
|
||||
<button class="print-button" onclick="window.print()">Print Report</button>
|
||||
|
||||
<div class="report-container">
|
||||
<div class="header">
|
||||
<h1>LABORATORY REPORT</h1>
|
||||
<div class="subtitle">Clinical Laboratory Quality Management System</div>
|
||||
</div>
|
||||
|
||||
<div class="patient-section">
|
||||
<div class="section-title">Patient Information</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Patient Name</span>
|
||||
<span class="info-value"><?= esc(($patient['NameFirst'] ?? '') . ' ' . ($patient['NameLast'] ?? '')) ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Patient ID</span>
|
||||
<span class="info-value"><?= esc($patient['PatientID'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Date of Birth</span>
|
||||
<span class="info-value"><?= esc($patient['Birthdate'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Sex</span>
|
||||
<span class="info-value"><?= esc($patient['Sex'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Phone</span>
|
||||
<span class="info-value"><?= esc($patient['MobilePhone'] ?? $patient['Phone'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Address</span>
|
||||
<span class="info-value"><?= esc(($patient['Street_1'] ?? '') . ' ' . ($patient['City'] ?? '')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-section">
|
||||
<div class="section-title">Order Information</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Order ID</span>
|
||||
<span class="info-value"><?= esc($order['OrderID'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Order Date</span>
|
||||
<span class="info-value"><?= esc($order['TrnDate'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Priority</span>
|
||||
<span class="info-value"><?= esc($order['Priority'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Requesting Physician</span>
|
||||
<span class="info-value"><?= esc($order['ReqApp'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Placer ID</span>
|
||||
<span class="info-value"><?= esc($order['PlacerID'] ?? 'N/A') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results-section">
|
||||
<div class="section-title">Test Results</div>
|
||||
<table class="results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Test Name</th>
|
||||
<th>Code</th>
|
||||
<th>Result</th>
|
||||
<th>Unit</th>
|
||||
<th>Flag</th>
|
||||
<th>Reference Range</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($results)): ?>
|
||||
<?php foreach ($results as $result): ?>
|
||||
<tr>
|
||||
<td><?= esc($result['TestSiteName'] ?? 'N/A') ?></td>
|
||||
<td><?= esc($result['TestSiteCode'] ?? 'N/A') ?></td>
|
||||
<td><?= esc($result['Result'] ?? 'Pending') ?></td>
|
||||
<td><?= esc($result['Unit1'] ?? '') ?></td>
|
||||
<td>
|
||||
<?php
|
||||
$flag = calculateFlag($result);
|
||||
if (!empty($flag)):
|
||||
?>
|
||||
<span class="flag flag-<?= $flag === 'H' ? 'high' : 'low' ?>">
|
||||
<?= $flag === 'H' ? 'HIGH' : 'LOW' ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="reference-range">
|
||||
<?php if (!empty($result['Low']) && !empty($result['High'])): ?>
|
||||
<?= esc($result['LowSign'] ?? '') ?> <?= esc($result['Low']) ?> - <?= esc($result['HighSign'] ?? '') ?> <?= esc($result['High']) ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center;">No results found for this order</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="signature-section">
|
||||
<div class="signature-box">
|
||||
<div class="signature-line"></div>
|
||||
<div>Authorized Signature</div>
|
||||
<div style="font-size: 10px; color: #666; margin-top: 5px;">Pathologist / Lab Director</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><strong>Report Generated:</strong> <?= esc($generatedAt) ?></p>
|
||||
<p><strong>CLQMS</strong> - Clinical Laboratory Quality Management System</p>
|
||||
<p>This report is electronically generated and is valid without signature.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
941
docs/manual-result-entry-plan.md
Normal file
941
docs/manual-result-entry-plan.md
Normal file
@ -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
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use App\Models\PatResultModel;
|
||||
use App\Models\Patient\PatientModel;
|
||||
use App\Models\RefRange\RefNumModel;
|
||||
use App\Models\RefRange\RefTxtModel;
|
||||
use App\Models\Test\TestDefSiteModel;
|
||||
|
||||
class ResultEntryService
|
||||
{
|
||||
/**
|
||||
* Validate result value based on test type
|
||||
*
|
||||
* @param string $value
|
||||
* @param int $testSiteID
|
||||
* @return array ['valid' => bool, 'error' => string|null]
|
||||
*/
|
||||
public function validateResult(string $value, int $testSiteID): array
|
||||
|
||||
/**
|
||||
* Find applicable reference range
|
||||
*
|
||||
* @param int $testSiteID
|
||||
* @param array $patient Demographics: age (months), sex, specimenType
|
||||
* @return array|null Reference range data
|
||||
*/
|
||||
public function getApplicableRange(int $testSiteID, array $patient): ?array
|
||||
|
||||
/**
|
||||
* Calculate abnormal flag based on value and range
|
||||
*
|
||||
* @param string|float $value
|
||||
* @param array $range Reference range data
|
||||
* @return string H, L, N, A, or C
|
||||
*/
|
||||
public function calculateAbnormalFlag($value, array $range): string
|
||||
|
||||
/**
|
||||
* Format reference range for display
|
||||
*
|
||||
* @param array $range
|
||||
* @return string Human-readable range (e.g., "10.0 - 20.0 mg/dL")
|
||||
*/
|
||||
public function formatDisplayRange(array $range): string
|
||||
|
||||
/**
|
||||
* Check delta (compare with previous result)
|
||||
*
|
||||
* @param int $resultID Current result being edited
|
||||
* @param string|float $newValue
|
||||
* @return array ['hasPrevious' => bool, 'previousValue' => string|null, 'deltaPercent' => float|null, 'significant' => bool]
|
||||
*/
|
||||
public function checkDelta(int $resultID, $newValue): array
|
||||
|
||||
/**
|
||||
* Process result entry
|
||||
*
|
||||
* @param int $resultID
|
||||
* @param array $data
|
||||
* @param int $userID User entering the result
|
||||
* @return array ['success' => bool, 'result' => array|null, 'errors' => array]
|
||||
*/
|
||||
public function processEntry(int $resultID, array $data, int $userID): array
|
||||
|
||||
/**
|
||||
* Update calculated tests after dependency changes
|
||||
*
|
||||
* @param string $orderID
|
||||
* @param int $userID
|
||||
* @return int Number of calculated results updated
|
||||
*/
|
||||
public function recalculateDependentResults(string $orderID, int $userID): int
|
||||
|
||||
/**
|
||||
* Get worklist for workstation
|
||||
*
|
||||
* @param int $workstationID
|
||||
* @param array $filters
|
||||
* @return array
|
||||
*/
|
||||
public function getWorklist(int $workstationID, array $filters = []): array
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Implement ResultController
|
||||
|
||||
**File:** `app/Controllers/ResultController.php`
|
||||
|
||||
Replace the placeholder controller with full implementation:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Traits\ResponseTrait;
|
||||
use App\Libraries\ResultEntryService;
|
||||
use App\Models\PatResultModel;
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class ResultController extends Controller
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $resultModel;
|
||||
protected $entryService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->resultModel = new PatResultModel();
|
||||
$this->entryService = new ResultEntryService();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/results
|
||||
* List results with filtering
|
||||
*/
|
||||
public function index()
|
||||
|
||||
/**
|
||||
* GET /api/results/{id}
|
||||
* Get single result with details
|
||||
*/
|
||||
public function show($id = null)
|
||||
|
||||
/**
|
||||
* PATCH /api/results/{id}
|
||||
* Update result value
|
||||
*/
|
||||
public function update($id = null)
|
||||
|
||||
/**
|
||||
* POST /api/results/batch
|
||||
* Batch update multiple results
|
||||
*/
|
||||
public function batchUpdate()
|
||||
|
||||
/**
|
||||
* POST /api/results/{id}/verify
|
||||
* Verify a result
|
||||
*/
|
||||
public function verify($id = null)
|
||||
|
||||
/**
|
||||
* POST /api/results/{id}/unverify
|
||||
* Unverify/amend a result
|
||||
*/
|
||||
public function unverify($id = null)
|
||||
|
||||
/**
|
||||
* GET /api/results/worklist
|
||||
* Get pending results for workstation
|
||||
*/
|
||||
public function worklist()
|
||||
|
||||
/**
|
||||
* GET /api/results/order/{orderID}
|
||||
* Get all results for an order
|
||||
*/
|
||||
public function byOrder($orderID = null)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Database Schema (Priority: HIGH)
|
||||
|
||||
### 2.1 Migration for New Fields
|
||||
|
||||
**File:** `app/Database/Migrations/2025-03-04-000001_AddResultFields.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddResultFields extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addColumn('patres', [
|
||||
'ResultStatus' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 10,
|
||||
'null' => true,
|
||||
'comment' => 'PEN=Pending, PRE=Preliminary, FIN=Final, AMD=Amended',
|
||||
'after' => 'RefTxtID'
|
||||
],
|
||||
'Verified' => [
|
||||
'type' => 'TINYINT',
|
||||
'constraint' => 1,
|
||||
'default' => 0,
|
||||
'after' => 'ResultStatus'
|
||||
],
|
||||
'VerifiedBy' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
'after' => 'Verified'
|
||||
],
|
||||
'VerifiedDate' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
'after' => 'VerifiedBy'
|
||||
],
|
||||
'EnteredBy' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => true,
|
||||
'after' => 'VerifiedDate'
|
||||
],
|
||||
'AbnormalFlag' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 1,
|
||||
'null' => true,
|
||||
'comment' => 'H=High, L=Low, N=Normal, A=Abnormal, C=Critical',
|
||||
'after' => 'EnteredBy'
|
||||
],
|
||||
'Comment' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true,
|
||||
'after' => 'AbnormalFlag'
|
||||
],
|
||||
'Unit' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 50,
|
||||
'null' => true,
|
||||
'after' => 'Result'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropColumn('patres', [
|
||||
'ResultStatus',
|
||||
'Verified',
|
||||
'VerifiedBy',
|
||||
'VerifiedDate',
|
||||
'EnteredBy',
|
||||
'AbnormalFlag',
|
||||
'Comment',
|
||||
'Unit'
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Create Result History Table (Audit Trail)
|
||||
|
||||
**File:** `app/Database/Migrations/2025-03-04-000002_CreatePatResHistory.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreatePatResHistory extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'HistoryID' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true
|
||||
],
|
||||
'ResultID' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true
|
||||
],
|
||||
'OrderID' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true
|
||||
],
|
||||
'TestSiteID' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true
|
||||
],
|
||||
'OldResult' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true
|
||||
],
|
||||
'NewResult' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true
|
||||
],
|
||||
'OldStatus' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 10,
|
||||
'null' => true
|
||||
],
|
||||
'NewStatus' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 10,
|
||||
'null' => true
|
||||
],
|
||||
'ChangedBy' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true
|
||||
],
|
||||
'ChangeReason' => [
|
||||
'type' => 'TEXT',
|
||||
'null' => true
|
||||
],
|
||||
'CreateDate' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => false
|
||||
]
|
||||
]);
|
||||
|
||||
$this->forge->addKey('HistoryID', true);
|
||||
$this->forge->addKey('ResultID');
|
||||
$this->forge->addKey('CreateDate');
|
||||
$this->forge->createTable('patreshistory');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('patreshistory');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: API Routes (Priority: HIGH)
|
||||
|
||||
### 3.1 Update Routes.php
|
||||
|
||||
Add to `app/Config/Routes.php` within the existing `api` group:
|
||||
|
||||
```php
|
||||
// Results
|
||||
$routes->group('results', function ($routes) {
|
||||
$routes->get('/', 'ResultController::index');
|
||||
$routes->get('worklist', 'ResultController::worklist');
|
||||
$routes->get('order/(:any)', 'ResultController::byOrder/$1');
|
||||
$routes->get('(:num)', 'ResultController::show/$1');
|
||||
$routes->patch('(:num)', 'ResultController::update/$1');
|
||||
$routes->post('batch', 'ResultController::batchUpdate');
|
||||
$routes->post('(:num)/verify', 'ResultController::verify/$1');
|
||||
$routes->post('(:num)/unverify', 'ResultController::unverify/$1');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: API Documentation (Priority: MEDIUM)
|
||||
|
||||
### 4.1 Create Results Schema
|
||||
|
||||
**File:** `public/components/schemas/results.yaml`
|
||||
|
||||
```yaml
|
||||
Result:
|
||||
type: object
|
||||
properties:
|
||||
ResultID:
|
||||
type: integer
|
||||
OrderID:
|
||||
type: integer
|
||||
InternalSID:
|
||||
type: integer
|
||||
nullable: true
|
||||
TestSiteID:
|
||||
type: integer
|
||||
TestSiteCode:
|
||||
type: string
|
||||
TestSiteName:
|
||||
type: string
|
||||
nullable: true
|
||||
SID:
|
||||
type: string
|
||||
SampleID:
|
||||
type: string
|
||||
Result:
|
||||
type: string
|
||||
nullable: true
|
||||
Unit:
|
||||
type: string
|
||||
nullable: true
|
||||
ResultStatus:
|
||||
type: string
|
||||
enum: [PEN, PRE, FIN, AMD]
|
||||
nullable: true
|
||||
Verified:
|
||||
type: boolean
|
||||
default: false
|
||||
VerifiedBy:
|
||||
type: integer
|
||||
nullable: true
|
||||
VerifiedDate:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
EnteredBy:
|
||||
type: integer
|
||||
nullable: true
|
||||
AbnormalFlag:
|
||||
type: string
|
||||
enum: [H, L, N, A, C]
|
||||
nullable: true
|
||||
Comment:
|
||||
type: string
|
||||
nullable: true
|
||||
CreateDate:
|
||||
type: string
|
||||
format: date-time
|
||||
ReferenceRange:
|
||||
type: object
|
||||
nullable: true
|
||||
properties:
|
||||
Low:
|
||||
type: number
|
||||
High:
|
||||
type: number
|
||||
Display:
|
||||
type: string
|
||||
Patient:
|
||||
type: object
|
||||
properties:
|
||||
InternalPID:
|
||||
type: integer
|
||||
PatientID:
|
||||
type: string
|
||||
NameFirst:
|
||||
type: string
|
||||
NameLast:
|
||||
type: string
|
||||
Birthdate:
|
||||
type: string
|
||||
format: date
|
||||
Sex:
|
||||
type: string
|
||||
|
||||
ResultEntryRequest:
|
||||
type: object
|
||||
required:
|
||||
- Result
|
||||
properties:
|
||||
Result:
|
||||
type: string
|
||||
description: The result value
|
||||
Unit:
|
||||
type: string
|
||||
description: Unit override (optional)
|
||||
AbnormalFlag:
|
||||
type: string
|
||||
enum: [H, L, N, A, C]
|
||||
description: Override auto-calculated flag (optional)
|
||||
Comment:
|
||||
type: string
|
||||
description: Result comment
|
||||
ResultStatus:
|
||||
type: string
|
||||
enum: [PEN, PRE, FIN]
|
||||
description: Set status (can't set to AMD via update)
|
||||
|
||||
ResultBatchRequest:
|
||||
type: object
|
||||
required:
|
||||
- results
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
ResultID:
|
||||
type: integer
|
||||
Result:
|
||||
type: string
|
||||
Unit:
|
||||
type: string
|
||||
Comment:
|
||||
type: string
|
||||
|
||||
ResultVerifyRequest:
|
||||
type: object
|
||||
properties:
|
||||
comment:
|
||||
type: string
|
||||
description: Optional verification comment
|
||||
|
||||
ResultUnverifyRequest:
|
||||
type: object
|
||||
required:
|
||||
- reason
|
||||
properties:
|
||||
reason:
|
||||
type: string
|
||||
description: Required reason for amendment
|
||||
|
||||
ResultWorklistResponse:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
ResultID:
|
||||
type: integer
|
||||
PatientName:
|
||||
type: string
|
||||
PatientID:
|
||||
type: string
|
||||
OrderID:
|
||||
type: string
|
||||
TestCode:
|
||||
type: string
|
||||
TestName:
|
||||
type: string
|
||||
ResultStatus:
|
||||
type: string
|
||||
Priority:
|
||||
type: string
|
||||
OrderDate:
|
||||
type: string
|
||||
format: date-time
|
||||
```
|
||||
|
||||
### 4.2 Create API Paths Documentation
|
||||
|
||||
**File:** `public/paths/results.yaml`
|
||||
|
||||
Document all endpoints (GET /api/results, GET /api/results/{id}, PATCH /api/results/{id}, POST /api/results/batch, POST /api/results/{id}/verify, POST /api/results/{id}/unverify, GET /api/results/worklist, GET /api/results/order/{orderID}) with:
|
||||
- Summary and description
|
||||
- Security (bearerAuth)
|
||||
- Parameters (path, query)
|
||||
- RequestBody schemas
|
||||
- Response schemas
|
||||
- Error responses
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Reference Range Integration (Priority: MEDIUM)
|
||||
|
||||
### 5.1 Create RefRangeService
|
||||
|
||||
**File:** `app/Libraries/RefRangeService.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use App\Models\RefRange\RefNumModel;
|
||||
use App\Models\RefRange\RefTxtModel;
|
||||
|
||||
class RefRangeService
|
||||
{
|
||||
/**
|
||||
* Get applicable reference range for a test and patient
|
||||
*
|
||||
* @param int $testSiteID
|
||||
* @param array $patient Contains: age (in months), sex (M/F), specimenType
|
||||
* @return array|null
|
||||
*/
|
||||
public function getApplicableRange(int $testSiteID, array $patient): ?array
|
||||
|
||||
/**
|
||||
* Evaluate numeric result against range
|
||||
*
|
||||
* @param float $value
|
||||
* @param array $range
|
||||
* @return string H, L, N, A, C
|
||||
*/
|
||||
public function evaluateNumeric(float $value, array $range): string
|
||||
|
||||
/**
|
||||
* Get text reference options
|
||||
*
|
||||
* @param int $refTxtID
|
||||
* @return array
|
||||
*/
|
||||
public function getTextOptions(int $refTxtID): array
|
||||
|
||||
/**
|
||||
* Format range for display
|
||||
*
|
||||
* @param array $range
|
||||
* @return string
|
||||
*/
|
||||
public function formatDisplay(array $range): string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Testing (Priority: MEDIUM)
|
||||
|
||||
### 6.1 Create Feature Tests
|
||||
|
||||
**File:** `tests/feature/Results/ResultEntryTest.php`
|
||||
|
||||
Test scenarios:
|
||||
- Get results list with filters
|
||||
- Get single result with details
|
||||
- Update result value
|
||||
- Validation errors (invalid value)
|
||||
- Auto-calculate abnormal flag
|
||||
- Delta check notification
|
||||
- Batch update
|
||||
|
||||
**File:** `tests/feature/Results/ResultVerifyTest.php`
|
||||
|
||||
Test scenarios:
|
||||
- Verify result successfully
|
||||
- Unverify with reason
|
||||
- Attempt to modify verified result
|
||||
- Permission checks
|
||||
- Amendment workflow
|
||||
|
||||
**File:** `tests/feature/Results/ResultWorklistTest.php`
|
||||
|
||||
Test scenarios:
|
||||
- Get worklist by workstation
|
||||
- Filter by priority
|
||||
- Sort by order date
|
||||
- Pagination
|
||||
|
||||
### 6.2 Create Unit Tests
|
||||
|
||||
**File:** `tests/unit/Libraries/ResultEntryServiceTest.php`
|
||||
|
||||
Test scenarios:
|
||||
- Result validation
|
||||
- Reference range matching
|
||||
- Abnormal flag calculation
|
||||
- Delta calculation
|
||||
- Calculated test formulas
|
||||
|
||||
**File:** `tests/unit/Libraries/RefRangeServiceTest.php`
|
||||
|
||||
Test scenarios:
|
||||
- Age-based range selection
|
||||
- Sex-based range selection
|
||||
- Specimen type matching
|
||||
- Boundary value evaluation
|
||||
|
||||
---
|
||||
|
||||
## Data Flow Diagram
|
||||
|
||||
```
|
||||
Order Creation
|
||||
|
|
||||
v
|
||||
Empty patres records created
|
||||
|
|
||||
v
|
||||
GET /api/results/worklist <-- Technician sees pending results
|
||||
|
|
||||
v
|
||||
GET /api/results/{id} <-- Load result with patient info
|
||||
|
|
||||
v
|
||||
PATCH /api/results/{id} <-- Enter result value
|
||||
| |
|
||||
| v
|
||||
| ResultEntryService.validateResult()
|
||||
| |
|
||||
| v
|
||||
| RefRangeService.getApplicableRange()
|
||||
| |
|
||||
| v
|
||||
| Auto-calculate AbnormalFlag
|
||||
| |
|
||||
v v
|
||||
Result updated in patres
|
||||
|
|
||||
v
|
||||
POST /api/results/{id}/verify <-- Senior tech/pathologist
|
||||
|
|
||||
v
|
||||
ResultStatus = FIN, Verified = true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Status Definitions
|
||||
|
||||
| Status | Code | Description |
|
||||
|--------|------|-------------|
|
||||
| Pending | PEN | Order created, awaiting result entry |
|
||||
| Preliminary | PRE | Result entered but not verified |
|
||||
| Final | FIN | Result verified by authorized user |
|
||||
| Amended | AMD | Previously final result modified |
|
||||
|
||||
## Abnormal Flag Definitions
|
||||
|
||||
| Flag | Meaning | Action Required |
|
||||
|------|---------|-----------------|
|
||||
| N | Normal | None |
|
||||
| H | High | Review |
|
||||
| L | Low | Review |
|
||||
| A | Abnormal (text) | Review |
|
||||
| C | Critical | Immediate notification |
|
||||
|
||||
---
|
||||
|
||||
## Questions for Implementation
|
||||
|
||||
Before starting implementation, clarify:
|
||||
|
||||
1. **Who can verify results?**
|
||||
- Option A: Any authenticated user
|
||||
- Option B: Users with specific role (senior tech, pathologist)
|
||||
- Option C: Configure per test/discipline
|
||||
|
||||
2. **Can calculated tests be manually edited?**
|
||||
- Option A: No, always auto-computed
|
||||
- Option B: Yes, allow override with reason
|
||||
- Option C: Configurable per test
|
||||
|
||||
3. **Audit trail requirements:**
|
||||
- Option A: Full history (every change)
|
||||
- Option B: Only amendments (verified→unverify→verify)
|
||||
- Option C: No audit trail needed
|
||||
|
||||
4. **Critical results handling:**
|
||||
- Option A: Flag only
|
||||
- Option B: Flag + notification system
|
||||
- Option C: Flag + mandatory acknowledgment
|
||||
|
||||
5. **Batch entry priority:**
|
||||
- Must-have or nice-to-have?
|
||||
- Support for templates/predefined sets?
|
||||
|
||||
6. **Delta check sensitivity:**
|
||||
- Fixed percentage threshold (e.g., 20%)?
|
||||
- Test-specific thresholds?
|
||||
- Configurable?
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] Phase 1.1: Extend PatResultModel with CRUD methods
|
||||
- [ ] Phase 1.2: Create ResultEntryService with business logic
|
||||
- [ ] Phase 1.3: Implement ResultController methods
|
||||
- [ ] Phase 2.1: Create migration for new patres fields
|
||||
- [ ] Phase 2.2: Create patreshistory table
|
||||
- [ ] Phase 3.1: Add routes to Routes.php
|
||||
- [ ] Phase 4.1: Create results.yaml schema
|
||||
- [ ] Phase 4.2: Create results.yaml paths documentation
|
||||
- [ ] Phase 4.3: Run `node public/bundle-api-docs.js`
|
||||
- [ ] Phase 5.1: Create RefRangeService
|
||||
- [ ] Phase 6.1: Create feature tests
|
||||
- [ ] Phase 6.2: Create unit tests
|
||||
- [ ] Run full test suite: `./vendor/bin/phpunit`
|
||||
|
||||
---
|
||||
|
||||
## Estimated Timeline
|
||||
|
||||
| Phase | Duration | Priority |
|
||||
|-------|----------|----------|
|
||||
| Phase 1: Core Management | 2-3 days | HIGH |
|
||||
| Phase 2: Database Schema | 0.5 day | HIGH |
|
||||
| Phase 3: API Routes | 0.5 day | HIGH |
|
||||
| Phase 4: Documentation | 1 day | MEDIUM |
|
||||
| Phase 5: Reference Ranges | 1-2 days | MEDIUM |
|
||||
| Phase 6: Testing | 2-3 days | MEDIUM |
|
||||
| **Total** | **7-10 days** | |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All dates stored in UTC, convert to local time for display
|
||||
- Use transactions for all multi-table operations
|
||||
- Follow existing code style (camelCase methods, snake_case properties)
|
||||
- Update AGENTS.md if adding new commands or patterns
|
||||
- Consider performance: worklist queries should be fast (< 500ms)
|
||||
|
||||
*Last Updated: 2025-03-04*
|
||||
249
docs/mvp_plan.md
Normal file
249
docs/mvp_plan.md
Normal file
@ -0,0 +1,249 @@
|
||||
# CLQMS MVP Sprint Plan
|
||||
|
||||
> **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*
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
34
public/paths/reports.yaml
Normal file
34
public/paths/reports.yaml
Normal file
@ -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'
|
||||
@ -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'
|
||||
Loading…
x
Reference in New Issue
Block a user