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->group('api', ['filter' => 'auth'], function ($routes) {
|
||||||
$routes->get('dashboard', 'DashboardController::index');
|
$routes->get('dashboard', 'DashboardController::index');
|
||||||
$routes->get('result', 'ResultController::index');
|
|
||||||
$routes->get('sample', 'SampleController::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 App\Traits\ResponseTrait;
|
||||||
use CodeIgniter\Controller;
|
use CodeIgniter\Controller;
|
||||||
|
use App\Models\PatResultModel;
|
||||||
use Firebase\JWT\JWT;
|
|
||||||
use Firebase\JWT\Key;
|
|
||||||
use Firebase\JWT\ExpiredException;
|
|
||||||
use Firebase\JWT\SignatureInvalidException;
|
|
||||||
use Firebase\JWT\BeforeValidException;
|
|
||||||
use CodeIgniter\Cookie\Cookie;
|
|
||||||
|
|
||||||
class ResultController extends Controller {
|
class ResultController extends Controller {
|
||||||
use ResponseTrait;
|
use ResponseTrait;
|
||||||
|
|
||||||
|
protected $model;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->model = new PatResultModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List results with optional filters
|
||||||
|
* GET /api/results
|
||||||
|
*/
|
||||||
public function index() {
|
public function index() {
|
||||||
|
try {
|
||||||
|
$orderID = $this->request->getGet('order_id');
|
||||||
|
$patientID = $this->request->getGet('patient_id');
|
||||||
|
|
||||||
$token = $this->request->getCookie('token');
|
if ($orderID) {
|
||||||
$key = getenv('JWT_SECRET');
|
$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);
|
||||||
|
|
||||||
// Decode Token dengan Key yg ada di .env
|
$results = $this->model
|
||||||
$decodedPayload = JWT::decode($token, new Key($key, 'HS256'));
|
->where('DelDate', null)
|
||||||
|
->orderBy('ResultID', 'DESC')
|
||||||
|
->paginate($perPage, 'default', $page);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->respond([
|
return $this->respond([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'code' => 200,
|
'message' => 'Results retrieved successfully',
|
||||||
'message' => 'Authenticated',
|
'data' => $results
|
||||||
'data' => $decodedPayload
|
|
||||||
], 200);
|
], 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
|
<?php
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\RefRange\RefNumModel;
|
||||||
|
use App\Models\OrderTest\OrderTestModel;
|
||||||
|
use App\Models\Patient\PatientModel;
|
||||||
|
|
||||||
class PatResultModel extends BaseModel {
|
class PatResultModel extends BaseModel {
|
||||||
protected $table = 'patres';
|
protected $table = 'patres';
|
||||||
protected $primaryKey = 'ResultID';
|
protected $primaryKey = 'ResultID';
|
||||||
@ -25,4 +29,245 @@ class PatResultModel extends BaseModel {
|
|||||||
'ArchiveDate',
|
'ArchiveDate',
|
||||||
'DelDate'
|
'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
|
- name: Orders
|
||||||
description: Laboratory order management
|
description: Laboratory order management
|
||||||
- name: Results
|
- name: Results
|
||||||
description: Patient results reporting
|
description: Patient results reporting with auto-validation
|
||||||
|
- name: Reports
|
||||||
|
description: Lab report generation (HTML view)
|
||||||
- name: Edge API
|
- name: Edge API
|
||||||
description: Instrument integration endpoints
|
description: Instrument integration endpoints
|
||||||
- name: Contacts
|
- name: Contacts
|
||||||
@ -2707,51 +2709,75 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
data:
|
data:
|
||||||
$ref: '#/components/schemas/Patient'
|
$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:
|
/api/results:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Results
|
- Results
|
||||||
summary: Get patient results
|
summary: List results
|
||||||
description: Retrieve patient test results with optional filters
|
description: Retrieve patient test results with optional filters by order or patient
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
- name: InternalPID
|
- name: order_id
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
description: Filter by internal patient ID
|
description: Filter by internal order ID
|
||||||
- name: OrderID
|
- name: patient_id
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: integer
|
||||||
description: Filter by order ID
|
description: Filter by internal patient ID (returns cumulative results)
|
||||||
- name: TestCode
|
- name: page
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: integer
|
||||||
description: Filter by test code
|
default: 1
|
||||||
- name: date_from
|
description: Page number for pagination
|
||||||
|
- name: per_page
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: integer
|
||||||
format: date
|
default: 20
|
||||||
description: Filter results from date (YYYY-MM-DD)
|
description: Number of results per page
|
||||||
- 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
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: List of patient results
|
description: List of results
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@ -2759,6 +2785,9 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
example: success
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
data:
|
data:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -2766,84 +2795,57 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
ResultID:
|
ResultID:
|
||||||
type: integer
|
type: integer
|
||||||
InternalPID:
|
|
||||||
type: integer
|
|
||||||
OrderID:
|
OrderID:
|
||||||
type: string
|
|
||||||
TestID:
|
|
||||||
type: integer
|
type: integer
|
||||||
TestCode:
|
TestSiteID:
|
||||||
|
type: integer
|
||||||
|
TestSiteCode:
|
||||||
type: string
|
type: string
|
||||||
TestName:
|
Result:
|
||||||
type: string
|
type: string
|
||||||
ResultValue:
|
nullable: true
|
||||||
type: string
|
ResultDateTime:
|
||||||
Unit:
|
|
||||||
type: string
|
|
||||||
ReferenceRange:
|
|
||||||
type: string
|
|
||||||
AbnormalFlag:
|
|
||||||
type: string
|
|
||||||
Verified:
|
|
||||||
type: boolean
|
|
||||||
VerifiedBy:
|
|
||||||
type: string
|
|
||||||
VerifiedDate:
|
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
ResultDate:
|
RefNumID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
RefTxtID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
CreateDate:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
post:
|
TestSiteName:
|
||||||
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
|
type: string
|
||||||
TestID:
|
nullable: true
|
||||||
type: integer
|
Unit1:
|
||||||
ResultValue:
|
|
||||||
type: string
|
type: string
|
||||||
Unit:
|
nullable: true
|
||||||
|
Unit2:
|
||||||
type: string
|
type: string
|
||||||
AbnormalFlag:
|
nullable: true
|
||||||
|
Low:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
High:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
LowSign:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
nullable: true
|
||||||
- H
|
HighSign:
|
||||||
- L
|
type: string
|
||||||
- 'N'
|
nullable: true
|
||||||
- A
|
RefDisplay:
|
||||||
- C
|
type: string
|
||||||
description: H=High, L=Low, N=Normal, A=Abnormal, C=Critical
|
nullable: true
|
||||||
responses:
|
|
||||||
'201':
|
|
||||||
description: Result created successfully
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/SuccessResponse'
|
|
||||||
/api/results/{id}:
|
/api/results/{id}:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Results
|
- Results
|
||||||
summary: Get result by ID
|
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:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -2863,44 +2865,94 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
example: success
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
data:
|
data:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
ResultID:
|
ResultID:
|
||||||
type: integer
|
type: integer
|
||||||
InternalPID:
|
SiteID:
|
||||||
type: integer
|
type: integer
|
||||||
OrderID:
|
OrderID:
|
||||||
type: string
|
|
||||||
TestID:
|
|
||||||
type: integer
|
type: integer
|
||||||
TestCode:
|
InternalSID:
|
||||||
|
type: integer
|
||||||
|
SID:
|
||||||
type: string
|
type: string
|
||||||
TestName:
|
SampleID:
|
||||||
type: string
|
type: string
|
||||||
ResultValue:
|
TestSiteID:
|
||||||
|
type: integer
|
||||||
|
TestSiteCode:
|
||||||
type: string
|
type: string
|
||||||
Unit:
|
AspCnt:
|
||||||
|
type: integer
|
||||||
|
Result:
|
||||||
type: string
|
type: string
|
||||||
ReferenceRange:
|
nullable: true
|
||||||
|
SampleType:
|
||||||
type: string
|
type: string
|
||||||
AbnormalFlag:
|
nullable: true
|
||||||
type: string
|
ResultDateTime:
|
||||||
Verified:
|
|
||||||
type: boolean
|
|
||||||
VerifiedBy:
|
|
||||||
type: string
|
|
||||||
VerifiedDate:
|
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
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
|
type: string
|
||||||
format: date-time
|
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:
|
patch:
|
||||||
tags:
|
tags:
|
||||||
- Results
|
- Results
|
||||||
summary: Update result
|
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:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -2917,32 +2969,63 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
ResultValue:
|
Result:
|
||||||
type: string
|
type: string
|
||||||
Unit:
|
description: The result value
|
||||||
|
RefNumID:
|
||||||
|
type: integer
|
||||||
|
description: Reference range ID to validate against
|
||||||
|
SampleType:
|
||||||
type: string
|
type: string
|
||||||
AbnormalFlag:
|
nullable: true
|
||||||
type: string
|
WorkstationID:
|
||||||
enum:
|
type: integer
|
||||||
- H
|
nullable: true
|
||||||
- L
|
EquipmentID:
|
||||||
- 'N'
|
type: integer
|
||||||
- A
|
nullable: true
|
||||||
- C
|
|
||||||
Verified:
|
|
||||||
type: boolean
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Result updated successfully
|
description: Result updated successfully
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
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:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
- Results
|
- Results
|
||||||
summary: Delete result
|
summary: Delete result
|
||||||
description: Soft delete a result entry
|
description: Soft delete a result entry by setting DelDate
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -2959,28 +3042,12 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/SuccessResponse'
|
$ref: '#/components/schemas/SuccessResponse'
|
||||||
/api/results/{id}/verify:
|
'404':
|
||||||
post:
|
description: Result not found
|
||||||
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
|
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/SuccessResponse'
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
/api/specimen:
|
/api/specimen:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|||||||
@ -38,7 +38,9 @@ tags:
|
|||||||
- name: Orders
|
- name: Orders
|
||||||
description: Laboratory order management
|
description: Laboratory order management
|
||||||
- name: Results
|
- name: Results
|
||||||
description: Patient results reporting
|
description: Patient results reporting with auto-validation
|
||||||
|
- name: Reports
|
||||||
|
description: Lab report generation (HTML view)
|
||||||
- name: Edge API
|
- name: Edge API
|
||||||
description: Instrument integration endpoints
|
description: Instrument integration endpoints
|
||||||
- name: Contacts
|
- 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:
|
/api/results:
|
||||||
get:
|
get:
|
||||||
tags: [Results]
|
tags: [Results]
|
||||||
summary: Get patient results
|
summary: List results
|
||||||
description: Retrieve patient test results with optional filters
|
description: Retrieve patient test results with optional filters by order or patient
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
- name: InternalPID
|
- name: order_id
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
description: Filter by internal patient ID
|
description: Filter by internal order ID
|
||||||
- name: OrderID
|
- name: patient_id
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: integer
|
||||||
description: Filter by order ID
|
description: Filter by internal patient ID (returns cumulative results)
|
||||||
- name: TestCode
|
- name: page
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: integer
|
||||||
description: Filter by test code
|
default: 1
|
||||||
- name: date_from
|
description: Page number for pagination
|
||||||
|
- name: per_page
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: integer
|
||||||
format: date
|
default: 20
|
||||||
description: Filter results from date (YYYY-MM-DD)
|
description: Number of results per page
|
||||||
- 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
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: List of patient results
|
description: List of results
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
@ -49,6 +38,9 @@
|
|||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
example: success
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
data:
|
data:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@ -56,79 +48,57 @@
|
|||||||
properties:
|
properties:
|
||||||
ResultID:
|
ResultID:
|
||||||
type: integer
|
type: integer
|
||||||
InternalPID:
|
|
||||||
type: integer
|
|
||||||
OrderID:
|
OrderID:
|
||||||
type: string
|
|
||||||
TestID:
|
|
||||||
type: integer
|
type: integer
|
||||||
TestCode:
|
TestSiteID:
|
||||||
|
type: integer
|
||||||
|
TestSiteCode:
|
||||||
type: string
|
type: string
|
||||||
TestName:
|
Result:
|
||||||
type: string
|
type: string
|
||||||
ResultValue:
|
nullable: true
|
||||||
type: string
|
ResultDateTime:
|
||||||
Unit:
|
|
||||||
type: string
|
|
||||||
ReferenceRange:
|
|
||||||
type: string
|
|
||||||
AbnormalFlag:
|
|
||||||
type: string
|
|
||||||
Verified:
|
|
||||||
type: boolean
|
|
||||||
VerifiedBy:
|
|
||||||
type: string
|
|
||||||
VerifiedDate:
|
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
ResultDate:
|
RefNumID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
RefTxtID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
CreateDate:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
TestSiteName:
|
||||||
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
|
type: string
|
||||||
TestID:
|
nullable: true
|
||||||
type: integer
|
Unit1:
|
||||||
ResultValue:
|
|
||||||
type: string
|
type: string
|
||||||
Unit:
|
nullable: true
|
||||||
|
Unit2:
|
||||||
type: string
|
type: string
|
||||||
AbnormalFlag:
|
nullable: true
|
||||||
|
Low:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
High:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
LowSign:
|
||||||
type: string
|
type: string
|
||||||
enum: [H, L, N, A, C]
|
nullable: true
|
||||||
description: H=High, L=Low, N=Normal, A=Abnormal, C=Critical
|
HighSign:
|
||||||
responses:
|
type: string
|
||||||
'201':
|
nullable: true
|
||||||
description: Result created successfully
|
RefDisplay:
|
||||||
content:
|
type: string
|
||||||
application/json:
|
nullable: true
|
||||||
schema:
|
|
||||||
$ref: '../components/schemas/common.yaml#/SuccessResponse'
|
|
||||||
|
|
||||||
/api/results/{id}:
|
/api/results/{id}:
|
||||||
get:
|
get:
|
||||||
tags: [Results]
|
tags: [Results]
|
||||||
summary: Get result by ID
|
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:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -148,44 +118,94 @@
|
|||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
|
example: success
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
data:
|
data:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
ResultID:
|
ResultID:
|
||||||
type: integer
|
type: integer
|
||||||
InternalPID:
|
SiteID:
|
||||||
type: integer
|
type: integer
|
||||||
OrderID:
|
OrderID:
|
||||||
type: string
|
|
||||||
TestID:
|
|
||||||
type: integer
|
type: integer
|
||||||
TestCode:
|
InternalSID:
|
||||||
|
type: integer
|
||||||
|
SID:
|
||||||
type: string
|
type: string
|
||||||
TestName:
|
SampleID:
|
||||||
type: string
|
type: string
|
||||||
ResultValue:
|
TestSiteID:
|
||||||
|
type: integer
|
||||||
|
TestSiteCode:
|
||||||
type: string
|
type: string
|
||||||
Unit:
|
AspCnt:
|
||||||
|
type: integer
|
||||||
|
Result:
|
||||||
type: string
|
type: string
|
||||||
ReferenceRange:
|
nullable: true
|
||||||
|
SampleType:
|
||||||
type: string
|
type: string
|
||||||
AbnormalFlag:
|
nullable: true
|
||||||
type: string
|
ResultDateTime:
|
||||||
Verified:
|
|
||||||
type: boolean
|
|
||||||
VerifiedBy:
|
|
||||||
type: string
|
|
||||||
VerifiedDate:
|
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
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
|
type: string
|
||||||
format: date-time
|
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:
|
patch:
|
||||||
tags: [Results]
|
tags: [Results]
|
||||||
summary: Update result
|
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:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -202,27 +222,61 @@
|
|||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
ResultValue:
|
Result:
|
||||||
type: string
|
type: string
|
||||||
Unit:
|
description: The result value
|
||||||
|
RefNumID:
|
||||||
|
type: integer
|
||||||
|
description: Reference range ID to validate against
|
||||||
|
SampleType:
|
||||||
type: string
|
type: string
|
||||||
AbnormalFlag:
|
nullable: true
|
||||||
type: string
|
WorkstationID:
|
||||||
enum: [H, L, N, A, C]
|
type: integer
|
||||||
Verified:
|
nullable: true
|
||||||
type: boolean
|
EquipmentID:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Result updated successfully
|
description: Result updated successfully
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
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:
|
delete:
|
||||||
tags: [Results]
|
tags: [Results]
|
||||||
summary: Delete result
|
summary: Delete result
|
||||||
description: Soft delete a result entry
|
description: Soft delete a result entry by setting DelDate
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -239,25 +293,9 @@
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/common.yaml#/SuccessResponse'
|
$ref: '../components/schemas/common.yaml#/SuccessResponse'
|
||||||
|
'404':
|
||||||
/api/results/{id}/verify:
|
description: Result not found
|
||||||
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
|
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '../components/schemas/common.yaml#/SuccessResponse'
|
$ref: '../components/schemas/common.yaml#/ErrorResponse'
|
||||||
Loading…
x
Reference in New Issue
Block a user