Merge branch 'main' of https://github.com/mahdahar/clqms-be
● refactor: update API responses to use {field}Label format
- Transform coded fields to lowercase with Label suffix for display text - Controllers: OrderTestController, DemoOrderController, SpecimenController,
SpecimenStatusController, SpecimenCollectionController, ContainerDefController,
ContactController, TestMapController
- Example: Priority: "R" → priority: "R", priorityLabel: "Routine"
- Update api-docs.yaml with new OpenAPI schema definitions
- Add API docs reminder to CLAUDE.md
This commit is contained in:
commit
212ab4e80a
6
.gitignore
vendored
6
.gitignore
vendored
@ -126,7 +126,5 @@ _modules/*
|
|||||||
/phpunit*.xml
|
/phpunit*.xml
|
||||||
/public/.htaccess
|
/public/.htaccess
|
||||||
|
|
||||||
#-------------------------
|
.serena/
|
||||||
# Claude
|
.claude/
|
||||||
#-------------------------
|
|
||||||
.claude
|
|
||||||
@ -54,7 +54,10 @@ $routes->group('v2', ['filter' => 'auth'], function ($routes) {
|
|||||||
|
|
||||||
// Master Data - Tests & ValueSets
|
// Master Data - Tests & ValueSets
|
||||||
$routes->get('master/tests', 'PagesController::masterTests');
|
$routes->get('master/tests', 'PagesController::masterTests');
|
||||||
$routes->get('master/valuesets', 'PagesController::masterValueSets');
|
|
||||||
|
$routes->get('valueset', 'PagesController::valueSetLibrary');
|
||||||
|
$routes->get('result/valueset', 'PagesController::resultValueSet');
|
||||||
|
$routes->get('result/valuesetdef', 'PagesController::resultValueSetDef');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Faker
|
// Faker
|
||||||
@ -161,12 +164,22 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->delete('items/(:num)', 'ValueSetController::deleteItem/$1');
|
$routes->delete('items/(:num)', 'ValueSetController::deleteItem/$1');
|
||||||
});
|
});
|
||||||
|
|
||||||
$routes->group('valuesetdef', function ($routes) {
|
$routes->group('result', function ($routes) {
|
||||||
$routes->get('/', 'ValueSetDefController::index');
|
$routes->group('valueset', function ($routes) {
|
||||||
$routes->get('(:num)', 'ValueSetDefController::show/$1');
|
$routes->get('/', 'Result\ResultValueSetController::index');
|
||||||
$routes->post('/', 'ValueSetDefController::create');
|
$routes->get('(:num)', 'Result\ResultValueSetController::show/$1');
|
||||||
$routes->put('(:num)', 'ValueSetDefController::update/$1');
|
$routes->post('/', 'Result\ResultValueSetController::create');
|
||||||
$routes->delete('(:num)', 'ValueSetDefController::delete/$1');
|
$routes->put('(:num)', 'Result\ResultValueSetController::update/$1');
|
||||||
|
$routes->delete('(:num)', 'Result\ResultValueSetController::delete/$1');
|
||||||
|
});
|
||||||
|
|
||||||
|
$routes->group('valuesetdef', function ($routes) {
|
||||||
|
$routes->get('/', 'ValueSetDefController::index');
|
||||||
|
$routes->get('(:num)', 'ValueSetDefController::show/$1');
|
||||||
|
$routes->post('/', 'ValueSetDefController::create');
|
||||||
|
$routes->put('(:num)', 'ValueSetDefController::update/$1');
|
||||||
|
$routes->delete('(:num)', 'ValueSetDefController::delete/$1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Counter
|
// Counter
|
||||||
|
|||||||
@ -32,16 +32,21 @@ class AreaGeoController extends BaseController {
|
|||||||
|
|
||||||
public function getProvinces() {
|
public function getProvinces() {
|
||||||
$rows = $this->model->getProvinces();
|
$rows = $this->model->getProvinces();
|
||||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "data not found", 'data' => '' ], 200); }
|
$transformed = array_map(function($row) {
|
||||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
return ['value' => $row['AreaGeoID'], 'label' => $row['AreaName']];
|
||||||
|
}, $rows);
|
||||||
|
if (empty($transformed)) { return $this->respond([ 'status' => 'success', 'data' => [] ], 200); }
|
||||||
|
return $this->respond([ 'status' => 'success', 'data' => $transformed ], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCities() {
|
public function getCities() {
|
||||||
$filter = [ 'Parent' => $this->request->getVar('Parent') ?? null ];
|
$filter = [ 'Parent' => $this->request->getVar('Parent') ?? null ];
|
||||||
$rows = $this->model->getCities($filter);
|
$rows = $this->model->getCities($filter);
|
||||||
|
$transformed = array_map(function($row) {
|
||||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "data not found", 'data' => [] ], 200); }
|
return ['value' => $row['AreaGeoID'], 'label' => $row['AreaName']];
|
||||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
}, $rows);
|
||||||
|
if (empty($transformed)) { return $this->respond([ 'status' => 'success', 'data' => [] ], 200); }
|
||||||
|
return $this->respond([ 'status' => 'success', 'data' => $transformed ], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -155,13 +155,35 @@ class PagesController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master Data - Value Sets
|
* Value Set Library - Read-only
|
||||||
*/
|
*/
|
||||||
public function masterValueSets()
|
public function valueSetLibrary()
|
||||||
{
|
{
|
||||||
return view('v2/master/valuesets/valuesets_index', [
|
return view('v2/valueset/valueset_index', [
|
||||||
'pageTitle' => 'Value Sets',
|
'pageTitle' => 'Value Set Library',
|
||||||
'activePage' => 'master-valuesets'
|
'activePage' => 'valueset-library'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result Valueset - CRUD for valueset table
|
||||||
|
*/
|
||||||
|
public function resultValueSet()
|
||||||
|
{
|
||||||
|
return view('v2/result/valueset/resultvalueset_index', [
|
||||||
|
'pageTitle' => 'Result Valuesets',
|
||||||
|
'activePage' => 'result-valueset'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result Valueset Definition - CRUD for valuesetdef table
|
||||||
|
*/
|
||||||
|
public function resultValueSetDef()
|
||||||
|
{
|
||||||
|
return view('v2/result/valuesetdef/resultvaluesetdef_index', [
|
||||||
|
'pageTitle' => 'Valueset Definitions',
|
||||||
|
'activePage' => 'result-valuesetdef'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
144
app/Controllers/Result/ResultValueSetController.php
Normal file
144
app/Controllers/Result/ResultValueSetController.php
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers\Result;
|
||||||
|
|
||||||
|
use App\Models\ValueSet\ValueSetModel;
|
||||||
|
use CodeIgniter\API\ResponseTrait;
|
||||||
|
|
||||||
|
class ResultValueSetController extends \CodeIgniter\Controller
|
||||||
|
{
|
||||||
|
use ResponseTrait;
|
||||||
|
|
||||||
|
protected $dbModel;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->dbModel = new ValueSetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$search = $this->request->getGet('search') ?? $this->request->getGet('param') ?? null;
|
||||||
|
$VSetID = $this->request->getGet('VSetID') ?? null;
|
||||||
|
|
||||||
|
$rows = $this->dbModel->getValueSets($search, $VSetID);
|
||||||
|
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => $rows
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id = null)
|
||||||
|
{
|
||||||
|
$row = $this->dbModel->getValueSet($id);
|
||||||
|
if (!$row) {
|
||||||
|
return $this->failNotFound("ValueSet item not found: $id");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => $row
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$input = $this->request->getJSON(true);
|
||||||
|
if (!$input) {
|
||||||
|
return $this->failValidationErrors(['Invalid JSON input']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'SiteID' => $input['SiteID'] ?? 1,
|
||||||
|
'VSetID' => $input['VSetID'] ?? null,
|
||||||
|
'VOrder' => $input['VOrder'] ?? 0,
|
||||||
|
'VValue' => $input['VValue'] ?? '',
|
||||||
|
'VDesc' => $input['VDesc'] ?? '',
|
||||||
|
'VCategory' => $input['VCategory'] ?? null
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($data['VSetID'] === null) {
|
||||||
|
return $this->failValidationErrors(['VSetID is required']);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$id = $this->dbModel->insert($data, true);
|
||||||
|
if (!$id) {
|
||||||
|
return $this->failValidationErrors($this->dbModel->errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$newRow = $this->dbModel->getValueSet($id);
|
||||||
|
return $this->respondCreated([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'ValueSet item created',
|
||||||
|
'data' => $newRow
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->failServerError('Failed to create: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id = null)
|
||||||
|
{
|
||||||
|
$input = $this->request->getJSON(true);
|
||||||
|
if (!$input) {
|
||||||
|
return $this->failValidationErrors(['Invalid JSON input']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = $this->dbModel->getValueSet($id);
|
||||||
|
if (!$existing) {
|
||||||
|
return $this->failNotFound("ValueSet item not found: $id");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
if (isset($input['VSetID'])) $data['VSetID'] = $input['VSetID'];
|
||||||
|
if (isset($input['VOrder'])) $data['VOrder'] = $input['VOrder'];
|
||||||
|
if (isset($input['VValue'])) $data['VValue'] = $input['VValue'];
|
||||||
|
if (isset($input['VDesc'])) $data['VDesc'] = $input['VDesc'];
|
||||||
|
if (isset($input['SiteID'])) $data['SiteID'] = $input['SiteID'];
|
||||||
|
if (isset($input['VCategory'])) $data['VCategory'] = $input['VCategory'];
|
||||||
|
|
||||||
|
if (empty($data)) {
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'No changes to update',
|
||||||
|
'data' => $existing
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$updated = $this->dbModel->update($id, $data);
|
||||||
|
if (!$updated) {
|
||||||
|
return $this->failValidationErrors($this->dbModel->errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
$newRow = $this->dbModel->getValueSet($id);
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'ValueSet item updated',
|
||||||
|
'data' => $newRow
|
||||||
|
], 200);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->failServerError('Failed to update: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($id = null)
|
||||||
|
{
|
||||||
|
$existing = $this->dbModel->getValueSet($id);
|
||||||
|
if (!$existing) {
|
||||||
|
return $this->failNotFound("ValueSet item not found: $id");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->dbModel->delete($id);
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => 'ValueSet item deleted'
|
||||||
|
], 200);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->failServerError('Failed to delete: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -158,12 +158,13 @@ class TestsController extends BaseController
|
|||||||
$row['refnum'] = array_map(function ($r) {
|
$row['refnum'] = array_map(function ($r) {
|
||||||
return [
|
return [
|
||||||
'RefNumID' => $r['RefNumID'],
|
'RefNumID' => $r['RefNumID'],
|
||||||
'NumRefType' => $r['NumRefType'],
|
'NumRefTypeKey' => $r['NumRefType'],
|
||||||
'NumRefTypeVValue' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']),
|
'NumRefType' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']),
|
||||||
'RangeTypeVValue' => ValueSet::getLabel('range_type', $r['RangeType']),
|
'RangeType' => ValueSet::getLabel('range_type', $r['RangeType']),
|
||||||
'SexVValue' => ValueSet::getLabel('gender', $r['Sex']),
|
'SexKey' => $r['Sex'],
|
||||||
'LowSignVValue' => ValueSet::getLabel('math_sign', $r['LowSign']),
|
'Sex' => ValueSet::getLabel('gender', $r['Sex']),
|
||||||
'HighSignVValue' => ValueSet::getLabel('math_sign', $r['HighSign']),
|
'LowSign' => ValueSet::getLabel('math_sign', $r['LowSign']),
|
||||||
|
'HighSign' => ValueSet::getLabel('math_sign', $r['HighSign']),
|
||||||
'High' => $r['High'] !== null ? (int) $r['High'] : null,
|
'High' => $r['High'] !== null ? (int) $r['High'] : null,
|
||||||
'Flag' => $r['Flag']
|
'Flag' => $r['Flag']
|
||||||
];
|
];
|
||||||
@ -183,10 +184,10 @@ class TestsController extends BaseController
|
|||||||
$row['reftxt'] = array_map(function ($r) {
|
$row['reftxt'] = array_map(function ($r) {
|
||||||
return [
|
return [
|
||||||
'RefTxtID' => $r['RefTxtID'],
|
'RefTxtID' => $r['RefTxtID'],
|
||||||
'TxtRefType' => $r['TxtRefType'],
|
'TxtRefTypeKey' => $r['TxtRefType'],
|
||||||
'TxtRefTypeVValue' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']),
|
'TxtRefType' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']),
|
||||||
'Sex' => $r['Sex'],
|
'SexKey' => $r['Sex'],
|
||||||
'SexVValue' => ValueSet::getLabel('gender', $r['Sex']),
|
'Sex' => ValueSet::getLabel('gender', $r['Sex']),
|
||||||
'AgeStart' => (int) $r['AgeStart'],
|
'AgeStart' => (int) $r['AgeStart'],
|
||||||
'AgeEnd' => (int) $r['AgeEnd'],
|
'AgeEnd' => (int) $r['AgeEnd'],
|
||||||
'RefTxt' => $r['RefTxt'],
|
'RefTxt' => $r['RefTxt'],
|
||||||
|
|||||||
@ -19,10 +19,19 @@ class ValueSetController extends \CodeIgniter\Controller
|
|||||||
|
|
||||||
public function index(?string $lookupName = null)
|
public function index(?string $lookupName = null)
|
||||||
{
|
{
|
||||||
|
$search = $this->request->getGet('search') ?? null;
|
||||||
|
|
||||||
if ($lookupName === null) {
|
if ($lookupName === null) {
|
||||||
$all = ValueSet::getAll();
|
$all = ValueSet::getAll();
|
||||||
$result = [];
|
$result = [];
|
||||||
foreach ($all as $name => $entry) {
|
foreach ($all as $name => $entry) {
|
||||||
|
if ($search) {
|
||||||
|
$nameLower = strtolower($name);
|
||||||
|
$searchLower = strtolower($search);
|
||||||
|
if (strpos($nameLower, $searchLower) === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
$count = count($entry['values'] ?? []);
|
$count = count($entry['values'] ?? []);
|
||||||
$result[$name] = $count;
|
$result[$name] = $count;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,9 @@ class ValueSet {
|
|||||||
foreach ($data as &$row) {
|
foreach ($data as &$row) {
|
||||||
foreach ($fieldMappings as $field => $lookupName) {
|
foreach ($fieldMappings as $field => $lookupName) {
|
||||||
if (isset($row[$field]) && $row[$field] !== null) {
|
if (isset($row[$field]) && $row[$field] !== null) {
|
||||||
$row[$field . 'Text'] = self::getLabel($lookupName, $row[$field]) ?? '';
|
$keyValue = $row[$field];
|
||||||
|
$row[$field . 'Key'] = $keyValue;
|
||||||
|
$row[$field] = self::getLabel($lookupName, $keyValue) ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,7 @@ class PatientModel extends BaseModel {
|
|||||||
|
|
||||||
$rows = $this->findAll();
|
$rows = $this->findAll();
|
||||||
$rows = ValueSet::transformLabels($rows, [
|
$rows = ValueSet::transformLabels($rows, [
|
||||||
'Sex' => 'gender',
|
'Sex' => 'sex',
|
||||||
]);
|
]);
|
||||||
return $rows;
|
return $rows;
|
||||||
}
|
}
|
||||||
@ -81,7 +81,7 @@ class PatientModel extends BaseModel {
|
|||||||
unset($patient['Comment']);
|
unset($patient['Comment']);
|
||||||
|
|
||||||
$patient = ValueSet::transformLabels([$patient], [
|
$patient = ValueSet::transformLabels([$patient], [
|
||||||
'Sex' => 'gender',
|
'Sex' => 'sex',
|
||||||
'Country' => 'country',
|
'Country' => 'country',
|
||||||
'Race' => 'race',
|
'Race' => 'race',
|
||||||
'Religion' => 'religion',
|
'Religion' => 'religion',
|
||||||
|
|||||||
@ -173,14 +173,39 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Value Sets -->
|
<!-- Value Sets (Nested Group) -->
|
||||||
<li>
|
<li>
|
||||||
<a href="<?= base_url('/v2/master/valuesets') ?>"
|
<div x-data="{
|
||||||
:class="isActive('master/valuesets') ? 'active' : ''"
|
isOpen: valuesetOpen,
|
||||||
class="group">
|
toggle() { this.isOpen = !this.isOpen; $root.layout().valuesetOpen = this.isOpen }
|
||||||
<i class="fa-solid fa-list-check w-5 text-center transition-transform group-hover:scale-110"></i>
|
}" x-init="$watch('valuesetOpen', v => isOpen = v)">
|
||||||
<span x-show="sidebarOpen">Value Sets</span>
|
<button @click="isOpen = !isOpen"
|
||||||
</a>
|
class="group w-full flex items-center justify-between"
|
||||||
|
:class="isParentActive('valueset') ? 'text-primary font-medium' : ''">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<i class="fa-solid fa-list-check w-5 text-center transition-transform group-hover:scale-110"></i>
|
||||||
|
<span x-show="sidebarOpen">Value Sets</span>
|
||||||
|
</div>
|
||||||
|
<i x-show="sidebarOpen" class="fa-solid fa-chevron-down text-xs transition-transform" :class="isOpen && 'rotate-180'"></i>
|
||||||
|
</button>
|
||||||
|
<ul x-show="isOpen && sidebarOpen" x-collapse class="ml-8 mt-2 space-y-1">
|
||||||
|
<li>
|
||||||
|
<a href="<?= base_url('/v2/valueset') ?>"
|
||||||
|
:class="isActive('valueset') && !isParentActive('result/valueset') && !isParentActive('result/valuesetdef') ? 'active' : ''"
|
||||||
|
class="text-sm">Library Valuesets</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="<?= base_url('/v2/result/valueset') ?>"
|
||||||
|
:class="isActive('result/valueset') ? 'active' : ''"
|
||||||
|
class="text-sm">Result Valuesets</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="<?= base_url('/v2/result/valuesetdef') ?>"
|
||||||
|
:class="isActive('result/valuesetdef') ? 'active' : ''"
|
||||||
|
class="text-sm">Valueset Definitions</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
@ -319,6 +344,7 @@
|
|||||||
lightMode: localStorage.getItem('theme') !== 'dark',
|
lightMode: localStorage.getItem('theme') !== 'dark',
|
||||||
orgOpen: false,
|
orgOpen: false,
|
||||||
specimenOpen: false,
|
specimenOpen: false,
|
||||||
|
valuesetOpen: false,
|
||||||
currentPath: window.location.pathname,
|
currentPath: window.location.pathname,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -333,6 +359,7 @@
|
|||||||
// Auto-expand menus based on active path
|
// Auto-expand menus based on active path
|
||||||
this.orgOpen = this.currentPath.includes('organization');
|
this.orgOpen = this.currentPath.includes('organization');
|
||||||
this.specimenOpen = this.currentPath.includes('specimen');
|
this.specimenOpen = this.currentPath.includes('specimen');
|
||||||
|
this.valuesetOpen = this.currentPath.includes('valueset');
|
||||||
|
|
||||||
// Watch sidebar state to persist
|
// Watch sidebar state to persist
|
||||||
this.$watch('sidebarOpen', val => localStorage.setItem('sidebarOpen', val));
|
this.$watch('sidebarOpen', val => localStorage.setItem('sidebarOpen', val));
|
||||||
|
|||||||
@ -1,363 +0,0 @@
|
|||||||
<!-- Nested ValueSet CRUD Modal -->
|
|
||||||
<div
|
|
||||||
x-show="showValueSetModal"
|
|
||||||
x-cloak
|
|
||||||
class="modal-overlay"
|
|
||||||
style="z-index: 1000;"
|
|
||||||
@click.self="$root.closeValueSetModal()"
|
|
||||||
x-transition:enter="transition ease-out duration-200"
|
|
||||||
x-transition:enter-start="opacity-0"
|
|
||||||
x-transition:enter-end="opacity-100"
|
|
||||||
x-transition:leave="transition ease-in duration-150"
|
|
||||||
x-transition:leave-start="opacity-100"
|
|
||||||
x-transition:leave-end="opacity-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="modal-content p-0 max-w-5xl w-full max-h-[90vh] overflow-hidden"
|
|
||||||
@click.stop
|
|
||||||
x-data="valueSetItems()"
|
|
||||||
x-init="selectedDef = $root.selectedDef; if(selectedDef) { fetchList(1); fetchDefsList(); }"
|
|
||||||
x-transition:enter="transition ease-out duration-200"
|
|
||||||
x-transition:enter-start="opacity-0 transform scale-95"
|
|
||||||
x-transition:enter-end="opacity-100 transform scale-100"
|
|
||||||
x-transition:leave="transition ease-in duration-150"
|
|
||||||
x-transition:leave-start="opacity-100 transform scale-100"
|
|
||||||
x-transition:leave-end="opacity-0 transform scale-95"
|
|
||||||
>
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="p-6 border-b flex items-center justify-between" style="background: rgb(var(--color-bg)); border-color: rgb(var(--color-border));">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-white shadow-md" style="background: rgb(var(--color-primary));">
|
|
||||||
<i class="fa-solid fa-list-ul"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-lg" style="color: rgb(var(--color-text));" x-text="selectedDef?.VSName || 'Value Items'"></h3>
|
|
||||||
<p class="text-xs uppercase font-bold opacity-40" style="color: rgb(var(--color-text-muted));">Manage Category Items</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button class="btn btn-primary btn-sm" @click="showForm()">
|
|
||||||
<i class="fa-solid fa-plus mr-2"></i> Add Item
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost btn-sm btn-square" @click="$root.closeValueSetModal()">
|
|
||||||
<i class="fa-solid fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search Bar -->
|
|
||||||
<div class="p-4 border-b" style="border-color: rgb(var(--color-border));">
|
|
||||||
<div class="relative">
|
|
||||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-gray-400"></i>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Filter items..."
|
|
||||||
class="input input-sm w-full pl-10"
|
|
||||||
x-model="keyword"
|
|
||||||
@keyup.enter="fetchList(1)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content Area -->
|
|
||||||
<div class="overflow-y-auto" style="max-height: calc(90vh - 200px);">
|
|
||||||
<!-- Loading Overlay -->
|
|
||||||
<div x-show="loading" class="py-20 text-center" x-cloak>
|
|
||||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
|
||||||
<p style="color: rgb(var(--color-text-muted));">Loading items...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Table Section -->
|
|
||||||
<div class="overflow-x-auto" x-show="!loading">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="w-20">ID</th>
|
|
||||||
<th>Value / Key</th>
|
|
||||||
<th>Definition</th>
|
|
||||||
<th class="text-center">Order</th>
|
|
||||||
<th class="text-center w-32">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<!-- Empty State -->
|
|
||||||
<template x-if="!list || list.length === 0">
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" class="py-20 text-center">
|
|
||||||
<div class="flex flex-col items-center gap-2 opacity-30" style="color: rgb(var(--color-text-muted));">
|
|
||||||
<i class="fa-solid fa-inbox text-4xl"></i>
|
|
||||||
<p class="font-bold italic">No items found in this category</p>
|
|
||||||
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
|
||||||
<i class="fa-solid fa-plus mr-1"></i> Add First Item
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Data Rows -->
|
|
||||||
<template x-for="v in list" :key="v.VID">
|
|
||||||
<tr class="hover:bg-opacity-50">
|
|
||||||
<td>
|
|
||||||
<span class="badge badge-ghost font-mono text-xs" x-text="v.VID || '-'"></span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="v.VValue || '-'"></div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="text-sm opacity-70" x-text="v.VDesc || '-'"></span>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<span class="font-mono text-sm" x-text="v.VOrder || 0"></span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="flex items-center justify-center gap-1">
|
|
||||||
<button class="btn btn-ghost btn-sm btn-square" @click="editValue(v.VID)" title="Edit">
|
|
||||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(v)" title="Delete">
|
|
||||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats Footer -->
|
|
||||||
<div class="p-4 flex items-center justify-between" style="border-top: 1px solid rgb(var(--color-border));" x-show="list && list.length > 0">
|
|
||||||
<span class="text-sm" style="color: rgb(var(--color-text-muted));" x-text="list.length + ' items'"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Item Form Dialog -->
|
|
||||||
<?= $this->include('v2/master/valuesets/valueset_dialog') ?>
|
|
||||||
|
|
||||||
<!-- Delete Modal -->
|
|
||||||
<div x-show="showDeleteModal" x-cloak class="modal-overlay" style="z-index: 1100;">
|
|
||||||
<div
|
|
||||||
class="card p-8 max-w-md w-full shadow-2xl"
|
|
||||||
x-show="showDeleteModal"
|
|
||||||
x-transition
|
|
||||||
>
|
|
||||||
<div class="w-16 h-16 rounded-2xl bg-rose-500/10 flex items-center justify-center text-rose-500 mx-auto mb-6">
|
|
||||||
<i class="fa-solid fa-triangle-exclamation text-2xl"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-center mb-2" style="color: rgb(var(--color-text));">Confirm Removal</h3>
|
|
||||||
<p class="text-center text-sm mb-8" style="color: rgb(var(--color-text-muted));">
|
|
||||||
Are you sure you want to delete <span class="font-bold text-rose-500" x-text="deleteTarget?.VValue"></span>?
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
|
||||||
<button class="btn flex-1 bg-rose-600 text-white hover:bg-rose-700 shadow-lg shadow-rose-600/20" @click="deleteValue()" :disabled="deleting">
|
|
||||||
<span x-show="deleting" class="spinner spinner-sm !border-white/20 !border-t-white"></span>
|
|
||||||
<span x-show="!deleting">Delete</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function valueSetItems() {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
list: [],
|
|
||||||
selectedDef: null,
|
|
||||||
keyword: "",
|
|
||||||
totalItems: 0,
|
|
||||||
|
|
||||||
// For dropdown population
|
|
||||||
defsList: [],
|
|
||||||
loadingDefs: false,
|
|
||||||
|
|
||||||
showModal: false,
|
|
||||||
isEditing: false,
|
|
||||||
saving: false,
|
|
||||||
errors: {},
|
|
||||||
form: {
|
|
||||||
VID: null,
|
|
||||||
VSetID: "",
|
|
||||||
VOrder: 0,
|
|
||||||
VValue: "",
|
|
||||||
VDesc: "",
|
|
||||||
SiteID: 1
|
|
||||||
},
|
|
||||||
|
|
||||||
showDeleteModal: false,
|
|
||||||
deleteTarget: null,
|
|
||||||
deleting: false,
|
|
||||||
|
|
||||||
async fetchList(page = 1) {
|
|
||||||
if (!this.selectedDef) return;
|
|
||||||
this.loading = true;
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('VSetID', this.selectedDef.VSetID);
|
|
||||||
if (this.keyword) params.append('search', this.keyword);
|
|
||||||
if (this.selectedDef) params.append('VSetID', this.selectedDef.VSetID);
|
|
||||||
|
|
||||||
const res = await fetch(`${BASEURL}api/valueset/items?${params}`, {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("HTTP error");
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
this.list = data.data || [];
|
|
||||||
this.totalItems = this.list.length;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.list = [];
|
|
||||||
this.totalItems = 0;
|
|
||||||
this.showToast('Failed to load items', 'error');
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchDefsList() {
|
|
||||||
this.loadingDefs = true;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASEURL}api/valuesetdef?limit=1000`, {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("HTTP error");
|
|
||||||
const data = await res.json();
|
|
||||||
this.defsList = data.data || [];
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch defs list:', err);
|
|
||||||
this.defsList = [];
|
|
||||||
} finally {
|
|
||||||
this.loadingDefs = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showForm() {
|
|
||||||
this.isEditing = false;
|
|
||||||
this.form = {
|
|
||||||
VID: null,
|
|
||||||
VSetID: this.selectedDef?.VSetID || "",
|
|
||||||
VOrder: 0,
|
|
||||||
VValue: "",
|
|
||||||
VDesc: "",
|
|
||||||
SiteID: 1
|
|
||||||
};
|
|
||||||
this.errors = {};
|
|
||||||
|
|
||||||
// If no selectedDef, we need to load all defs for dropdown
|
|
||||||
if (!this.selectedDef && this.defsList.length === 0) {
|
|
||||||
this.fetchDefsList();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showModal = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async editValue(id) {
|
|
||||||
this.isEditing = true;
|
|
||||||
this.errors = {};
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASEURL}api/valueset/items/${id}`, {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.data) {
|
|
||||||
this.form = { ...this.form, ...data.data };
|
|
||||||
this.showModal = true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.showToast('Failed to load item data', 'error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
validate() {
|
|
||||||
const e = {};
|
|
||||||
if (!this.form.VValue?.trim()) e.VValue = "Value is required";
|
|
||||||
if (!this.form.VSetID) e.VSetID = "Category is required";
|
|
||||||
this.errors = e;
|
|
||||||
return Object.keys(e).length === 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
closeModal() {
|
|
||||||
this.showModal = false;
|
|
||||||
this.errors = {};
|
|
||||||
},
|
|
||||||
|
|
||||||
async save() {
|
|
||||||
if (!this.validate()) return;
|
|
||||||
|
|
||||||
this.saving = true;
|
|
||||||
try {
|
|
||||||
const method = this.isEditing ? 'PUT' : 'POST';
|
|
||||||
const url = this.isEditing ? `${BASEURL}api/valueset/items/${this.form.VID}` : `${BASEURL}api/valueset/items`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: method,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(this.form),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
this.closeModal();
|
|
||||||
await this.fetchList(1);
|
|
||||||
this.showToast(this.isEditing ? 'Item updated successfully' : 'Item created successfully', 'success');
|
|
||||||
} else {
|
|
||||||
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
|
|
||||||
this.errors = { general: errorData.message || 'Failed to save' };
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Save failed:', err);
|
|
||||||
this.errors = { general: err.message || 'An error occurred while saving' };
|
|
||||||
this.showToast('Failed to save item', 'error');
|
|
||||||
} finally {
|
|
||||||
this.saving = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmDelete(v) {
|
|
||||||
this.deleteTarget = v;
|
|
||||||
this.showDeleteModal = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteValue() {
|
|
||||||
if (!this.deleteTarget) return;
|
|
||||||
|
|
||||||
this.deleting = true;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASEURL}api/valueset/items/${this.deleteTarget.VID}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
this.showDeleteModal = false;
|
|
||||||
await this.fetchList(1);
|
|
||||||
this.showToast('Item deleted successfully', 'success');
|
|
||||||
} else {
|
|
||||||
this.showToast('Failed to delete item', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Delete failed:', err);
|
|
||||||
this.showToast('Failed to delete item', 'error');
|
|
||||||
} finally {
|
|
||||||
this.deleting = false;
|
|
||||||
this.deleteTarget = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showToast(message, type = 'info') {
|
|
||||||
if (this.$root && this.$root.showToast) {
|
|
||||||
this.$root.showToast(message, type);
|
|
||||||
} else {
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@ -1,678 +0,0 @@
|
|||||||
<?= $this->extend("v2/layout/main_layout"); ?>
|
|
||||||
|
|
||||||
<?= $this->section("content") ?>
|
|
||||||
<div x-data="valueSetManager()" x-init="init()">
|
|
||||||
|
|
||||||
<!-- Page Header -->
|
|
||||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-800 flex items-center justify-center shadow-lg">
|
|
||||||
<i class="fa-solid fa-layer-group text-2xl text-white"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Value Set Manager</h2>
|
|
||||||
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage value set categories and their items</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Two Column Layout with Independent Scrolling -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
|
|
||||||
<!-- LEFT PANEL: ValueSetDef List -->
|
|
||||||
<div class="card overflow-hidden flex flex-col" style="height: calc(100vh - 280px); min-height: 400px;">
|
|
||||||
<!-- Left Panel Header -->
|
|
||||||
<div class="p-4 border-b flex items-center justify-between" style="border-color: rgb(var(--color-border));">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center" style="background: rgb(var(--color-primary));">
|
|
||||||
<i class="fa-solid fa-layer-group text-white"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold" style="color: rgb(var(--color-text));">Categories</h3>
|
|
||||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">Value Set Definitions</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary btn-sm" @click="showDefForm()">
|
|
||||||
<i class="fa-solid fa-plus mr-1"></i> Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search Bar -->
|
|
||||||
<div class="p-3 border-b" style="border-color: rgb(var(--color-border));">
|
|
||||||
<div class="relative">
|
|
||||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-gray-400" style="z-index: 10;"></i>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search categories..."
|
|
||||||
class="input input-sm w-full input-with-icon"
|
|
||||||
x-model="defKeyword"
|
|
||||||
@keyup.enter="fetchDefs()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div x-show="defLoading" class="p-12 text-center" x-cloak>
|
|
||||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
|
||||||
<p style="color: rgb(var(--color-text-muted));">Loading categories...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Def List Table -->
|
|
||||||
<div class="overflow-y-auto flex-1" x-show="!defLoading" x-cloak>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="w-16">ID</th>
|
|
||||||
<th>Category Name</th>
|
|
||||||
<th class="w-20 text-center">Items</th>
|
|
||||||
<th class="w-24 text-center">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template x-if="!defList || defList.length === 0">
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" class="text-center py-12">
|
|
||||||
<div class="flex flex-col items-center gap-2" style="color: rgb(var(--color-text-muted));">
|
|
||||||
<i class="fa-solid fa-folder-open text-4xl opacity-40"></i>
|
|
||||||
<p class="text-sm">No categories found</p>
|
|
||||||
<button class="btn btn-primary btn-sm mt-2" @click="showDefForm()">
|
|
||||||
<i class="fa-solid fa-plus mr-1"></i> Add Category
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-for="def in defList" :key="def.VSetID">
|
|
||||||
<tr
|
|
||||||
class="hover:bg-opacity-50 cursor-pointer transition-colors"
|
|
||||||
:class="selectedDef?.VSetID === def.VSetID ? 'bg-primary/10' : ''"
|
|
||||||
@click="selectDef(def)"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<span class="badge badge-ghost font-mono text-xs" x-text="def.VSetID || '-'"></span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="def.VSName || '-'"></div>
|
|
||||||
<div class="text-xs opacity-50" x-text="def.VSDesc || ''"></div>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<span class="badge badge-sm" x-text="(def.ItemCount || 0) + ' items'"></span>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<div class="flex items-center justify-center gap-1" @click.stop>
|
|
||||||
<button class="btn btn-ghost btn-sm btn-square" @click="editDef(def.VSetID)" title="Edit">
|
|
||||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDeleteDef(def)" title="Delete">
|
|
||||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Left Panel Footer -->
|
|
||||||
<div class="p-3 flex items-center justify-between text-xs" style="border-top: 1px solid rgb(var(--color-border));" x-show="defList && defList.length > 0">
|
|
||||||
<span style="color: rgb(var(--color-text-muted));" x-text="defList.length + ' categories'"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RIGHT PANEL: ValueSet Items -->
|
|
||||||
<div class="card overflow-hidden flex flex-col" style="height: calc(100vh - 280px); min-height: 400px;">
|
|
||||||
<!-- Right Panel Header -->
|
|
||||||
<div class="p-4 border-b flex items-center justify-between" style="border-color: rgb(var(--color-border));">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center" style="background: rgb(var(--color-secondary));">
|
|
||||||
<i class="fa-solid fa-list-ul text-white"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold" style="color: rgb(var(--color-text));">Items</h3>
|
|
||||||
<p class="text-xs" style="color: rgb(var(--color-text-muted));">
|
|
||||||
<template x-if="selectedDef">
|
|
||||||
<span x-text="selectedDef.VSName + ' Items'"></span>
|
|
||||||
</template>
|
|
||||||
<template x-if="!selectedDef">
|
|
||||||
<span>Select a category to view items</span>
|
|
||||||
</template>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
@click="showValueForm()"
|
|
||||||
:disabled="!selectedDef"
|
|
||||||
>
|
|
||||||
<i class="fa-solid fa-plus mr-1"></i> Add Item
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search Bar (Right Panel) -->
|
|
||||||
<div class="p-3 border-b" style="border-color: rgb(var(--color-border));" x-show="selectedDef">
|
|
||||||
<div class="relative">
|
|
||||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-gray-400" style="z-index: 10;"></i>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Filter items..."
|
|
||||||
class="input input-sm w-full input-with-icon"
|
|
||||||
x-model="valueKeyword"
|
|
||||||
@keyup.enter="fetchValues()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Empty State - No Selection -->
|
|
||||||
<div x-show="!selectedDef" class="p-16 text-center" x-cloak>
|
|
||||||
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
|
||||||
<i class="fa-solid fa-hand-pointer text-5xl opacity-30"></i>
|
|
||||||
<p class="text-lg font-medium">Select a category</p>
|
|
||||||
<p class="text-sm opacity-60">Click on a category from the left panel to view and manage its items</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div x-show="valueLoading && selectedDef" class="p-12 text-center" x-cloak>
|
|
||||||
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
|
||||||
<p style="color: rgb(var(--color-text-muted));">Loading items...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Value List Table -->
|
|
||||||
<div class="overflow-y-auto flex-1" x-show="!valueLoading && selectedDef" x-cloak>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="w-16">ID</th>
|
|
||||||
<th>Value</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th class="w-16 text-center">Order</th>
|
|
||||||
<th class="w-20 text-center">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<template x-if="!valueList || valueList.length === 0">
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" class="text-center py-12">
|
|
||||||
<div class="flex flex-col items-center gap-2" style="color: rgb(var(--color-text-muted));">
|
|
||||||
<i class="fa-solid fa-inbox text-4xl opacity-40"></i>
|
|
||||||
<p class="text-sm">No items found</p>
|
|
||||||
<button class="btn btn-primary btn-sm mt-2" @click="showValueForm()">
|
|
||||||
<i class="fa-solid fa-plus mr-1"></i> Add First Item
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template x-for="value in valueList" :key="value.VID">
|
|
||||||
<tr class="hover:bg-opacity-50">
|
|
||||||
<td>
|
|
||||||
<span class="badge badge-ghost font-mono text-xs" x-text="value.VID || '-'"></span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="value.VValue || '-'"></div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="text-sm opacity-70" x-text="value.VDesc || '-'"></span>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<span class="font-mono text-sm" x-text="value.VOrder || 0"></span>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<div class="flex items-center justify-center gap-1">
|
|
||||||
<button class="btn btn-ghost btn-sm btn-square" @click="editValue(value.VID)" title="Edit">
|
|
||||||
<i class="fa-solid fa-pen text-sky-500"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDeleteValue(value)" title="Delete">
|
|
||||||
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</template>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Panel Footer -->
|
|
||||||
<div class="p-3 flex items-center justify-between text-xs" style="border-top: 1px solid rgb(var(--color-border));" x-show="valueList && valueList.length > 0 && selectedDef">
|
|
||||||
<span style="color: rgb(var(--color-text-muted));" x-text="valueList.length + ' items'"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Include Definition Form Dialog -->
|
|
||||||
<?= $this->include('v2/master/valuesets/valuesetdef_dialog') ?>
|
|
||||||
|
|
||||||
<!-- Include Value Form Dialog -->
|
|
||||||
<?= $this->include('v2/master/valuesets/valueset_dialog') ?>
|
|
||||||
|
|
||||||
<!-- Delete Category Confirmation Modal -->
|
|
||||||
<div
|
|
||||||
x-show="showDeleteDefModal"
|
|
||||||
x-cloak
|
|
||||||
class="modal-overlay"
|
|
||||||
@click.self="showDeleteDefModal = false"
|
|
||||||
>
|
|
||||||
<div class="modal-content p-6 max-w-md">
|
|
||||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
|
||||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
|
||||||
Confirm Delete
|
|
||||||
</h3>
|
|
||||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
|
||||||
Are you sure you want to delete category <strong x-text="deleteDefTarget?.VSName"></strong>?
|
|
||||||
This will also delete all items in this category and cannot be undone.
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button class="btn btn-ghost flex-1" @click="showDeleteDefModal = false">Cancel</button>
|
|
||||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteDef()" :disabled="deletingDef">
|
|
||||||
<span x-show="deletingDef" class="spinner spinner-sm"></span>
|
|
||||||
<span x-show="!deletingDef">Delete</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Delete Value Confirmation Modal -->
|
|
||||||
<div
|
|
||||||
x-show="showDeleteValueModal"
|
|
||||||
x-cloak
|
|
||||||
class="modal-overlay"
|
|
||||||
@click.self="showDeleteValueModal = false"
|
|
||||||
>
|
|
||||||
<div class="modal-content p-6 max-w-md">
|
|
||||||
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
|
||||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
|
||||||
Confirm Delete
|
|
||||||
</h3>
|
|
||||||
<p class="mb-6" style="color: rgb(var(--color-text));">
|
|
||||||
Are you sure you want to delete item <strong x-text="deleteValueTarget?.VValue"></strong>?
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button class="btn btn-ghost flex-1" @click="showDeleteValueModal = false">Cancel</button>
|
|
||||||
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteValue()" :disabled="deletingValue">
|
|
||||||
<span x-show="deletingValue" class="spinner spinner-sm"></span>
|
|
||||||
<span x-show="!deletingValue">Delete</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<?= $this->endSection() ?>
|
|
||||||
|
|
||||||
<?= $this->section("script") ?>
|
|
||||||
<script>
|
|
||||||
function valueSetManager() {
|
|
||||||
return {
|
|
||||||
// State - Definitions
|
|
||||||
defLoading: false,
|
|
||||||
defList: [],
|
|
||||||
defKeyword: "",
|
|
||||||
|
|
||||||
// State - Values
|
|
||||||
valueLoading: false,
|
|
||||||
valueList: [],
|
|
||||||
valueKeyword: "",
|
|
||||||
selectedDef: null,
|
|
||||||
|
|
||||||
// Definition Form
|
|
||||||
showDefModal: false,
|
|
||||||
isEditingDef: false,
|
|
||||||
savingDef: false,
|
|
||||||
defErrors: {},
|
|
||||||
defForm: {
|
|
||||||
VSetID: null,
|
|
||||||
VSName: "",
|
|
||||||
VSDesc: "",
|
|
||||||
SiteID: 1
|
|
||||||
},
|
|
||||||
|
|
||||||
// Value Form
|
|
||||||
showValueModal: false,
|
|
||||||
isEditingValue: false,
|
|
||||||
savingValue: false,
|
|
||||||
valueErrors: {},
|
|
||||||
valueForm: {
|
|
||||||
VID: null,
|
|
||||||
VSetID: "",
|
|
||||||
VOrder: 0,
|
|
||||||
VValue: "",
|
|
||||||
VDesc: "",
|
|
||||||
SiteID: 1
|
|
||||||
},
|
|
||||||
|
|
||||||
// Delete Definition
|
|
||||||
showDeleteDefModal: false,
|
|
||||||
deleteDefTarget: null,
|
|
||||||
deletingDef: false,
|
|
||||||
|
|
||||||
// Delete Value
|
|
||||||
showDeleteValueModal: false,
|
|
||||||
deleteValueTarget: null,
|
|
||||||
deletingValue: false,
|
|
||||||
|
|
||||||
// Dropdown data
|
|
||||||
defsList: [],
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
async init() {
|
|
||||||
await this.fetchDefs();
|
|
||||||
},
|
|
||||||
|
|
||||||
// ==================== DEFINITION METHODS ====================
|
|
||||||
|
|
||||||
async fetchDefs() {
|
|
||||||
this.defLoading = true;
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (this.defKeyword) params.append('search', this.defKeyword);
|
|
||||||
|
|
||||||
const res = await fetch(`${BASEURL}api/valuesetdef?${params}`, {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("HTTP error");
|
|
||||||
const data = await res.json();
|
|
||||||
this.defList = data.data || [];
|
|
||||||
|
|
||||||
if (this.selectedDef) {
|
|
||||||
const updated = this.defList.find(d => d.VSetID === this.selectedDef.VSetID);
|
|
||||||
if (updated) {
|
|
||||||
this.selectedDef = updated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.defList = [];
|
|
||||||
this.showToast('Failed to load categories', 'error');
|
|
||||||
} finally {
|
|
||||||
this.defLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showDefForm() {
|
|
||||||
this.isEditingDef = false;
|
|
||||||
this.defForm = {
|
|
||||||
VSetID: null,
|
|
||||||
VSName: "",
|
|
||||||
VSDesc: "",
|
|
||||||
SiteID: 1
|
|
||||||
};
|
|
||||||
this.defErrors = {};
|
|
||||||
this.showDefModal = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async editDef(id) {
|
|
||||||
this.isEditingDef = true;
|
|
||||||
this.defErrors = {};
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASEURL}api/valuesetdef/${id}`, {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.data) {
|
|
||||||
this.defForm = { ...this.defForm, ...data.data };
|
|
||||||
this.showDefModal = true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.showToast('Failed to load category data', 'error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
validateDef() {
|
|
||||||
const e = {};
|
|
||||||
if (!this.defForm.VSName?.trim()) e.VSName = "Category name is required";
|
|
||||||
this.defErrors = e;
|
|
||||||
return Object.keys(e).length === 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
closeDefModal() {
|
|
||||||
this.showDefModal = false;
|
|
||||||
this.defErrors = {};
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveDef() {
|
|
||||||
if (!this.validateDef()) return;
|
|
||||||
|
|
||||||
this.savingDef = true;
|
|
||||||
try {
|
|
||||||
const method = this.isEditingDef ? 'PUT' : 'POST';
|
|
||||||
const url = this.isEditingDef ? `${BASEURL}api/valuesetdef/${this.defForm.VSetID}` : `${BASEURL}api/valuesetdef`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: method,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(this.defForm),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
this.closeDefModal();
|
|
||||||
await this.fetchDefs();
|
|
||||||
this.showToast(this.isEditingDef ? 'Category updated successfully' : 'Category created successfully', 'success');
|
|
||||||
} else {
|
|
||||||
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
|
|
||||||
this.defErrors = { general: errorData.message || 'Failed to save' };
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.defErrors = { general: 'Failed to save category' };
|
|
||||||
this.showToast('Failed to save category', 'error');
|
|
||||||
} finally {
|
|
||||||
this.savingDef = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmDeleteDef(def) {
|
|
||||||
this.deleteDefTarget = def;
|
|
||||||
this.showDeleteDefModal = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteDef() {
|
|
||||||
if (!this.deleteDefTarget) return;
|
|
||||||
|
|
||||||
this.deletingDef = true;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASEURL}api/valuesetdef/${this.deleteDefTarget.VSetID}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
this.showDeleteDefModal = false;
|
|
||||||
if (this.selectedDef?.VSetID === this.deleteDefTarget.VSetID) {
|
|
||||||
this.selectedDef = null;
|
|
||||||
this.valueList = [];
|
|
||||||
}
|
|
||||||
await this.fetchDefs();
|
|
||||||
this.showToast('Category deleted successfully', 'success');
|
|
||||||
} else {
|
|
||||||
this.showToast('Failed to delete category', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.showToast('Failed to delete category', 'error');
|
|
||||||
} finally {
|
|
||||||
this.deletingDef = false;
|
|
||||||
this.deleteDefTarget = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ==================== VALUE METHODS ====================
|
|
||||||
|
|
||||||
selectDef(def) {
|
|
||||||
this.selectedDef = def;
|
|
||||||
this.fetchValues();
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchValues() {
|
|
||||||
if (!this.selectedDef) return;
|
|
||||||
|
|
||||||
this.valueLoading = true;
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('VSetID', this.selectedDef.VSetID);
|
|
||||||
if (this.valueKeyword) params.append('search', this.valueKeyword);
|
|
||||||
|
|
||||||
const res = await fetch(`${BASEURL}api/valueset/items?${params}`, {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("HTTP error");
|
|
||||||
const data = await res.json();
|
|
||||||
this.valueList = data.data || [];
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.valueList = [];
|
|
||||||
this.showToast('Failed to load items', 'error');
|
|
||||||
} finally {
|
|
||||||
this.valueLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchDefsList() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASEURL}api/valuesetdef?limit=1000`, {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("HTTP error");
|
|
||||||
const data = await res.json();
|
|
||||||
this.defsList = data.data || [];
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch defs list:', err);
|
|
||||||
this.defsList = [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showValueForm() {
|
|
||||||
if (!this.selectedDef) {
|
|
||||||
this.showToast('Please select a category first', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isEditingValue = false;
|
|
||||||
this.valueForm = {
|
|
||||||
VID: null,
|
|
||||||
VSetID: this.selectedDef.VSetID,
|
|
||||||
VOrder: 0,
|
|
||||||
VValue: "",
|
|
||||||
VDesc: "",
|
|
||||||
SiteID: 1
|
|
||||||
};
|
|
||||||
this.valueErrors = {};
|
|
||||||
this.showValueModal = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async editValue(id) {
|
|
||||||
this.isEditingValue = true;
|
|
||||||
this.valueErrors = {};
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASEURL}api/valueset/items/${id}`, {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.data) {
|
|
||||||
this.valueForm = { ...this.valueForm, ...data.data };
|
|
||||||
this.showValueModal = true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.showToast('Failed to load item data', 'error');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
validateValue() {
|
|
||||||
const e = {};
|
|
||||||
if (!this.valueForm.VValue?.trim()) e.VValue = "Value is required";
|
|
||||||
if (!this.valueForm.VSetID) e.VSetID = "Category is required";
|
|
||||||
this.valueErrors = e;
|
|
||||||
return Object.keys(e).length === 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
closeValueModal() {
|
|
||||||
this.showValueModal = false;
|
|
||||||
this.valueErrors = {};
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveValue() {
|
|
||||||
if (!this.validateValue()) return;
|
|
||||||
|
|
||||||
this.savingValue = true;
|
|
||||||
try {
|
|
||||||
const method = this.isEditingValue ? 'PUT' : 'POST';
|
|
||||||
const url = this.isEditingValue ? `${BASEURL}api/valueset/items/${this.valueForm.VID}` : `${BASEURL}api/valueset/items`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: method,
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(this.valueForm),
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
this.closeValueModal();
|
|
||||||
await this.fetchValues();
|
|
||||||
await this.fetchDefs();
|
|
||||||
this.showToast(this.isEditingValue ? 'Item updated successfully' : 'Item created successfully', 'success');
|
|
||||||
} else {
|
|
||||||
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
|
|
||||||
this.valueErrors = { general: errorData.message || 'Failed to save' };
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.valueErrors = { general: 'Failed to save item' };
|
|
||||||
this.showToast('Failed to save item', 'error');
|
|
||||||
} finally {
|
|
||||||
this.savingValue = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmDeleteValue(value) {
|
|
||||||
this.deleteValueTarget = value;
|
|
||||||
this.showDeleteValueModal = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteValue() {
|
|
||||||
if (!this.deleteValueTarget) return;
|
|
||||||
|
|
||||||
this.deletingValue = true;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASEURL}api/valueset/items/${this.deleteValueTarget.VID}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
this.showDeleteValueModal = false;
|
|
||||||
await this.fetchValues();
|
|
||||||
await this.fetchDefs();
|
|
||||||
this.showToast('Item deleted successfully', 'success');
|
|
||||||
} else {
|
|
||||||
this.showToast('Failed to delete item', 'error');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.showToast('Failed to delete item', 'error');
|
|
||||||
} finally {
|
|
||||||
this.deletingValue = false;
|
|
||||||
this.deleteValueTarget = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// ==================== UTILITIES ====================
|
|
||||||
|
|
||||||
showToast(message, type = 'info') {
|
|
||||||
if (this.$root && this.$root.showToast) {
|
|
||||||
this.$root.showToast(message, type);
|
|
||||||
} else {
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<?= $this->endSection() ?>
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
<!-- Value Set Item Form Modal -->
|
<!-- Result Value Set Item Form Modal -->
|
||||||
<div
|
<div
|
||||||
x-show="showModal"
|
x-show="showModal"
|
||||||
x-cloak
|
x-cloak
|
||||||
@ -21,7 +21,6 @@
|
|||||||
x-transition:leave-start="opacity-100 transform scale-100"
|
x-transition:leave-start="opacity-100 transform scale-100"
|
||||||
x-transition:leave-end="opacity-0 transform scale-95"
|
x-transition:leave-end="opacity-0 transform scale-95"
|
||||||
>
|
>
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||||
<i class="fa-solid fa-list-plus" style="color: rgb(var(--color-primary));"></i>
|
<i class="fa-solid fa-list-plus" style="color: rgb(var(--color-primary));"></i>
|
||||||
@ -32,10 +31,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form -->
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|
||||||
<!-- General Error -->
|
|
||||||
<div x-show="errors.general" class="p-4 rounded-lg bg-rose-50 border border-rose-200" style="display: none;">
|
<div x-show="errors.general" class="p-4 rounded-lg bg-rose-50 border border-rose-200" style="display: none;">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<i class="fa-solid fa-exclamation-triangle text-rose-500"></i>
|
<i class="fa-solid fa-exclamation-triangle text-rose-500"></i>
|
||||||
@ -43,8 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Category Selection (only show if no selectedDef) -->
|
<div class="space-y-4">
|
||||||
<div x-show="!selectedDef" class="space-y-4">
|
|
||||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Category Assignment</h4>
|
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Category Assignment</h4>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -63,7 +59,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Basic Information Section -->
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Item Details</h4>
|
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Item Details</h4>
|
||||||
|
|
||||||
@ -109,7 +104,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- System Information Section -->
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">System Information</h4>
|
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">System Information</h4>
|
||||||
|
|
||||||
@ -143,7 +137,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||||
322
app/Views/v2/result/valueset/resultvalueset_index.php
Normal file
322
app/Views/v2/result/valueset/resultvalueset_index.php
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||||
|
|
||||||
|
<?= $this->section("content") ?>
|
||||||
|
<div x-data="resultValueSet()" x-init="init()">
|
||||||
|
|
||||||
|
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-amber-600 to-orange-800 flex items-center justify-center shadow-lg">
|
||||||
|
<i class="fa-solid fa-list-ul text-2xl text-white"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Result Valuesets</h2>
|
||||||
|
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage valueset items from database</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-6">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||||
|
<div class="flex w-full sm:w-auto gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search valuesets..."
|
||||||
|
class="input flex-1 sm:w-80"
|
||||||
|
x-model="keyword"
|
||||||
|
@keyup.enter="fetchList()"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-primary" @click="fetchList()">
|
||||||
|
<i class="fa-solid fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i>
|
||||||
|
Add Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card overflow-hidden">
|
||||||
|
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||||
|
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||||
|
<p style="color: rgb(var(--color-text-muted));">Loading valuesets...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto" x-show="!loading">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-16">ID</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="w-20 text-center">Order</th>
|
||||||
|
<th class="w-24 text-center">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-if="!list || list.length === 0">
|
||||||
|
<tr>
|
||||||
|
<td colspan="6" class="text-center py-12">
|
||||||
|
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||||
|
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
|
||||||
|
<p class="text-lg">No valuesets found</p>
|
||||||
|
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||||
|
<i class="fa-solid fa-plus mr-1"></i> Add First Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-for="item in list" :key="item.VID">
|
||||||
|
<tr class="hover:bg-opacity-50">
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-ghost font-mono text-xs" x-text="item.VID || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="item.VCategoryName || '-'"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="item.VValue || '-'"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-sm opacity-70" x-text="item.VDesc || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="font-mono text-sm" x-text="item.VOrder || 0"></span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
<button class="btn btn-ghost btn-sm btn-square" @click="editItem(item.VID)" title="Edit">
|
||||||
|
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(item)" title="Delete">
|
||||||
|
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?= $this->include('v2/result/valueset/resultvalueset_dialog') ?>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-show="showDeleteModal"
|
||||||
|
x-cloak
|
||||||
|
class="modal-overlay"
|
||||||
|
@click.self="showDeleteModal = false"
|
||||||
|
>
|
||||||
|
<div class="modal-content p-6 max-w-md">
|
||||||
|
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||||
|
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||||
|
Confirm Delete
|
||||||
|
</h3>
|
||||||
|
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||||
|
Are you sure you want to delete item <strong x-text="deleteTarget?.VValue"></strong>?
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||||
|
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteItem()" :disabled="deleting">
|
||||||
|
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||||
|
<span x-show="!deleting">Delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section("script") ?>
|
||||||
|
<script>
|
||||||
|
function resultValueSet() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
list: [],
|
||||||
|
keyword: "",
|
||||||
|
defsList: [],
|
||||||
|
|
||||||
|
showModal: false,
|
||||||
|
isEditing: false,
|
||||||
|
saving: false,
|
||||||
|
errors: {},
|
||||||
|
form: {
|
||||||
|
VID: null,
|
||||||
|
VSetID: "",
|
||||||
|
VOrder: 0,
|
||||||
|
VValue: "",
|
||||||
|
VDesc: "",
|
||||||
|
SiteID: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
showDeleteModal: false,
|
||||||
|
deleteTarget: null,
|
||||||
|
deleting: false,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.fetchList();
|
||||||
|
await this.fetchDefsList();
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchList() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (this.keyword) params.append('search', this.keyword);
|
||||||
|
|
||||||
|
const res = await fetch(`${BASEURL}api/result/valueset?${params}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("HTTP error");
|
||||||
|
const data = await res.json();
|
||||||
|
this.list = data.data || [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.list = [];
|
||||||
|
this.showToast('Failed to load valuesets', 'error');
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchDefsList() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASEURL}api/result/valuesetdef?limit=1000`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("HTTP error");
|
||||||
|
const data = await res.json();
|
||||||
|
this.defsList = data.data || [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch defs list:', err);
|
||||||
|
this.defsList = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showForm() {
|
||||||
|
this.isEditing = false;
|
||||||
|
this.form = {
|
||||||
|
VID: null,
|
||||||
|
VSetID: "",
|
||||||
|
VOrder: 0,
|
||||||
|
VValue: "",
|
||||||
|
VDesc: "",
|
||||||
|
SiteID: 1
|
||||||
|
};
|
||||||
|
this.errors = {};
|
||||||
|
this.showModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async editItem(id) {
|
||||||
|
this.isEditing = true;
|
||||||
|
this.errors = {};
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASEURL}api/result/valueset/${id}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.data) {
|
||||||
|
this.form = { ...this.form, ...data.data };
|
||||||
|
this.showModal = true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.showToast('Failed to load item data', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
const e = {};
|
||||||
|
if (!this.form.VValue?.trim()) e.VValue = "Value is required";
|
||||||
|
if (!this.form.VSetID) e.VSetID = "Category is required";
|
||||||
|
this.errors = e;
|
||||||
|
return Object.keys(e).length === 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.showModal = false;
|
||||||
|
this.errors = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
if (!this.validate()) return;
|
||||||
|
|
||||||
|
this.saving = true;
|
||||||
|
try {
|
||||||
|
const method = this.isEditing ? 'PUT' : 'POST';
|
||||||
|
const url = this.isEditing ? `${BASEURL}api/result/valueset/${this.form.VID}` : `${BASEURL}api/result/valueset`;
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.closeModal();
|
||||||
|
await this.fetchList();
|
||||||
|
this.showToast(this.isEditing ? 'Item updated successfully' : 'Item created successfully', 'success');
|
||||||
|
} else {
|
||||||
|
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
|
||||||
|
this.errors = { general: errorData.message || 'Failed to save' };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.errors = { general: 'Failed to save item' };
|
||||||
|
this.showToast('Failed to save item', 'error');
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDelete(item) {
|
||||||
|
this.deleteTarget = item;
|
||||||
|
this.showDeleteModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteItem() {
|
||||||
|
if (!this.deleteTarget) return;
|
||||||
|
|
||||||
|
this.deleting = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASEURL}api/result/valueset/${this.deleteTarget.VID}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.showDeleteModal = false;
|
||||||
|
await this.fetchList();
|
||||||
|
this.showToast('Item deleted successfully', 'success');
|
||||||
|
} else {
|
||||||
|
this.showToast('Failed to delete item', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.showToast('Failed to delete item', 'error');
|
||||||
|
} finally {
|
||||||
|
this.deleting = false;
|
||||||
|
this.deleteTarget = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showToast(message, type = 'info') {
|
||||||
|
if (this.$root && this.$root.showToast) {
|
||||||
|
this.$root.showToast(message, type);
|
||||||
|
} else {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
<!-- Value Set Definition Form Modal -->
|
<!-- Result Value Set Definition Form Modal -->
|
||||||
<div
|
<div
|
||||||
x-show="showModal"
|
x-show="showModal"
|
||||||
x-cloak
|
x-cloak
|
||||||
@ -21,7 +21,6 @@
|
|||||||
x-transition:leave-start="opacity-100 transform scale-100"
|
x-transition:leave-start="opacity-100 transform scale-100"
|
||||||
x-transition:leave-end="opacity-0 transform scale-95"
|
x-transition:leave-end="opacity-0 transform scale-95"
|
||||||
>
|
>
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
<h3 class="font-bold text-xl flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||||
<i class="fa-solid fa-layer-group-plus" style="color: rgb(var(--color-primary));"></i>
|
<i class="fa-solid fa-layer-group-plus" style="color: rgb(var(--color-primary));"></i>
|
||||||
@ -32,10 +31,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form -->
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
|
|
||||||
<!-- General Error -->
|
|
||||||
<div x-show="errors.general" class="p-4 rounded-lg bg-rose-50 border border-rose-200" style="display: none;">
|
<div x-show="errors.general" class="p-4 rounded-lg bg-rose-50 border border-rose-200" style="display: none;">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<i class="fa-solid fa-exclamation-triangle text-rose-500"></i>
|
<i class="fa-solid fa-exclamation-triangle text-rose-500"></i>
|
||||||
@ -43,7 +40,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Basic Information Section -->
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Basic Information</h4>
|
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">Basic Information</h4>
|
||||||
|
|
||||||
@ -75,7 +71,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Additional Info Section -->
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">System Information</h4>
|
<h4 class="font-semibold text-sm uppercase tracking-wider opacity-60" style="color: rgb(var(--color-text));">System Information</h4>
|
||||||
|
|
||||||
@ -109,7 +104,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
<div class="flex gap-3 mt-8 pt-6" style="border-top: 1px solid rgb(var(--color-border));">
|
||||||
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
<button class="btn btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||||
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
<button class="btn btn-primary flex-1" @click="save()" :disabled="saving">
|
||||||
298
app/Views/v2/result/valuesetdef/resultvaluesetdef_index.php
Normal file
298
app/Views/v2/result/valuesetdef/resultvaluesetdef_index.php
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||||
|
|
||||||
|
<?= $this->section("content") ?>
|
||||||
|
<div x-data="resultValueSetDef()" x-init="init()">
|
||||||
|
|
||||||
|
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-rose-600 to-pink-800 flex items-center justify-center shadow-lg">
|
||||||
|
<i class="fa-solid fa-layer-group text-2xl text-white"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Valueset Definitions</h2>
|
||||||
|
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage valueset categories and definitions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-6">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
|
||||||
|
<div class="flex w-full sm:w-auto gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search definitions..."
|
||||||
|
class="input flex-1 sm:w-80"
|
||||||
|
x-model="keyword"
|
||||||
|
@keyup.enter="fetchList()"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-primary" @click="fetchList()">
|
||||||
|
<i class="fa-solid fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i>
|
||||||
|
Add Category
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card overflow-hidden">
|
||||||
|
<div x-show="loading" class="p-12 text-center" x-cloak>
|
||||||
|
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||||
|
<p style="color: rgb(var(--color-text-muted));">Loading definitions...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto" x-show="!loading">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="w-16">ID</th>
|
||||||
|
<th>Category Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="w-20 text-center">Items</th>
|
||||||
|
<th class="w-24 text-center">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-if="!list || list.length === 0">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center py-12">
|
||||||
|
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
|
||||||
|
<i class="fa-solid fa-folder-open text-5xl opacity-40"></i>
|
||||||
|
<p class="text-lg">No definitions found</p>
|
||||||
|
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
|
||||||
|
<i class="fa-solid fa-plus mr-1"></i> Add First Category
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-for="def in list" :key="def.VSetID">
|
||||||
|
<tr class="hover:bg-opacity-50">
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-ghost font-mono text-xs" x-text="def.VSetID || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="def.VSName || '-'"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-sm opacity-70" x-text="def.VSDesc || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge badge-sm" x-text="(def.ItemCount || 0) + ' items'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
<button class="btn btn-ghost btn-sm btn-square" @click="editItem(def.VSetID)" title="Edit">
|
||||||
|
<i class="fa-solid fa-pen text-sky-500"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(def)" title="Delete">
|
||||||
|
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?= $this->include('v2/result/valuesetdef/resultvaluesetdef_dialog') ?>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-show="showDeleteModal"
|
||||||
|
x-cloak
|
||||||
|
class="modal-overlay"
|
||||||
|
@click.self="showDeleteModal = false"
|
||||||
|
>
|
||||||
|
<div class="modal-content p-6 max-w-md">
|
||||||
|
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
|
||||||
|
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||||
|
Confirm Delete
|
||||||
|
</h3>
|
||||||
|
<p class="mb-6" style="color: rgb(var(--color-text));">
|
||||||
|
Are you sure you want to delete category <strong x-text="deleteTarget?.VSName"></strong>?
|
||||||
|
This will also delete all items in this category and cannot be undone.
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
|
||||||
|
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteItem()" :disabled="deleting">
|
||||||
|
<span x-show="deleting" class="spinner spinner-sm"></span>
|
||||||
|
<span x-show="!deleting">Delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section("script") ?>
|
||||||
|
<script>
|
||||||
|
function resultValueSetDef() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
list: [],
|
||||||
|
keyword: "",
|
||||||
|
|
||||||
|
showModal: false,
|
||||||
|
isEditing: false,
|
||||||
|
saving: false,
|
||||||
|
errors: {},
|
||||||
|
form: {
|
||||||
|
VSetID: null,
|
||||||
|
VSName: "",
|
||||||
|
VSDesc: "",
|
||||||
|
SiteID: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
showDeleteModal: false,
|
||||||
|
deleteTarget: null,
|
||||||
|
deleting: false,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchList() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (this.keyword) params.append('search', this.keyword);
|
||||||
|
|
||||||
|
const res = await fetch(`${BASEURL}api/result/valuesetdef?${params}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("HTTP error");
|
||||||
|
const data = await res.json();
|
||||||
|
this.list = data.data || [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.list = [];
|
||||||
|
this.showToast('Failed to load definitions', 'error');
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showForm() {
|
||||||
|
this.isEditing = false;
|
||||||
|
this.form = {
|
||||||
|
VSetID: null,
|
||||||
|
VSName: "",
|
||||||
|
VSDesc: "",
|
||||||
|
SiteID: 1
|
||||||
|
};
|
||||||
|
this.errors = {};
|
||||||
|
this.showModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async editItem(id) {
|
||||||
|
this.isEditing = true;
|
||||||
|
this.errors = {};
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASEURL}api/result/valuesetdef/${id}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.data) {
|
||||||
|
this.form = { ...this.form, ...data.data };
|
||||||
|
this.showModal = true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.showToast('Failed to load category data', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
const e = {};
|
||||||
|
if (!this.form.VSName?.trim()) e.VSName = "Category name is required";
|
||||||
|
this.errors = e;
|
||||||
|
return Object.keys(e).length === 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.showModal = false;
|
||||||
|
this.errors = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
if (!this.validate()) return;
|
||||||
|
|
||||||
|
this.saving = true;
|
||||||
|
try {
|
||||||
|
const method = this.isEditing ? 'PUT' : 'POST';
|
||||||
|
const url = this.isEditing ? `${BASEURL}api/result/valuesetdef/${this.form.VSetID}` : `${BASEURL}api/result/valuesetdef`;
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(this.form),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.closeModal();
|
||||||
|
await this.fetchList();
|
||||||
|
this.showToast(this.isEditing ? 'Category updated successfully' : 'Category created successfully', 'success');
|
||||||
|
} else {
|
||||||
|
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
|
||||||
|
this.errors = { general: errorData.message || 'Failed to save' };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.errors = { general: 'Failed to save category' };
|
||||||
|
this.showToast('Failed to save category', 'error');
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDelete(def) {
|
||||||
|
this.deleteTarget = def;
|
||||||
|
this.showDeleteModal = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteItem() {
|
||||||
|
if (!this.deleteTarget) return;
|
||||||
|
|
||||||
|
this.deleting = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASEURL}api/result/valuesetdef/${this.deleteTarget.VSetID}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.showDeleteModal = false;
|
||||||
|
await this.fetchList();
|
||||||
|
this.showToast('Category deleted successfully', 'success');
|
||||||
|
} else {
|
||||||
|
this.showToast('Failed to delete category', 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.showToast('Failed to delete category', 'error');
|
||||||
|
} finally {
|
||||||
|
this.deleting = false;
|
||||||
|
this.deleteTarget = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showToast(message, type = 'info') {
|
||||||
|
if (this.$root && this.$root.showToast) {
|
||||||
|
this.$root.showToast(message, type);
|
||||||
|
} else {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
371
app/Views/v2/valueset/valueset_index.php
Normal file
371
app/Views/v2/valueset/valueset_index.php
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
<?= $this->extend("v2/layout/main_layout"); ?>
|
||||||
|
|
||||||
|
<?= $this->section("content") ?>
|
||||||
|
<div x-data="valueSetLibrary()" x-init="init()" class="relative">
|
||||||
|
|
||||||
|
<!-- Header & Stats -->
|
||||||
|
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-emerald-600 to-teal-800 flex items-center justify-center shadow-lg">
|
||||||
|
<i class="fa-solid fa-layer-group text-2xl text-white"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Value Set Library</h2>
|
||||||
|
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Browse predefined value sets from library</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-2xl font-bold" style="color: rgb(var(--color-primary));" x-text="Object.keys(list).length"></p>
|
||||||
|
<p class="text-xs uppercase tracking-wider opacity-60">Value Sets</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-px h-8 bg-current opacity-10"></div>
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-2xl font-bold" style="color: rgb(var(--color-secondary));" x-text="totalItems"></p>
|
||||||
|
<p class="text-xs uppercase tracking-wider opacity-60">Total Items</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2-Column Layout: Left Sidebar (Categories) + Right Content (Values) -->
|
||||||
|
<div class="grid grid-cols-12 gap-4" style="height: calc(100vh - 200px);">
|
||||||
|
|
||||||
|
<!-- LEFT PANEL: Value Set Categories -->
|
||||||
|
<div class="col-span-4 xl:col-span-3 flex flex-col card-glass overflow-hidden">
|
||||||
|
<!-- Left Panel Header -->
|
||||||
|
<div class="p-4 border-b shrink-0" style="border-color: rgb(var(--color-border));">
|
||||||
|
<h3 class="font-semibold text-sm uppercase tracking-wider opacity-60 mb-3">Categories</h3>
|
||||||
|
<div class="flex items-center gap-2 bg-base-200 rounded-lg px-3 border border-dashed border-base-content/20">
|
||||||
|
<i class="fa-solid fa-search text-xs opacity-50"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search categories..."
|
||||||
|
class="input input-sm bg-transparent border-0 p-2 flex-1 min-w-0 focus:outline-none"
|
||||||
|
x-model.debounce.300ms="keyword"
|
||||||
|
@input="fetchList()"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
x-show="keyword"
|
||||||
|
@click="keyword = ''; fetchList()"
|
||||||
|
class="btn btn-ghost btn-xs btn-square"
|
||||||
|
x-cloak
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-times text-xs"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Categories List -->
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<!-- Skeleton Loading -->
|
||||||
|
<div x-show="loading && !Object.keys(list).length" class="p-4 space-y-2" x-cloak>
|
||||||
|
<template x-for="i in 5">
|
||||||
|
<div class="p-3 animate-pulse rounded-lg bg-current opacity-5">
|
||||||
|
<div class="h-4 w-3/4 rounded bg-current opacity-10 mb-2"></div>
|
||||||
|
<div class="h-3 w-1/4 rounded bg-current opacity-10"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div x-show="!loading && !Object.keys(list).length" class="p-8 text-center opacity-40" x-cloak>
|
||||||
|
<i class="fa-solid fa-folder-open text-3xl mb-2"></i>
|
||||||
|
<p class="text-sm">No categories found</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category Items -->
|
||||||
|
<div x-show="!loading && Object.keys(list).length > 0" class="p-2" x-cloak>
|
||||||
|
<template x-for="(count, name) in filteredList" :key="name">
|
||||||
|
<div
|
||||||
|
class="p-3 rounded-lg cursor-pointer transition-all mb-1 group"
|
||||||
|
:class="selectedCategory === name ? 'bg-primary/10 border border-primary/30' : 'hover:bg-black/5 border border-transparent'"
|
||||||
|
@click="selectCategory(name)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="truncate flex-1">
|
||||||
|
<div
|
||||||
|
class="font-medium text-sm transition-colors"
|
||||||
|
:class="selectedCategory === name ? 'text-primary' : 'opacity-80'"
|
||||||
|
x-text="formatName(name)"
|
||||||
|
></div>
|
||||||
|
<div class="text-xs opacity-40 font-mono truncate" x-text="name"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 ml-2">
|
||||||
|
<span
|
||||||
|
class="badge badge-sm"
|
||||||
|
:class="selectedCategory === name ? 'badge-primary' : 'badge-ghost'"
|
||||||
|
x-text="count"
|
||||||
|
></span>
|
||||||
|
<i
|
||||||
|
class="fa-solid fa-chevron-right text-xs transition-transform"
|
||||||
|
:class="selectedCategory === name ? 'opacity-100 rotate-0' : 'opacity-0 group-hover:opacity-50'"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Left Panel Footer -->
|
||||||
|
<div class="p-3 border-t text-xs text-center opacity-40" style="border-color: rgb(var(--color-border));">
|
||||||
|
<span x-text="Object.keys(list).length"></span> categories
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT PANEL: Value Set Values -->
|
||||||
|
<div class="col-span-8 xl:col-span-9 flex flex-col card-glass overflow-hidden">
|
||||||
|
<!-- Right Panel Header -->
|
||||||
|
<div class="p-4 border-b shrink-0" style="border-color: rgb(var(--color-border));">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-lg flex items-center justify-center transition-transform"
|
||||||
|
:class="selectedCategory ? 'bg-primary/10' : 'bg-black/5'"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa-solid text-lg"
|
||||||
|
:class="selectedCategory ? 'fa-table-list text-primary' : 'fa-list opacity-20'"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold" style="color: rgb(var(--color-text));" x-text="selectedCategory ? formatName(selectedCategory) : 'Select a Category'"></h3>
|
||||||
|
<p x-show="selectedCategory" class="text-xs font-mono opacity-50" x-text="selectedCategory"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Input (when category selected) -->
|
||||||
|
<div x-show="selectedCategory" class="mt-3" x-transition>
|
||||||
|
<div class="flex items-center gap-2 bg-black/5 rounded-lg px-3 border border-dashed">
|
||||||
|
<i class="fa-solid fa-filter text-xs opacity-40"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Filter items..."
|
||||||
|
class="input input-sm bg-transparent border-0 p-2 flex-1 focus:outline-none"
|
||||||
|
x-model="itemFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Panel Content -->
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<!-- No Category Selected State -->
|
||||||
|
<div x-show="!selectedCategory" class="h-full flex flex-col items-center justify-center opacity-30" x-cloak>
|
||||||
|
<i class="fa-solid fa-arrow-left text-5xl mb-4"></i>
|
||||||
|
<p class="text-lg">Select a category from the left to view values</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div x-show="itemLoading" class="h-full flex flex-col items-center justify-center" x-cloak>
|
||||||
|
<div class="spinner spinner-lg mx-auto mb-4"></div>
|
||||||
|
<p style="color: rgb(var(--color-text-muted));">Loading items...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Values Table -->
|
||||||
|
<div x-show="!itemLoading && selectedCategory">
|
||||||
|
<template x-if="!items[selectedCategory]?.length">
|
||||||
|
<div class="h-full flex flex-col items-center justify-center opacity-30" x-cloak>
|
||||||
|
<i class="fa-solid fa-box-open text-5xl mb-4"></i>
|
||||||
|
<p>This category has no items</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template x-if="items[selectedCategory]?.length">
|
||||||
|
<table class="table table-zebra w-full">
|
||||||
|
<thead class="sticky top-0 bg-inherit shadow-sm z-10">
|
||||||
|
<tr>
|
||||||
|
<th class="w-24">Key</th>
|
||||||
|
<th>Value / Label</th>
|
||||||
|
<th class="w-20 text-center">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="item in filteredItems" :key="item.value">
|
||||||
|
<tr class="group">
|
||||||
|
<td class="font-mono text-xs">
|
||||||
|
<span class="badge badge-ghost px-2 py-1" x-text="item.value || '-'"></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="item.label || '-'"></div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs btn-square opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
@click="copyToClipboard(item.label)"
|
||||||
|
title="Copy label"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Filter Empty State -->
|
||||||
|
<template x-if="filteredItems.length === 0 && items[selectedCategory]?.length && itemFilter">
|
||||||
|
<div class="p-12 text-center opacity-40" x-cloak>
|
||||||
|
<i class="fa-solid fa-magnifying-glass text-4xl mb-3"></i>
|
||||||
|
<p>No items match your filter</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Panel Footer -->
|
||||||
|
<div x-show="selectedCategory" class="p-3 border-t text-xs text-center opacity-40" style="border-color: rgb(var(--color-border));" x-transition>
|
||||||
|
Showing <span x-text="filteredItems.length"></span> of <span x-text="items[selectedCategory]?.length || 0"></span> items
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section("script") ?>
|
||||||
|
<script>
|
||||||
|
function valueSetLibrary() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
itemLoading: false,
|
||||||
|
list: {},
|
||||||
|
items: {},
|
||||||
|
keyword: "",
|
||||||
|
sortBy: 'name',
|
||||||
|
selectedCategory: null,
|
||||||
|
itemFilter: "",
|
||||||
|
|
||||||
|
get totalItems() {
|
||||||
|
return Object.values(this.list).reduce((acc, count) => acc + count, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
get filteredList() {
|
||||||
|
if (!this.keyword) return this.list;
|
||||||
|
const filter = this.keyword.toLowerCase();
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(this.list).filter(([name]) =>
|
||||||
|
this.matchesPartial(name, filter)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
matchesPartial(name, filter) {
|
||||||
|
const nameLower = name.toLowerCase().replace(/_/g, ' ');
|
||||||
|
let nameIndex = 0;
|
||||||
|
for (let i = 0; i < filter.length; i++) {
|
||||||
|
const char = filter[i];
|
||||||
|
const foundIndex = nameLower.indexOf(char, nameIndex);
|
||||||
|
if (foundIndex === -1) return false;
|
||||||
|
nameIndex = foundIndex + 1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
get filteredItems() {
|
||||||
|
if (!this.items[this.selectedCategory]) return [];
|
||||||
|
const filter = this.itemFilter.toLowerCase();
|
||||||
|
return this.items[this.selectedCategory].filter(item => {
|
||||||
|
const label = (item.label || "").toLowerCase();
|
||||||
|
const value = (item.value || "").toLowerCase();
|
||||||
|
return label.includes(filter) || value.includes(filter);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchList() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (this.keyword) params.append('search', this.keyword);
|
||||||
|
|
||||||
|
const res = await fetch(`${BASEURL}api/valueset?${params}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("HTTP error");
|
||||||
|
const data = await res.json();
|
||||||
|
this.list = data.data || {};
|
||||||
|
this.sortList();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.list = {};
|
||||||
|
this.showToast('Failed to load value sets', 'error');
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sortList() {
|
||||||
|
const entries = Object.entries(this.list);
|
||||||
|
entries.sort((a, b) => {
|
||||||
|
if (this.sortBy === 'count') return b[1] - a[1];
|
||||||
|
return a[0].localeCompare(b[0]);
|
||||||
|
});
|
||||||
|
this.list = Object.fromEntries(entries);
|
||||||
|
},
|
||||||
|
|
||||||
|
async selectCategory(name) {
|
||||||
|
if (this.selectedCategory === name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedCategory = name;
|
||||||
|
this.itemFilter = "";
|
||||||
|
|
||||||
|
if (!this.items[name]) {
|
||||||
|
await this.fetchItems(name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchItems(name) {
|
||||||
|
this.itemLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASEURL}api/valueset/${name}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("HTTP error");
|
||||||
|
const data = await res.json();
|
||||||
|
this.items[name] = data.data || [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.items[name] = [];
|
||||||
|
this.showToast('Failed to load items', 'error');
|
||||||
|
} finally {
|
||||||
|
this.itemLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatName(name) {
|
||||||
|
if (!name) return '';
|
||||||
|
return name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
},
|
||||||
|
|
||||||
|
async copyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
this.showToast('Copied to clipboard', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showToast(message, type = 'info') {
|
||||||
|
if (this.$root && this.$root.showToast) {
|
||||||
|
this.$root.showToast(message, type);
|
||||||
|
} else {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Hello World Page</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Hello World!</h1>
|
|
||||||
<p>This is a simple HTML page.</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Hello World Page</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Hello World!</h1>
|
|
||||||
<p>This is a simple HTML page.</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
1129
docs/ERD_EXTRACT.md
Normal file
1129
docs/ERD_EXTRACT.md
Normal file
File diff suppressed because it is too large
Load Diff
1914
docs/clqms_database.dbdiagram
Normal file
1914
docs/clqms_database.dbdiagram
Normal file
File diff suppressed because it is too large
Load Diff
854
docs/clqms_database.dbml
Normal file
854
docs/clqms_database.dbml
Normal file
@ -0,0 +1,854 @@
|
|||||||
|
// CLQMS Database Schema
|
||||||
|
// Generated from ERD_EXTRACT.md
|
||||||
|
// Database Markup Language (DBML) for dbdiagram.io and other tools
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TABLE 1: Organization Structure
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
Table account {
|
||||||
|
AccountID int [pk]
|
||||||
|
AccountName varchar(255)
|
||||||
|
ParentAccountID int
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table site {
|
||||||
|
SiteID int [pk]
|
||||||
|
AccountID int
|
||||||
|
SiteName varchar(255)
|
||||||
|
Location varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table discipline {
|
||||||
|
DisciplineID int [pk]
|
||||||
|
DisciplineName varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table department {
|
||||||
|
DepartmentID int [pk]
|
||||||
|
DepartmentName varchar(255)
|
||||||
|
DisciplineID int
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table workstation {
|
||||||
|
WorkstationID int [pk]
|
||||||
|
SiteID int
|
||||||
|
DepartmentID int
|
||||||
|
WorkstationName varchar(255)
|
||||||
|
LocalDB boolean
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table instrument {
|
||||||
|
InstrumentID int [pk]
|
||||||
|
SiteID int
|
||||||
|
WorkstationID int
|
||||||
|
InstrumentAlias varchar(255)
|
||||||
|
InstrumentName varchar(255)
|
||||||
|
InstrumentType varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table personnel {
|
||||||
|
PersonnelID int [pk]
|
||||||
|
SiteID int
|
||||||
|
PersonnelName varchar(255)
|
||||||
|
Position varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table personneldocument {
|
||||||
|
DocID int [pk]
|
||||||
|
PersonnelID int
|
||||||
|
DocType varchar(255)
|
||||||
|
DocFile blob
|
||||||
|
ExpiryDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table personnelaccess {
|
||||||
|
AccessID int [pk]
|
||||||
|
PersonnelID int
|
||||||
|
Role varchar(255)
|
||||||
|
Permissions text
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table location {
|
||||||
|
LocationID int [pk]
|
||||||
|
SiteID int
|
||||||
|
ParentLocationID int
|
||||||
|
LocationTypeID int
|
||||||
|
LocationName varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table locationaddress {
|
||||||
|
AddressID int [pk]
|
||||||
|
LocationID int
|
||||||
|
AddressLine1 varchar(255)
|
||||||
|
AddressLine2 varchar(255)
|
||||||
|
City varchar(100)
|
||||||
|
PostalCode varchar(20)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table patient {
|
||||||
|
PatientID int [pk]
|
||||||
|
SiteID int
|
||||||
|
InternalPID int
|
||||||
|
FirstName varchar(255)
|
||||||
|
LastName varchar(255)
|
||||||
|
DateOfBirth datetime
|
||||||
|
Sex varchar(10)
|
||||||
|
Race varchar(50)
|
||||||
|
Ethnicity varchar(50)
|
||||||
|
Religion varchar(50)
|
||||||
|
CreateDate datetime
|
||||||
|
DelDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table patientcontact {
|
||||||
|
ContactID int [pk]
|
||||||
|
InternalPID int
|
||||||
|
ContactType varchar(50)
|
||||||
|
ContactValue varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table patientinsurance {
|
||||||
|
InsuranceID int [pk]
|
||||||
|
InternalPID int
|
||||||
|
InsuranceProvider varchar(255)
|
||||||
|
PolicyNumber varchar(100)
|
||||||
|
GroupNumber varchar(100)
|
||||||
|
EffectiveDate datetime
|
||||||
|
ExpiryDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table patientvisit {
|
||||||
|
VisitID int [pk]
|
||||||
|
InternalPID int
|
||||||
|
SiteID int
|
||||||
|
VisitClass varchar(50)
|
||||||
|
VisitType varchar(50)
|
||||||
|
VisitDate datetime
|
||||||
|
DischargeDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table admission {
|
||||||
|
AdmissionID int [pk]
|
||||||
|
VisitID int
|
||||||
|
PatientID int
|
||||||
|
SiteID int
|
||||||
|
AdmissionDate datetime
|
||||||
|
DischargeDate datetime
|
||||||
|
ADTCode varchar(50)
|
||||||
|
ReferringParty varchar(255)
|
||||||
|
BillingAccount varchar(255)
|
||||||
|
AttendingDoctor varchar(255)
|
||||||
|
ReferringDoctor varchar(255)
|
||||||
|
VitalSigns text
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table admissionlocation {
|
||||||
|
ID int [pk]
|
||||||
|
AdmissionID int
|
||||||
|
LocationID int
|
||||||
|
TransferDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table testorder {
|
||||||
|
OrderID varchar(13) [pk]
|
||||||
|
SiteID int
|
||||||
|
PatientID int
|
||||||
|
VisitID int
|
||||||
|
OrderDate datetime
|
||||||
|
Urgency varchar(50)
|
||||||
|
Status varchar(50)
|
||||||
|
OrderingProvider varchar(255)
|
||||||
|
ProductionSiteID int
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table testorderdetail {
|
||||||
|
OrderDetailID int [pk]
|
||||||
|
OrderID varchar(13)
|
||||||
|
TestID int
|
||||||
|
Priority int
|
||||||
|
Status varchar(50)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table specimen {
|
||||||
|
SID varchar(17) [pk]
|
||||||
|
OrderID varchar(13)
|
||||||
|
SpecimenDefID int
|
||||||
|
ParentSID varchar(17)
|
||||||
|
SpecimenType varchar(50)
|
||||||
|
SpecimenRole varchar(50)
|
||||||
|
CollectionDate datetime
|
||||||
|
CollectionSite int
|
||||||
|
CollectedBy int
|
||||||
|
ContainerType varchar(50)
|
||||||
|
Additive varchar(50)
|
||||||
|
CollectionMethod varchar(50)
|
||||||
|
BodySite varchar(50)
|
||||||
|
SpecimenCondition varchar(50)
|
||||||
|
Status varchar(50)
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table specimencollection {
|
||||||
|
ID int [pk]
|
||||||
|
SID varchar(17)
|
||||||
|
Activity varchar(50)
|
||||||
|
ActivityName varchar(100)
|
||||||
|
ActRes varchar(50)
|
||||||
|
LocationID int
|
||||||
|
EquipmentID int
|
||||||
|
PersonnelID int
|
||||||
|
ActivityDate datetime
|
||||||
|
Notes text
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table specimentransport {
|
||||||
|
TransportID int [pk]
|
||||||
|
SID varchar(17)
|
||||||
|
SenderID int
|
||||||
|
ReceiverID int
|
||||||
|
TransportDate datetime
|
||||||
|
Condition text
|
||||||
|
PackagingID varchar(50)
|
||||||
|
FromLocation int
|
||||||
|
ToLocation int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table specimenstorage {
|
||||||
|
StorageID int [pk]
|
||||||
|
SID varchar(17)
|
||||||
|
LocationID int
|
||||||
|
StorageTemperature decimal(10,2)
|
||||||
|
StorageDate datetime
|
||||||
|
ThawCount int
|
||||||
|
ExpiryDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table testdef {
|
||||||
|
TestID int [pk]
|
||||||
|
TestName varchar(255)
|
||||||
|
TestCode varchar(50)
|
||||||
|
LOINCCode varchar(50)
|
||||||
|
TestType varchar(50)
|
||||||
|
DisciplineID int
|
||||||
|
SpecimenTypeID int
|
||||||
|
ContainerTypeID int
|
||||||
|
ResultType varchar(50)
|
||||||
|
ResultUnit varchar(50)
|
||||||
|
Methodology varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table testdefsite {
|
||||||
|
ID int [pk]
|
||||||
|
TestID int
|
||||||
|
SiteID int
|
||||||
|
TestNameLocal varchar(255)
|
||||||
|
TestCodeLocal varchar(50)
|
||||||
|
WorkstationID int
|
||||||
|
InstrumentID int
|
||||||
|
Active boolean
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table testdeftech {
|
||||||
|
ID int [pk]
|
||||||
|
TestID int
|
||||||
|
InstrumentID int
|
||||||
|
InstrumentTestCode varchar(50)
|
||||||
|
TestMapping varchar(255)
|
||||||
|
Active boolean
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table calculatedtest {
|
||||||
|
CalculatedTestID int [pk]
|
||||||
|
TestID int
|
||||||
|
Formula text
|
||||||
|
ParamTestID1 int
|
||||||
|
ParamTestID2 int
|
||||||
|
ParamTestID3 int
|
||||||
|
ParamTestID4 int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table grouptest {
|
||||||
|
GroupTestID int [pk]
|
||||||
|
GroupTestName varchar(255)
|
||||||
|
GroupTestType varchar(50)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table grouptestmember {
|
||||||
|
ID int [pk]
|
||||||
|
GroupTestID int
|
||||||
|
TestID int
|
||||||
|
Sequence int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table panel {
|
||||||
|
PanelID int [pk]
|
||||||
|
PanelName varchar(255)
|
||||||
|
PanelType varchar(50)
|
||||||
|
ParentPanelID int
|
||||||
|
DisciplineID int
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table panelmember {
|
||||||
|
ID int [pk]
|
||||||
|
PanelID int
|
||||||
|
TestID int
|
||||||
|
Sequence int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table referencerangenumeric {
|
||||||
|
RefRangeID int [pk]
|
||||||
|
TestID int
|
||||||
|
AgeFrom int
|
||||||
|
AgeTo int
|
||||||
|
Sex varchar(10)
|
||||||
|
LowValue decimal(10,2)
|
||||||
|
HighValue decimal(10,2)
|
||||||
|
Unit varchar(20)
|
||||||
|
SpecimenTypeID int
|
||||||
|
SiteID int
|
||||||
|
EffectiveDate datetime
|
||||||
|
ExpiryDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table referencerangethreshold {
|
||||||
|
RefRangeID int [pk]
|
||||||
|
TestID int
|
||||||
|
AgeFrom int
|
||||||
|
AgeTo int
|
||||||
|
Sex varchar(10)
|
||||||
|
CutOffLow decimal(10,2)
|
||||||
|
CutOffHigh decimal(10,2)
|
||||||
|
GrayZoneLow decimal(10,2)
|
||||||
|
GrayZoneHigh decimal(10,2)
|
||||||
|
SpecimenTypeID int
|
||||||
|
SiteID int
|
||||||
|
EffectiveDate datetime
|
||||||
|
ExpiryDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table referencerangetext {
|
||||||
|
RefRangeID int [pk]
|
||||||
|
TestID int
|
||||||
|
AgeFrom int
|
||||||
|
AgeTo int
|
||||||
|
Sex varchar(10)
|
||||||
|
TextValue text
|
||||||
|
SpecimenTypeID int
|
||||||
|
SiteID int
|
||||||
|
EffectiveDate datetime
|
||||||
|
ExpiryDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table calibrator {
|
||||||
|
CalibratorID int [pk]
|
||||||
|
CalibratorName varchar(255)
|
||||||
|
Manufacturer varchar(255)
|
||||||
|
LotNumber varchar(50)
|
||||||
|
ExpiryDate datetime
|
||||||
|
TestID int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table calibration {
|
||||||
|
CalibrationID int [pk]
|
||||||
|
InstrumentID int
|
||||||
|
TestID int
|
||||||
|
CalibratorID int
|
||||||
|
Level int
|
||||||
|
CalibrationDate datetime
|
||||||
|
Factor decimal(10,4)
|
||||||
|
Absorbance decimal(10,4)
|
||||||
|
TargetValue decimal(10,4)
|
||||||
|
TargetUnit varchar(20)
|
||||||
|
PersonnelID int
|
||||||
|
Status varchar(50)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table calparinst {
|
||||||
|
CalParInstID int [pk]
|
||||||
|
EquipmentID int
|
||||||
|
Calibrator varchar(255)
|
||||||
|
LotNo varchar(50)
|
||||||
|
ExpiryDate datetime
|
||||||
|
TestInstID1 int
|
||||||
|
SampleType varchar(50)
|
||||||
|
Level int
|
||||||
|
Concentration decimal(10,4)
|
||||||
|
CalUnit varchar(20)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table qcmaterial {
|
||||||
|
QCMaterialID int [pk]
|
||||||
|
MaterialName varchar(255)
|
||||||
|
Manufacturer varchar(255)
|
||||||
|
LotNumber varchar(50)
|
||||||
|
ExpiryDate datetime
|
||||||
|
Level int
|
||||||
|
TestID int
|
||||||
|
TargetMean decimal(10,4)
|
||||||
|
TargetSD decimal(10,4)
|
||||||
|
TargetCV decimal(10,4)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table qcresult {
|
||||||
|
QCResultID int [pk]
|
||||||
|
InstrumentID int
|
||||||
|
TestID int
|
||||||
|
QCMaterialID int
|
||||||
|
Level int
|
||||||
|
QCDate datetime
|
||||||
|
ResultValue decimal(10,4)
|
||||||
|
Mean decimal(10,4)
|
||||||
|
SD decimal(10,4)
|
||||||
|
CV decimal(10,4)
|
||||||
|
Sigma decimal(10,4)
|
||||||
|
ZScore decimal(10,4)
|
||||||
|
Flag varchar(10)
|
||||||
|
PersonnelID int
|
||||||
|
Status varchar(50)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table qcstatistic {
|
||||||
|
StatisticID int [pk]
|
||||||
|
InstrumentID int
|
||||||
|
TestID int
|
||||||
|
QCMaterialID int
|
||||||
|
Level int
|
||||||
|
StatisticDate datetime
|
||||||
|
Mean decimal(10,4)
|
||||||
|
SD decimal(10,4)
|
||||||
|
CV decimal(10,4)
|
||||||
|
SampleSize int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table patres {
|
||||||
|
ResultID int [pk]
|
||||||
|
SID varchar(17)
|
||||||
|
TestID int
|
||||||
|
OrderID varchar(13)
|
||||||
|
ResultValue varchar(100)
|
||||||
|
ResultNumeric decimal(15,5)
|
||||||
|
ResultText text
|
||||||
|
ResultUnit varchar(20)
|
||||||
|
ResultStatus varchar(50)
|
||||||
|
PersonnelID int
|
||||||
|
VerificationDate datetime
|
||||||
|
VerificationPersonnel int
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table patrestech {
|
||||||
|
TechResultID int [pk]
|
||||||
|
ResultID int
|
||||||
|
InstrumentID int
|
||||||
|
RawResult text
|
||||||
|
ResultDate datetime
|
||||||
|
RerunCount int
|
||||||
|
Dilution decimal(10,4)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table patresflag {
|
||||||
|
FlagID int [pk]
|
||||||
|
ResultID int
|
||||||
|
FlagType varchar(10)
|
||||||
|
FlagDescription varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table resultdistribution {
|
||||||
|
DistributionID int [pk]
|
||||||
|
ResultID int
|
||||||
|
RecipientType varchar(50)
|
||||||
|
RecipientID int
|
||||||
|
DistributionDate datetime
|
||||||
|
DistributionMethod varchar(50)
|
||||||
|
Status varchar(50)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table valuesetmember {
|
||||||
|
MemberID int [pk]
|
||||||
|
ValueSetID int
|
||||||
|
MemberCode varchar(50)
|
||||||
|
MemberValue varchar(255)
|
||||||
|
DisplayOrder int
|
||||||
|
Active boolean
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table reagent {
|
||||||
|
ReagentID int [pk]
|
||||||
|
ReagentName varchar(255)
|
||||||
|
Manufacturer varchar(255)
|
||||||
|
CatalogNumber varchar(100)
|
||||||
|
LotNumber varchar(50)
|
||||||
|
ExpiryDate datetime
|
||||||
|
TestID int
|
||||||
|
InstrumentID int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table reagentusage {
|
||||||
|
UsageID int [pk]
|
||||||
|
ReagentID int
|
||||||
|
TestID int
|
||||||
|
UsageDate datetime
|
||||||
|
QuantityUsed decimal(10,2)
|
||||||
|
PersonnelID int
|
||||||
|
OrderID varchar(13)
|
||||||
|
SID varchar(17)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table product {
|
||||||
|
ProductID int [pk]
|
||||||
|
CatalogID int
|
||||||
|
SiteID int
|
||||||
|
LotNumber varchar(50)
|
||||||
|
ExpiryDate datetime
|
||||||
|
Quantity int
|
||||||
|
ReorderLevel int
|
||||||
|
LocationID int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table inventorytransaction {
|
||||||
|
TransactionID int [pk]
|
||||||
|
ProductID int
|
||||||
|
TransactionType varchar(50)
|
||||||
|
Quantity int
|
||||||
|
TransactionDate datetime
|
||||||
|
PersonnelID int
|
||||||
|
ReferenceID varchar(100)
|
||||||
|
Notes text
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table equipment {
|
||||||
|
EquipmentID int [pk]
|
||||||
|
EquipmentName varchar(255)
|
||||||
|
EquipmentType varchar(50)
|
||||||
|
Manufacturer varchar(255)
|
||||||
|
Model varchar(100)
|
||||||
|
SerialNumber varchar(100)
|
||||||
|
SiteID int
|
||||||
|
LocationID int
|
||||||
|
Status varchar(50)
|
||||||
|
InstallDate datetime
|
||||||
|
DecommissionDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table equipmentmaintenance {
|
||||||
|
MaintenanceID int [pk]
|
||||||
|
EquipmentID int
|
||||||
|
MaintenanceType varchar(100)
|
||||||
|
MaintenanceDate datetime
|
||||||
|
Description text
|
||||||
|
PerformedBy varchar(255)
|
||||||
|
NextMaintenanceDate datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table equipmentactivity {
|
||||||
|
ActivityID int [pk]
|
||||||
|
EquipmentID int
|
||||||
|
ActivityType varchar(100)
|
||||||
|
ActivityDate datetime
|
||||||
|
ActivityResult varchar(50)
|
||||||
|
PersonnelID int
|
||||||
|
Notes text
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table equipmenttestcount {
|
||||||
|
ID int [pk]
|
||||||
|
EquipmentID int
|
||||||
|
TestDate datetime
|
||||||
|
TestType varchar(50)
|
||||||
|
TestCount int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table doctor {
|
||||||
|
DoctorID int [pk]
|
||||||
|
DoctorName varchar(255)
|
||||||
|
DoctorCode varchar(50)
|
||||||
|
Specialty varchar(100)
|
||||||
|
SIP varchar(50)
|
||||||
|
PracticeLocation varchar(255)
|
||||||
|
ContactID int
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table contactdetail {
|
||||||
|
DetailID int [pk]
|
||||||
|
ContactID int
|
||||||
|
DetailType varchar(50)
|
||||||
|
DetailValue varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table auditarchive {
|
||||||
|
ArchiveID int [pk]
|
||||||
|
AuditID int
|
||||||
|
ArchiveDate datetime
|
||||||
|
ArchiveLocation varchar(255)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table user {
|
||||||
|
UserID int [pk]
|
||||||
|
Username varchar(100)
|
||||||
|
PasswordHash varchar(255)
|
||||||
|
PersonnelID int
|
||||||
|
Role varchar(50)
|
||||||
|
Status varchar(20)
|
||||||
|
LastLogin datetime
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table usersession {
|
||||||
|
SessionID int [pk]
|
||||||
|
UserID int
|
||||||
|
SessionToken varchar(255)
|
||||||
|
ExpiryDate datetime
|
||||||
|
IPAddress varchar(50)
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table reporttemplate {
|
||||||
|
TemplateID int [pk]
|
||||||
|
TemplateName varchar(255)
|
||||||
|
TemplateType varchar(50)
|
||||||
|
DisciplineID int
|
||||||
|
TemplateConfig text
|
||||||
|
CreateDate datetime
|
||||||
|
EndDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table reportoutput {
|
||||||
|
OutputID int [pk]
|
||||||
|
TemplateID int
|
||||||
|
OrderID varchar(13)
|
||||||
|
OutputFormat varchar(50)
|
||||||
|
OutputData blob
|
||||||
|
GeneratedDate datetime
|
||||||
|
PersonnelID int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table hosttestmapping {
|
||||||
|
MappingID int [pk]
|
||||||
|
HostID int
|
||||||
|
HostTestCode varchar(50)
|
||||||
|
LocalTestID int
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
Table hostsynclog {
|
||||||
|
LogID int [pk]
|
||||||
|
HostID int
|
||||||
|
SyncDate datetime
|
||||||
|
RecordsProcessed int
|
||||||
|
Errors int
|
||||||
|
Status varchar(20)
|
||||||
|
Details text
|
||||||
|
CreateDate datetime
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// RELATIONSHIPS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Organization Structure
|
||||||
|
Ref: account.ParentAccountID > account.AccountID [delete: cascade]
|
||||||
|
Ref: site.AccountID > account.AccountID
|
||||||
|
Ref: department.DisciplineID > discipline.DisciplineID
|
||||||
|
Ref: workstation.SiteID > site.SiteID
|
||||||
|
Ref: workstation.DepartmentID > department.DepartmentID
|
||||||
|
Ref: instrument.SiteID > site.SiteID
|
||||||
|
Ref: instrument.WorkstationID > workstation.WorkstationID
|
||||||
|
|
||||||
|
// Personnel
|
||||||
|
Ref: personnel.SiteID > site.SiteID
|
||||||
|
Ref: personneldocument.PersonnelID > personnel.PersonnelID
|
||||||
|
Ref: personnelaccess.PersonnelID > personnel.PersonnelID
|
||||||
|
|
||||||
|
// Location Management
|
||||||
|
Ref: location.SiteID > site.SiteID
|
||||||
|
Ref: location.ParentLocationID > location.LocationID
|
||||||
|
Ref: locationaddress.LocationID > location.LocationID
|
||||||
|
|
||||||
|
// Patient Registration
|
||||||
|
Ref: patient.SiteID > site.SiteID
|
||||||
|
Ref: patientcontact.InternalPID > patient.InternalPID
|
||||||
|
Ref: patientinsurance.InternalPID > patient.InternalPID
|
||||||
|
Ref: patientvisit.InternalPID > patient.InternalPID
|
||||||
|
Ref: patientvisit.SiteID > site.SiteID
|
||||||
|
|
||||||
|
// Patient Admission
|
||||||
|
Ref: admission.VisitID > patientvisit.VisitID
|
||||||
|
Ref: admission.PatientID > patient.PatientID
|
||||||
|
Ref: admission.SiteID > site.SiteID
|
||||||
|
Ref: admissionlocation.AdmissionID > admission.AdmissionID
|
||||||
|
Ref: admissionlocation.LocationID > location.LocationID
|
||||||
|
|
||||||
|
// Test Ordering
|
||||||
|
Ref: testorder.SiteID > site.SiteID
|
||||||
|
Ref: testorder.PatientID > patient.PatientID
|
||||||
|
Ref: testorder.VisitID > patientvisit.VisitID
|
||||||
|
Ref: testorder.ProductionSiteID > site.SiteID
|
||||||
|
Ref: testorderdetail.OrderID > testorder.OrderID
|
||||||
|
|
||||||
|
// Specimen Management
|
||||||
|
Ref: specimen.OrderID > testorder.OrderID
|
||||||
|
Ref: specimencollection.SID > specimen.SID
|
||||||
|
Ref: specimencollection.LocationID > location.LocationID
|
||||||
|
Ref: specimencollection.EquipmentID > instrument.InstrumentID
|
||||||
|
Ref: specimencollection.PersonnelID > personnel.PersonnelID
|
||||||
|
Ref: specimentransport.SID > specimen.SID
|
||||||
|
Ref: specimentransport.SenderID > personnel.PersonnelID
|
||||||
|
Ref: specimentransport.ReceiverID > personnel.PersonnelID
|
||||||
|
Ref: specimenstorage.SID > specimen.SID
|
||||||
|
Ref: specimenstorage.LocationID > location.LocationID
|
||||||
|
|
||||||
|
// Test Management
|
||||||
|
Ref: testdef.DisciplineID > discipline.DisciplineID
|
||||||
|
Ref: testdefsite.TestID > testdef.TestID
|
||||||
|
Ref: testdefsite.SiteID > site.SiteID
|
||||||
|
Ref: testdefsite.WorkstationID > workstation.WorkstationID
|
||||||
|
Ref: testdefsite.InstrumentID > instrument.InstrumentID
|
||||||
|
Ref: testdeftech.TestID > testdef.TestID
|
||||||
|
Ref: testdeftech.InstrumentID > instrument.InstrumentID
|
||||||
|
Ref: calculatedtest.TestID > testdef.TestID
|
||||||
|
Ref: grouptestmember.GroupTestID > grouptest.GroupTestID
|
||||||
|
Ref: grouptestmember.TestID > testdef.TestID
|
||||||
|
Ref: panel.ParentPanelID > panel.PanelID
|
||||||
|
Ref: panel.DisciplineID > discipline.DisciplineID
|
||||||
|
Ref: panelmember.PanelID > panel.PanelID
|
||||||
|
Ref: panelmember.TestID > testdef.TestID
|
||||||
|
|
||||||
|
// Reference Range
|
||||||
|
Ref: referencerangenumeric.TestID > testdef.TestID
|
||||||
|
Ref: referencerangenumeric.SiteID > site.SiteID
|
||||||
|
Ref: referencerangethreshold.TestID > testdef.TestID
|
||||||
|
Ref: referencerangethreshold.SiteID > site.SiteID
|
||||||
|
Ref: referencerangetext.TestID > testdef.TestID
|
||||||
|
Ref: referencerangetext.SiteID > site.SiteID
|
||||||
|
|
||||||
|
// Calibration
|
||||||
|
Ref: calibrator.TestID > testdef.TestID
|
||||||
|
Ref: calibration.InstrumentID > instrument.InstrumentID
|
||||||
|
Ref: calibration.TestID > testdef.TestID
|
||||||
|
Ref: calibration.CalibratorID > calibrator.CalibratorID
|
||||||
|
Ref: calibration.PersonnelID > personnel.PersonnelID
|
||||||
|
Ref: calparinst.EquipmentID > instrument.InstrumentID
|
||||||
|
|
||||||
|
// Quality Control
|
||||||
|
Ref: qcmaterial.TestID > testdef.TestID
|
||||||
|
Ref: qcresult.InstrumentID > instrument.InstrumentID
|
||||||
|
Ref: qcresult.TestID > testdef.TestID
|
||||||
|
Ref: qcresult.QCMaterialID > qcmaterial.QCMaterialID
|
||||||
|
Ref: qcresult.PersonnelID > personnel.PersonnelID
|
||||||
|
Ref: qcstatistic.InstrumentID > instrument.InstrumentID
|
||||||
|
Ref: qcstatistic.TestID > testdef.TestID
|
||||||
|
Ref: qcstatistic.QCMaterialID > qcmaterial.QCMaterialID
|
||||||
|
|
||||||
|
// Test Results
|
||||||
|
Ref: patres.SID > specimen.SID
|
||||||
|
Ref: patres.TestID > testdef.TestID
|
||||||
|
Ref: patres.OrderID > testorder.OrderID
|
||||||
|
Ref: patres.PersonnelID > personnel.PersonnelID
|
||||||
|
Ref: patrestech.ResultID > patres.ResultID
|
||||||
|
Ref: patrestech.InstrumentID > instrument.InstrumentID
|
||||||
|
Ref: patresflag.ResultID > patres.ResultID
|
||||||
|
Ref: resultdistribution.ResultID > patres.ResultID
|
||||||
|
|
||||||
|
// Reagent & Inventory
|
||||||
|
Ref: reagent.TestID > testdef.TestID
|
||||||
|
Ref: reagent.InstrumentID > instrument.InstrumentID
|
||||||
|
Ref: reagentusage.ReagentID > reagent.ReagentID
|
||||||
|
Ref: reagentusage.TestID > testdef.TestID
|
||||||
|
Ref: reagentusage.PersonnelID > personnel.PersonnelID
|
||||||
|
|
||||||
|
Ref: product.SiteID > site.SiteID
|
||||||
|
Ref: product.LocationID > location.LocationID
|
||||||
|
Ref: inventorytransaction.ProductID > product.ProductID
|
||||||
|
Ref: inventorytransaction.PersonnelID > personnel.PersonnelID
|
||||||
|
|
||||||
|
// Equipment Management
|
||||||
|
Ref: equipment.SiteID > site.SiteID
|
||||||
|
Ref: equipment.LocationID > location.LocationID
|
||||||
|
Ref: equipmentmaintenance.EquipmentID > equipment.EquipmentID
|
||||||
|
Ref: equipmentactivity.EquipmentID > equipment.EquipmentID
|
||||||
|
Ref: equipmentactivity.PersonnelID > personnel.PersonnelID
|
||||||
|
Ref: equipmenttestcount.EquipmentID > equipment.EquipmentID
|
||||||
|
|
||||||
|
|
||||||
|
// User & Authentication
|
||||||
|
Ref: user.PersonnelID > personnel.PersonnelID
|
||||||
|
Ref: usersession.UserID > user.UserID
|
||||||
|
|
||||||
|
// Visualization & Reporting
|
||||||
|
Ref: reporttemplate.DisciplineID > discipline.DisciplineID
|
||||||
|
Ref: reportoutput.TemplateID > reporttemplate.TemplateID
|
||||||
|
Ref: reportoutput.OrderID > testorder.OrderID
|
||||||
|
Ref: reportoutput.PersonnelID > personnel.PersonnelID
|
||||||
|
|
||||||
|
// Host System Integration
|
||||||
|
Ref: hosttestmapping.LocalTestID > testdef.TestID
|
||||||
|
|
||||||
3287
docs/openapi.yaml
Normal file
3287
docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
17
public/docs.html
Normal file
17
public/docs.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<title>Elements in HTML</title>
|
||||||
|
<!-- Embed elements Elements via Web Component -->
|
||||||
|
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<elements-api apiDescriptionUrl="openapi.yaml" router="hash" layout="sidebar" />
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
3287
public/openapi.yaml
Normal file
3287
public/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,325 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Support\v2;
|
|
||||||
|
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
|
||||||
use CodeIgniter\Test\FeatureTestTrait;
|
|
||||||
use Firebase\JWT\JWT;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base test case for v2 Master Data tests
|
|
||||||
*
|
|
||||||
* Provides common setup, authentication, and helper methods
|
|
||||||
* for all v2 master test feature and unit tests.
|
|
||||||
*/
|
|
||||||
abstract class MasterTestCase extends CIUnitTestCase
|
|
||||||
{
|
|
||||||
use FeatureTestTrait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JWT token for authentication
|
|
||||||
*/
|
|
||||||
protected ?string $token = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test site ID
|
|
||||||
*/
|
|
||||||
protected int $testSiteId = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test site code
|
|
||||||
*/
|
|
||||||
protected string $testSiteCode = 'TEST01';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Valueset IDs for test types
|
|
||||||
*/
|
|
||||||
public const VALUESET_TEST_TYPE = 27; // VSetID for Test Types
|
|
||||||
public const VALUESET_RESULT_TYPE = 43; // VSetID for Result Types
|
|
||||||
public const VALUESET_REF_TYPE = 44; // VSetID for Reference Types
|
|
||||||
public const VALUESET_ENTITY_TYPE = 39; // VSetID for Entity Types
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Type VIDs
|
|
||||||
*/
|
|
||||||
public const TEST_TYPE_TEST = 1; // VID for TEST
|
|
||||||
public const TEST_TYPE_PARAM = 2; // VID for PARAM
|
|
||||||
public const TEST_TYPE_CALC = 3; // VID for CALC
|
|
||||||
public const TEST_TYPE_GROUP = 4; // VID for GROUP
|
|
||||||
public const TEST_TYPE_TITLE = 5; // VID for TITLE
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup test environment
|
|
||||||
*/
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->token = $this->generateTestToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup after test
|
|
||||||
*/
|
|
||||||
protected function tearDown(): void
|
|
||||||
{
|
|
||||||
parent::tearDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate JWT token for testing
|
|
||||||
*/
|
|
||||||
protected function generateTestToken(): string
|
|
||||||
{
|
|
||||||
$key = getenv('JWT_SECRET') ?: 'my-secret-key';
|
|
||||||
$payload = [
|
|
||||||
'iss' => 'localhost',
|
|
||||||
'aud' => 'localhost',
|
|
||||||
'iat' => time(),
|
|
||||||
'nbf' => time(),
|
|
||||||
'exp' => time() + 3600,
|
|
||||||
'uid' => 1,
|
|
||||||
'email' => 'admin@admin.com'
|
|
||||||
];
|
|
||||||
return JWT::encode($payload, $key, 'HS256');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make authenticated GET request
|
|
||||||
*/
|
|
||||||
protected function get(string $path, array $options = [])
|
|
||||||
{
|
|
||||||
$this->withHeaders(['Authorization' => 'Bearer ' . $this->token]);
|
|
||||||
return $this->call('get', $path, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make authenticated POST request
|
|
||||||
*/
|
|
||||||
protected function post(string $path, array $options = [])
|
|
||||||
{
|
|
||||||
$this->withHeaders(['Authorization' => 'Bearer ' . $this->token]);
|
|
||||||
return $this->call('post', $path, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make authenticated PUT request
|
|
||||||
*/
|
|
||||||
protected function put(string $path, array $options = [])
|
|
||||||
{
|
|
||||||
$this->withHeaders(['Authorization' => 'Bearer ' . $this->token]);
|
|
||||||
return $this->call('put', $path, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make authenticated DELETE request
|
|
||||||
*/
|
|
||||||
protected function delete(string $path, array $options = [])
|
|
||||||
{
|
|
||||||
$this->withHeaders(['Authorization' => 'Bearer ' . $this->token]);
|
|
||||||
return $this->call('delete', $path, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a TEST type test definition
|
|
||||||
*/
|
|
||||||
protected function createTestData(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => $this->testSiteCode,
|
|
||||||
'TestSiteName' => 'Test Definition ' . time(),
|
|
||||||
'TestType' => self::TEST_TYPE_TEST,
|
|
||||||
'Description' => 'Test description',
|
|
||||||
'SeqScr' => 10,
|
|
||||||
'SeqRpt' => 10,
|
|
||||||
'IndentLeft' => 0,
|
|
||||||
'VisibleScr' => 1,
|
|
||||||
'VisibleRpt' => 1,
|
|
||||||
'CountStat' => 1,
|
|
||||||
'details' => [
|
|
||||||
'DisciplineID' => 1,
|
|
||||||
'DepartmentID' => 1,
|
|
||||||
'ResultType' => 1, // Numeric
|
|
||||||
'RefType' => 1, // NMRC
|
|
||||||
'Unit1' => 'mg/dL',
|
|
||||||
'Decimal' => 2,
|
|
||||||
'Method' => 'Test Method',
|
|
||||||
'ExpectedTAT' => 60
|
|
||||||
],
|
|
||||||
'testmap' => [
|
|
||||||
[
|
|
||||||
'HostType' => 'HIS',
|
|
||||||
'HostID' => 'TEST001',
|
|
||||||
'HostTestCode' => 'TEST001',
|
|
||||||
'HostTestName' => 'Test (HIS)'
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a PARAM type test definition
|
|
||||||
*/
|
|
||||||
protected function createParamData(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'PARM' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Parameter Test ' . time(),
|
|
||||||
'TestType' => self::TEST_TYPE_PARAM,
|
|
||||||
'Description' => 'Parameter test description',
|
|
||||||
'SeqScr' => 5,
|
|
||||||
'SeqRpt' => 5,
|
|
||||||
'VisibleScr' => 1,
|
|
||||||
'VisibleRpt' => 1,
|
|
||||||
'CountStat' => 1,
|
|
||||||
'details' => [
|
|
||||||
'DisciplineID' => 1,
|
|
||||||
'DepartmentID' => 1,
|
|
||||||
'ResultType' => 1,
|
|
||||||
'RefType' => 1,
|
|
||||||
'Unit1' => 'unit',
|
|
||||||
'Decimal' => 1,
|
|
||||||
'Method' => 'Parameter Method'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a GROUP type test definition with members
|
|
||||||
*/
|
|
||||||
protected function createGroupData(array $memberIds = []): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'GRUP' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Group Test ' . time(),
|
|
||||||
'TestType' => self::TEST_TYPE_GROUP,
|
|
||||||
'Description' => 'Group test description',
|
|
||||||
'SeqScr' => 100,
|
|
||||||
'SeqRpt' => 100,
|
|
||||||
'VisibleScr' => 1,
|
|
||||||
'VisibleRpt' => 1,
|
|
||||||
'CountStat' => 1,
|
|
||||||
'Members' => $memberIds ?: [1, 2],
|
|
||||||
'testmap' => [
|
|
||||||
[
|
|
||||||
'HostType' => 'LIS',
|
|
||||||
'HostID' => 'LIS001',
|
|
||||||
'HostTestCode' => 'PANEL',
|
|
||||||
'HostTestName' => 'Test Panel (LIS)'
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a CALC type test definition
|
|
||||||
*/
|
|
||||||
protected function createCalcData(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'CALC' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Calculated Test ' . time(),
|
|
||||||
'TestType' => self::TEST_TYPE_CALC,
|
|
||||||
'Description' => 'Calculated test description',
|
|
||||||
'SeqScr' => 50,
|
|
||||||
'SeqRpt' => 50,
|
|
||||||
'VisibleScr' => 1,
|
|
||||||
'VisibleRpt' => 1,
|
|
||||||
'CountStat' => 1,
|
|
||||||
'details' => [
|
|
||||||
'DisciplineID' => 1,
|
|
||||||
'DepartmentID' => 1,
|
|
||||||
'FormulaInput' => '["TEST1", "TEST2"]',
|
|
||||||
'FormulaCode' => 'TEST1 + TEST2',
|
|
||||||
'FormulaLang' => 'SQL',
|
|
||||||
'RefType' => 1,
|
|
||||||
'Unit1' => 'mg/dL',
|
|
||||||
'Decimal' => 0,
|
|
||||||
'Method' => 'Calculation Method'
|
|
||||||
],
|
|
||||||
'testmap' => [
|
|
||||||
[
|
|
||||||
'HostType' => 'LIS',
|
|
||||||
'HostID' => 'LIS001',
|
|
||||||
'HostTestCode' => 'CALCR',
|
|
||||||
'HostTestName' => 'Calculated Result (LIS)'
|
|
||||||
]
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert API response has success status
|
|
||||||
*/
|
|
||||||
protected function assertSuccessResponse($response, string $message = 'Response should be successful'): void
|
|
||||||
{
|
|
||||||
$body = json_decode($response->response()->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('status', $body, $message);
|
|
||||||
$this->assertEquals('success', $body['status'], $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert API response has error status
|
|
||||||
*/
|
|
||||||
protected function assertErrorResponse($response, string $message = 'Response should be an error'): void
|
|
||||||
{
|
|
||||||
$body = json_decode($response->response()->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('status', $body, $message);
|
|
||||||
$this->assertNotEquals('success', $body['status'], $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert response has data key
|
|
||||||
*/
|
|
||||||
protected function assertHasData($response, string $message = 'Response should have data'): void
|
|
||||||
{
|
|
||||||
$body = json_decode($response->response()->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('data', $body, $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get test type name from VID
|
|
||||||
*/
|
|
||||||
protected function getTestTypeName(int $vid): string
|
|
||||||
{
|
|
||||||
return match ($vid) {
|
|
||||||
self::TEST_TYPE_TEST => 'TEST',
|
|
||||||
self::TEST_TYPE_PARAM => 'PARAM',
|
|
||||||
self::TEST_TYPE_CALC => 'CALC',
|
|
||||||
self::TEST_TYPE_GROUP => 'GROUP',
|
|
||||||
self::TEST_TYPE_TITLE => 'TITLE',
|
|
||||||
default => 'UNKNOWN'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skip test if database not available
|
|
||||||
*/
|
|
||||||
protected function requireDatabase(): void
|
|
||||||
{
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
try {
|
|
||||||
$db->connect();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->markTestSkipped('Database not available: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skip test if required seeded data not found
|
|
||||||
*/
|
|
||||||
protected function requireSeededData(): void
|
|
||||||
{
|
|
||||||
$db = \Config\Database::connect();
|
|
||||||
$count = $db->table('valueset')
|
|
||||||
->where('VSetID', self::VALUESET_TEST_TYPE)
|
|
||||||
->countAllResults();
|
|
||||||
|
|
||||||
if ($count === 0) {
|
|
||||||
$this->markTestSkipped('Test type valuesets not seeded');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,328 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\v2\master\TestDef;
|
|
||||||
|
|
||||||
use Tests\Support\v2\MasterTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature tests for CALC type test definitions
|
|
||||||
*
|
|
||||||
* Tests CALC-specific functionality including formula configuration
|
|
||||||
*/
|
|
||||||
class TestDefCalcTest extends MasterTestCase
|
|
||||||
{
|
|
||||||
protected string $endpoint = 'v2/master/tests';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test create CALC with formula
|
|
||||||
*/
|
|
||||||
public function testCreateCalcWithFormula(): void
|
|
||||||
{
|
|
||||||
$calcData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'CALC' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Calculated Test ' . time(),
|
|
||||||
'TestType' => $this::TEST_TYPE_CALC,
|
|
||||||
'Description' => 'Calculated test with formula',
|
|
||||||
'SeqScr' => 50,
|
|
||||||
'SeqRpt' => 50,
|
|
||||||
'VisibleScr' => 1,
|
|
||||||
'VisibleRpt' => 1,
|
|
||||||
'CountStat' => 1,
|
|
||||||
'details' => [
|
|
||||||
'DisciplineID' => 1,
|
|
||||||
'DepartmentID' => 1,
|
|
||||||
'FormulaInput' => '["CHOL", "HDL", "TG"]',
|
|
||||||
'FormulaCode' => 'CHOL - HDL - (TG / 5)',
|
|
||||||
'FormulaLang' => 'SQL',
|
|
||||||
'RefType' => 1, // NMRC
|
|
||||||
'Unit1' => 'mg/dL',
|
|
||||||
'Decimal' => 0,
|
|
||||||
'Method' => 'Friedewald Formula'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
|
||||||
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($status === 201) {
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('created', $body['status']);
|
|
||||||
|
|
||||||
// Verify calc details were created
|
|
||||||
$calcId = $body['data']['TestSiteId'];
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $calcId);
|
|
||||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if ($showBody['data'] !== null) {
|
|
||||||
$this->assertArrayHasKey('testdefcal', $showBody['data']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test CALC with different formula languages
|
|
||||||
*/
|
|
||||||
public function testCalcWithDifferentFormulaLanguages(): void
|
|
||||||
{
|
|
||||||
$languages = ['Phyton', 'CQL', 'FHIRP', 'SQL'];
|
|
||||||
|
|
||||||
foreach ($languages as $lang) {
|
|
||||||
$calcData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'C' . substr(time(), -5) . strtoupper(substr($lang, 0, 1)),
|
|
||||||
'TestSiteName' => "Calc with $lang",
|
|
||||||
'TestType' => $this::TEST_TYPE_CALC,
|
|
||||||
'details' => [
|
|
||||||
'FormulaInput' => '["TEST1"]',
|
|
||||||
'FormulaCode' => 'TEST1 * 2',
|
|
||||||
'FormulaLang' => $lang
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"CALC with $lang: Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test CALC with JSON formula input
|
|
||||||
*/
|
|
||||||
public function testCalcWithJsonFormulaInput(): void
|
|
||||||
{
|
|
||||||
$calcData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'CJSN' . substr(time(), -3),
|
|
||||||
'TestSiteName' => 'Calc with JSON Input',
|
|
||||||
'TestType' => $this::TEST_TYPE_CALC,
|
|
||||||
'details' => [
|
|
||||||
'FormulaInput' => '["parameter1", "parameter2", "parameter3"]',
|
|
||||||
'FormulaCode' => '(param1 + param2) / param3',
|
|
||||||
'FormulaLang' => 'FHIRP'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test CALC with complex formula
|
|
||||||
*/
|
|
||||||
public function testCalcWithComplexFormula(): void
|
|
||||||
{
|
|
||||||
$calcData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'CCMP' . substr(time(), -3),
|
|
||||||
'TestSiteName' => 'Calc with Complex Formula',
|
|
||||||
'TestType' => $this::TEST_TYPE_CALC,
|
|
||||||
'details' => [
|
|
||||||
'FormulaInput' => '["WBC", "NEUT", "LYMPH", "MONO", "EOS", "BASO"]',
|
|
||||||
'FormulaCode' => 'if WBC > 0 then (NEUT + LYMPH + MONO + EOS + BASO) / WBC * 100 else 0',
|
|
||||||
'FormulaLang' => 'Phyton'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test update CALC formula
|
|
||||||
*/
|
|
||||||
public function testUpdateCalcFormula(): void
|
|
||||||
{
|
|
||||||
// Create a CALC first
|
|
||||||
$calcData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'UPCL' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Update Calc Test',
|
|
||||||
'TestType' => $this::TEST_TYPE_CALC,
|
|
||||||
'details' => [
|
|
||||||
'FormulaInput' => '["A", "B"]',
|
|
||||||
'FormulaCode' => 'A + B',
|
|
||||||
'FormulaLang' => 'SQL'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
|
||||||
$createStatus = $createResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
if ($createStatus === 201) {
|
|
||||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
|
||||||
$calcId = $createBody['data']['TestSiteId'] ?? null;
|
|
||||||
|
|
||||||
if ($calcId) {
|
|
||||||
// Update formula
|
|
||||||
$updateData = [
|
|
||||||
'TestSiteName' => 'Updated Calc Test Name',
|
|
||||||
'details' => [
|
|
||||||
'FormulaInput' => '["A", "B", "C"]',
|
|
||||||
'FormulaCode' => 'A + B + C'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$updateResult = $this->put($this->endpoint . '/' . $calcId, ['body' => json_encode($updateData)]);
|
|
||||||
$updateStatus = $updateResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($updateStatus, [200, 400, 500]),
|
|
||||||
"Expected 200, 400, or 500, got $updateStatus"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test CALC has correct TypeCode in response
|
|
||||||
*/
|
|
||||||
public function testCalcTypeCodeInResponse(): void
|
|
||||||
{
|
|
||||||
$indexResult = $this->get($this->endpoint . '?TestType=CALC');
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
|
||||||
$calc = $indexBody['data'][0];
|
|
||||||
|
|
||||||
// Verify TypeCode is CALC
|
|
||||||
$this->assertEquals('CALC', $calc['TypeCode'] ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test CALC details structure
|
|
||||||
*/
|
|
||||||
public function testCalcDetailsStructure(): void
|
|
||||||
{
|
|
||||||
$indexResult = $this->get($this->endpoint . '?TestType=CALC');
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
|
||||||
$calc = $indexBody['data'][0];
|
|
||||||
$calcId = $calc['TestSiteID'] ?? null;
|
|
||||||
|
|
||||||
if ($calcId) {
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $calcId);
|
|
||||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if ($showBody['data'] !== null && isset($showBody['data']['testdefcal'])) {
|
|
||||||
$calcDetails = $showBody['data']['testdefcal'];
|
|
||||||
|
|
||||||
if (is_array($calcDetails) && !empty($calcDetails)) {
|
|
||||||
$firstDetail = $calcDetails[0];
|
|
||||||
|
|
||||||
// Check required fields in calc structure
|
|
||||||
$this->assertArrayHasKey('TestCalID', $firstDetail);
|
|
||||||
$this->assertArrayHasKey('TestSiteID', $firstDetail);
|
|
||||||
$this->assertArrayHasKey('FormulaInput', $firstDetail);
|
|
||||||
$this->assertArrayHasKey('FormulaCode', $firstDetail);
|
|
||||||
|
|
||||||
// Check for joined discipline/department
|
|
||||||
if (isset($firstDetail['DisciplineName'])) {
|
|
||||||
$this->assertArrayHasKey('DepartmentName', $firstDetail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test CALC delete cascades to details
|
|
||||||
*/
|
|
||||||
public function testCalcDeleteCascadesToDetails(): void
|
|
||||||
{
|
|
||||||
// Create a CALC
|
|
||||||
$calcData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'CDEL' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Calc to Delete',
|
|
||||||
'TestType' => $this::TEST_TYPE_CALC,
|
|
||||||
'details' => [
|
|
||||||
'FormulaInput' => '["TEST1"]',
|
|
||||||
'FormulaCode' => 'TEST1 * 2'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
|
||||||
$createStatus = $createResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
if ($createStatus === 201) {
|
|
||||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
|
||||||
$calcId = $createBody['data']['TestSiteId'] ?? null;
|
|
||||||
|
|
||||||
if ($calcId) {
|
|
||||||
// Delete the CALC
|
|
||||||
$deleteResult = $this->delete($this->endpoint . '/' . $calcId);
|
|
||||||
$deleteStatus = $deleteResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($deleteStatus, [200, 404, 500]),
|
|
||||||
"Expected 200, 404, or 500, got $deleteStatus"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($deleteStatus === 200) {
|
|
||||||
// Verify CALC details are also soft deleted
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $calcId);
|
|
||||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
// CALC should show EndDate set
|
|
||||||
if ($showBody['data'] !== null) {
|
|
||||||
$this->assertNotNull($showBody['data']['EndDate']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test CALC with result unit configuration
|
|
||||||
*/
|
|
||||||
public function testCalcWithResultUnit(): void
|
|
||||||
{
|
|
||||||
$units = ['mg/dL', 'g/L', 'mmol/L', '%', 'IU/L'];
|
|
||||||
|
|
||||||
foreach ($units as $unit) {
|
|
||||||
$calcData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'CUNT' . substr(time(), -3) . substr($unit, 0, 1),
|
|
||||||
'TestSiteName' => "Calc with $unit",
|
|
||||||
'TestType' => $this::TEST_TYPE_CALC,
|
|
||||||
'details' => [
|
|
||||||
'Unit1' => $unit,
|
|
||||||
'Decimal' => 2,
|
|
||||||
'FormulaInput' => '["TEST1"]',
|
|
||||||
'FormulaCode' => 'TEST1'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"CALC with unit $unit: Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,291 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\v2\master\TestDef;
|
|
||||||
|
|
||||||
use Tests\Support\v2\MasterTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature tests for GROUP type test definitions
|
|
||||||
*
|
|
||||||
* Tests GROUP-specific functionality including member management
|
|
||||||
*/
|
|
||||||
class TestDefGroupTest extends MasterTestCase
|
|
||||||
{
|
|
||||||
protected string $endpoint = 'v2/master/tests';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test create GROUP with members
|
|
||||||
*/
|
|
||||||
public function testCreateGroupWithMembers(): void
|
|
||||||
{
|
|
||||||
// Get existing test IDs to use as members
|
|
||||||
$memberIds = $this->getExistingTestIds();
|
|
||||||
|
|
||||||
$groupData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'GRUP' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Test Group ' . time(),
|
|
||||||
'TestType' => $this::TEST_TYPE_GROUP,
|
|
||||||
'Description' => 'Group test with members',
|
|
||||||
'SeqScr' => 100,
|
|
||||||
'SeqRpt' => 100,
|
|
||||||
'VisibleScr' => 1,
|
|
||||||
'VisibleRpt' => 1,
|
|
||||||
'CountStat' => 1,
|
|
||||||
'Members' => $memberIds
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
|
||||||
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($status === 201) {
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('created', $body['status']);
|
|
||||||
|
|
||||||
// Verify members were created
|
|
||||||
$groupId = $body['data']['TestSiteId'];
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $groupId);
|
|
||||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if ($showBody['data'] !== null) {
|
|
||||||
$this->assertArrayHasKey('testdefgrp', $showBody['data']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test create GROUP without members
|
|
||||||
*/
|
|
||||||
public function testCreateGroupWithoutMembers(): void
|
|
||||||
{
|
|
||||||
$groupData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'GREM' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Empty Group ' . time(),
|
|
||||||
'TestType' => $this::TEST_TYPE_GROUP,
|
|
||||||
'Members' => [] // Empty members
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
|
||||||
|
|
||||||
// Should still succeed but with warning or empty members
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400]),
|
|
||||||
"Expected 201 or 400, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test update GROUP members
|
|
||||||
*/
|
|
||||||
public function testUpdateGroupMembers(): void
|
|
||||||
{
|
|
||||||
// Create a group first
|
|
||||||
$memberIds = $this->getExistingTestIds();
|
|
||||||
$groupData = $this->createGroupData($memberIds);
|
|
||||||
$groupData['TestSiteCode'] = 'UPMB' . substr(time(), -4);
|
|
||||||
|
|
||||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
|
||||||
$createStatus = $createResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
if ($createStatus === 201) {
|
|
||||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
|
||||||
$groupId = $createBody['data']['TestSiteId'] ?? null;
|
|
||||||
|
|
||||||
if ($groupId) {
|
|
||||||
// Update with new members
|
|
||||||
$updateData = [
|
|
||||||
'Members' => array_slice($memberIds, 0, 1) // Only one member
|
|
||||||
];
|
|
||||||
|
|
||||||
$updateResult = $this->put($this->endpoint . '/' . $groupId, ['body' => json_encode($updateData)]);
|
|
||||||
$updateStatus = $updateResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($updateStatus, [200, 400, 500]),
|
|
||||||
"Expected 200, 400, or 500, got $updateStatus"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test add member to existing GROUP
|
|
||||||
*/
|
|
||||||
public function testAddMemberToGroup(): void
|
|
||||||
{
|
|
||||||
// Get existing test
|
|
||||||
$indexResult = $this->get($this->endpoint . '?TestType=GROUP');
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
|
||||||
$group = $indexBody['data'][0];
|
|
||||||
$groupId = $group['TestSiteID'] ?? null;
|
|
||||||
|
|
||||||
if ($groupId) {
|
|
||||||
// Get a test ID to add
|
|
||||||
$testIds = $this->getExistingTestIds();
|
|
||||||
$newMemberId = $testIds[0] ?? 1;
|
|
||||||
|
|
||||||
$updateData = [
|
|
||||||
'Members' => [$newMemberId]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->put($this->endpoint . '/' . $groupId, ['body' => json_encode($updateData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [200, 400, 500]),
|
|
||||||
"Expected 200, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test GROUP with single member
|
|
||||||
*/
|
|
||||||
public function testGroupWithSingleMember(): void
|
|
||||||
{
|
|
||||||
$memberIds = $this->getExistingTestIds();
|
|
||||||
|
|
||||||
$groupData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'GSGL' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Single Member Group ' . time(),
|
|
||||||
'TestType' => $this::TEST_TYPE_GROUP,
|
|
||||||
'Members' => [array_slice($memberIds, 0, 1)[0] ?? 1]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
|
||||||
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test GROUP members have correct structure
|
|
||||||
*/
|
|
||||||
public function testGroupMembersStructure(): void
|
|
||||||
{
|
|
||||||
$indexResult = $this->get($this->endpoint . '?TestType=GROUP');
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
|
||||||
$group = $indexBody['data'][0];
|
|
||||||
$groupId = $group['TestSiteID'] ?? null;
|
|
||||||
|
|
||||||
if ($groupId) {
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $groupId);
|
|
||||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if ($showBody['data'] !== null && isset($showBody['data']['testdefgrp'])) {
|
|
||||||
$members = $showBody['data']['testdefgrp'];
|
|
||||||
|
|
||||||
if (is_array($members) && !empty($members)) {
|
|
||||||
$firstMember = $members[0];
|
|
||||||
|
|
||||||
// Check required fields in member structure
|
|
||||||
$this->assertArrayHasKey('TestGrpID', $firstMember);
|
|
||||||
$this->assertArrayHasKey('TestSiteID', $firstMember);
|
|
||||||
$this->assertArrayHasKey('Member', $firstMember);
|
|
||||||
|
|
||||||
// Check for joined test details (if loaded)
|
|
||||||
if (isset($firstMember['TestSiteCode'])) {
|
|
||||||
$this->assertArrayHasKey('TestSiteName', $firstMember);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test GROUP delete cascades to members
|
|
||||||
*/
|
|
||||||
public function testGroupDeleteCascadesToMembers(): void
|
|
||||||
{
|
|
||||||
// Create a group
|
|
||||||
$memberIds = $this->getExistingTestIds();
|
|
||||||
$groupData = $this->createGroupData($memberIds);
|
|
||||||
$groupData['TestSiteCode'] = 'GDEL' . substr(time(), -4);
|
|
||||||
|
|
||||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
|
||||||
$createStatus = $createResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
if ($createStatus === 201) {
|
|
||||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
|
||||||
$groupId = $createBody['data']['TestSiteId'] ?? null;
|
|
||||||
|
|
||||||
if ($groupId) {
|
|
||||||
// Delete the group
|
|
||||||
$deleteResult = $this->delete($this->endpoint . '/' . $groupId);
|
|
||||||
$deleteStatus = $deleteResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($deleteStatus, [200, 404, 500]),
|
|
||||||
"Expected 200, 404, or 500, got $deleteStatus"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($deleteStatus === 200) {
|
|
||||||
// Verify group members are also soft deleted
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $groupId);
|
|
||||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
// Group should show EndDate set
|
|
||||||
if ($showBody['data'] !== null) {
|
|
||||||
$this->assertNotNull($showBody['data']['EndDate']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test GROUP type has correct TypeCode in response
|
|
||||||
*/
|
|
||||||
public function testGroupTypeCodeInResponse(): void
|
|
||||||
{
|
|
||||||
$indexResult = $this->get($this->endpoint . '?TestType=GROUP');
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
|
||||||
$group = $indexBody['data'][0];
|
|
||||||
|
|
||||||
// Verify TypeCode is GROUP
|
|
||||||
$this->assertEquals('GROUP', $group['TypeCode'] ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to get existing test IDs
|
|
||||||
*/
|
|
||||||
private function getExistingTestIds(): array
|
|
||||||
{
|
|
||||||
$indexResult = $this->get($this->endpoint);
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
$ids = [];
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data'])) {
|
|
||||||
foreach ($indexBody['data'] as $item) {
|
|
||||||
if (isset($item['TestSiteID'])) {
|
|
||||||
$ids[] = $item['TestSiteID'];
|
|
||||||
}
|
|
||||||
if (count($ids) >= 3) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $ids ?: [1, 2, 3];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,288 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\v2\master\TestDef;
|
|
||||||
|
|
||||||
use Tests\Support\v2\MasterTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature tests for PARAM type test definitions
|
|
||||||
*
|
|
||||||
* Tests PARAM-specific functionality as sub-test components
|
|
||||||
*/
|
|
||||||
class TestDefParamTest extends MasterTestCase
|
|
||||||
{
|
|
||||||
protected string $endpoint = 'v2/master/tests';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test create PARAM type test
|
|
||||||
*/
|
|
||||||
public function testCreateParamTypeTest(): void
|
|
||||||
{
|
|
||||||
$paramData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'PARM' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Parameter Test ' . time(),
|
|
||||||
'TestType' => $this::TEST_TYPE_PARAM,
|
|
||||||
'Description' => 'Parameter/sub-test description',
|
|
||||||
'SeqScr' => 5,
|
|
||||||
'SeqRpt' => 5,
|
|
||||||
'VisibleScr' => 1,
|
|
||||||
'VisibleRpt' => 1,
|
|
||||||
'CountStat' => 1,
|
|
||||||
'details' => [
|
|
||||||
'DisciplineID' => 1,
|
|
||||||
'DepartmentID' => 1,
|
|
||||||
'ResultType' => 1, // Numeric
|
|
||||||
'RefType' => 1, // NMRC
|
|
||||||
'Unit1' => 'unit',
|
|
||||||
'Decimal' => 1,
|
|
||||||
'Method' => 'Parameter Method'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
|
||||||
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($status === 201) {
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('created', $body['status']);
|
|
||||||
|
|
||||||
// Verify tech details were created
|
|
||||||
$paramId = $body['data']['TestSiteId'];
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $paramId);
|
|
||||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if ($showBody['data'] !== null) {
|
|
||||||
$this->assertArrayHasKey('testdeftech', $showBody['data']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test PARAM has correct TypeCode in response
|
|
||||||
*/
|
|
||||||
public function testParamTypeCodeInResponse(): void
|
|
||||||
{
|
|
||||||
$indexResult = $this->get($this->endpoint . '?TestType=PARAM');
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
|
||||||
$param = $indexBody['data'][0];
|
|
||||||
|
|
||||||
// Verify TypeCode is PARAM
|
|
||||||
$this->assertEquals('PARAM', $param['TypeCode'] ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test PARAM details structure
|
|
||||||
*/
|
|
||||||
public function testParamDetailsStructure(): void
|
|
||||||
{
|
|
||||||
$indexResult = $this->get($this->endpoint . '?TestType=PARAM');
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
|
||||||
$param = $indexBody['data'][0];
|
|
||||||
$paramId = $param['TestSiteID'] ?? null;
|
|
||||||
|
|
||||||
if ($paramId) {
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $paramId);
|
|
||||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if ($showBody['data'] !== null && isset($showBody['data']['testdeftech'])) {
|
|
||||||
$techDetails = $showBody['data']['testdeftech'];
|
|
||||||
|
|
||||||
if (is_array($techDetails) && !empty($techDetails)) {
|
|
||||||
$firstDetail = $techDetails[0];
|
|
||||||
|
|
||||||
// Check required fields in tech structure
|
|
||||||
$this->assertArrayHasKey('TestTechID', $firstDetail);
|
|
||||||
$this->assertArrayHasKey('TestSiteID', $firstDetail);
|
|
||||||
$this->assertArrayHasKey('ResultType', $firstDetail);
|
|
||||||
$this->assertArrayHasKey('RefType', $firstDetail);
|
|
||||||
|
|
||||||
// Check for joined discipline/department
|
|
||||||
if (isset($firstDetail['DisciplineName'])) {
|
|
||||||
$this->assertArrayHasKey('DepartmentName', $firstDetail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test PARAM with different result types
|
|
||||||
*/
|
|
||||||
public function testParamWithDifferentResultTypes(): void
|
|
||||||
{
|
|
||||||
$resultTypes = [
|
|
||||||
1 => 'NMRIC', // Numeric
|
|
||||||
2 => 'RANGE', // Range
|
|
||||||
3 => 'TEXT', // Text
|
|
||||||
4 => 'VSET' // Value Set
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($resultTypes as $resultTypeId => $resultTypeName) {
|
|
||||||
$paramData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'PR' . substr(time(), -4) . substr($resultTypeName, 0, 1),
|
|
||||||
'TestSiteName' => "Param with $resultTypeName",
|
|
||||||
'TestType' => $this::TEST_TYPE_PARAM,
|
|
||||||
'details' => [
|
|
||||||
'ResultType' => $resultTypeId,
|
|
||||||
'RefType' => 1
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"PARAM with ResultType $resultTypeName: Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test PARAM with different reference types
|
|
||||||
*/
|
|
||||||
public function testParamWithDifferentRefTypes(): void
|
|
||||||
{
|
|
||||||
$refTypes = [
|
|
||||||
1 => 'NMRC', // Numeric
|
|
||||||
2 => 'TEXT' // Text
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($refTypes as $refTypeId => $refTypeName) {
|
|
||||||
$paramData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'PR' . substr(time(), -4) . 'R' . substr($refTypeName, 0, 1),
|
|
||||||
'TestSiteName' => "Param with RefType $refTypeName",
|
|
||||||
'TestType' => $this::TEST_TYPE_PARAM,
|
|
||||||
'details' => [
|
|
||||||
'ResultType' => 1,
|
|
||||||
'RefType' => $refTypeId
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"PARAM with RefType $refTypeName: Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test PARAM delete cascades to details
|
|
||||||
*/
|
|
||||||
public function testParamDeleteCascadesToDetails(): void
|
|
||||||
{
|
|
||||||
// Create a PARAM
|
|
||||||
$paramData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'PDEL' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Param to Delete',
|
|
||||||
'TestType' => $this::TEST_TYPE_PARAM,
|
|
||||||
'details' => [
|
|
||||||
'ResultType' => 1,
|
|
||||||
'RefType' => 1
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
|
||||||
$createStatus = $createResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
if ($createStatus === 201) {
|
|
||||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
|
||||||
$paramId = $createBody['data']['TestSiteId'] ?? null;
|
|
||||||
|
|
||||||
if ($paramId) {
|
|
||||||
// Delete the PARAM
|
|
||||||
$deleteResult = $this->delete($this->endpoint . '/' . $paramId);
|
|
||||||
$deleteStatus = $deleteResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($deleteStatus, [200, 404, 500]),
|
|
||||||
"Expected 200, 404, or 500, got $deleteStatus"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($deleteStatus === 200) {
|
|
||||||
// Verify PARAM details are also soft deleted
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $paramId);
|
|
||||||
$showBody = json_decode($showResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
// PARAM should show EndDate set
|
|
||||||
if ($showBody['data'] !== null) {
|
|
||||||
$this->assertNotNull($showBody['data']['EndDate']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test PARAM visibility settings
|
|
||||||
*/
|
|
||||||
public function testParamVisibilitySettings(): void
|
|
||||||
{
|
|
||||||
$visibilityCombinations = [
|
|
||||||
['VisibleScr' => 1, 'VisibleRpt' => 1],
|
|
||||||
['VisibleScr' => 1, 'VisibleRpt' => 0],
|
|
||||||
['VisibleScr' => 0, 'VisibleRpt' => 1],
|
|
||||||
['VisibleScr' => 0, 'VisibleRpt' => 0]
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($visibilityCombinations as $vis) {
|
|
||||||
$paramData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'PVIS' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Visibility Test',
|
|
||||||
'TestType' => $this::TEST_TYPE_PARAM,
|
|
||||||
'VisibleScr' => $vis['VisibleScr'],
|
|
||||||
'VisibleRpt' => $vis['VisibleRpt']
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"PARAM visibility ({$vis['VisibleScr']}, {$vis['VisibleRpt']}): Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test PARAM sequence ordering
|
|
||||||
*/
|
|
||||||
public function testParamSequenceOrdering(): void
|
|
||||||
{
|
|
||||||
$paramData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'PSEQ' . substr(time(), -4),
|
|
||||||
'TestSiteName' => 'Sequenced Param',
|
|
||||||
'TestType' => $this::TEST_TYPE_PARAM,
|
|
||||||
'SeqScr' => 25,
|
|
||||||
'SeqRpt' => 30
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,375 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\v2\master\TestDef;
|
|
||||||
|
|
||||||
use Tests\Support\v2\MasterTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature tests for v2 Test Definition API endpoints
|
|
||||||
*
|
|
||||||
* Tests CRUD operations for TEST, PARAM, GROUP, and CALC types
|
|
||||||
*/
|
|
||||||
class TestDefSiteTest extends MasterTestCase
|
|
||||||
{
|
|
||||||
protected string $endpoint = 'v2/master/tests';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test index endpoint returns list of tests
|
|
||||||
*/
|
|
||||||
public function testIndexReturnsTestList(): void
|
|
||||||
{
|
|
||||||
$result = $this->get($this->endpoint);
|
|
||||||
$result->assertStatus(200);
|
|
||||||
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('status', $body);
|
|
||||||
$this->assertArrayHasKey('data', $body);
|
|
||||||
$this->assertArrayHasKey('message', $body);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test index with SiteID filter
|
|
||||||
*/
|
|
||||||
public function testIndexWithSiteFilter(): void
|
|
||||||
{
|
|
||||||
$result = $this->get($this->endpoint . '?SiteID=1');
|
|
||||||
$result->assertStatus(200);
|
|
||||||
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('success', $body['status']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test index with TestType filter
|
|
||||||
*/
|
|
||||||
public function testIndexWithTestTypeFilter(): void
|
|
||||||
{
|
|
||||||
// Filter by TEST type
|
|
||||||
$result = $this->get($this->endpoint . '?TestType=TEST');
|
|
||||||
$result->assertStatus(200);
|
|
||||||
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('success', $body['status']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test index with Visibility filter
|
|
||||||
*/
|
|
||||||
public function testIndexWithVisibilityFilter(): void
|
|
||||||
{
|
|
||||||
$result = $this->get($this->endpoint . '?VisibleScr=1&VisibleRpt=1');
|
|
||||||
$result->assertStatus(200);
|
|
||||||
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('success', $body['status']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test index with keyword search
|
|
||||||
*/
|
|
||||||
public function testIndexWithKeywordSearch(): void
|
|
||||||
{
|
|
||||||
$result = $this->get($this->endpoint . '?TestSiteName=hemoglobin');
|
|
||||||
$result->assertStatus(200);
|
|
||||||
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('success', $body['status']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test show endpoint returns single test
|
|
||||||
*/
|
|
||||||
public function testShowReturnsSingleTest(): void
|
|
||||||
{
|
|
||||||
// First get the list to find a valid ID
|
|
||||||
$indexResult = $this->get($this->endpoint);
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
|
||||||
$firstItem = $indexBody['data'][0];
|
|
||||||
$testSiteID = $firstItem['TestSiteID'] ?? null;
|
|
||||||
|
|
||||||
if ($testSiteID) {
|
|
||||||
$showResult = $this->get($this->endpoint . '/' . $testSiteID);
|
|
||||||
$showResult->assertStatus(200);
|
|
||||||
|
|
||||||
$body = json_decode($showResult->response()->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('data', $body);
|
|
||||||
$this->assertEquals('success', $body['status']);
|
|
||||||
|
|
||||||
// Check that related details are loaded based on TestType
|
|
||||||
if ($body['data'] !== null) {
|
|
||||||
$typeCode = $body['data']['TypeCode'] ?? '';
|
|
||||||
if ($typeCode === 'CALC') {
|
|
||||||
$this->assertArrayHasKey('testdefcal', $body['data']);
|
|
||||||
} elseif ($typeCode === 'GROUP') {
|
|
||||||
$this->assertArrayHasKey('testdefgrp', $body['data']);
|
|
||||||
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
|
||||||
$this->assertArrayHasKey('testdeftech', $body['data']);
|
|
||||||
}
|
|
||||||
// All types should have testmap
|
|
||||||
$this->assertArrayHasKey('testmap', $body['data']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test show with non-existent ID returns null data
|
|
||||||
*/
|
|
||||||
public function testShowWithInvalidIDReturnsNull(): void
|
|
||||||
{
|
|
||||||
$result = $this->get($this->endpoint . '/9999999');
|
|
||||||
$result->assertStatus(200);
|
|
||||||
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertArrayHasKey('data', $body);
|
|
||||||
$this->assertNull($body['data']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test create new TEST type test definition
|
|
||||||
*/
|
|
||||||
public function testCreateTestTypeTest(): void
|
|
||||||
{
|
|
||||||
$testData = $this->createTestData();
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($testData)]);
|
|
||||||
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
// Expect 201 (created) or 400 (validation error) or 500 (server error)
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($status === 201) {
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('created', $body['status']);
|
|
||||||
$this->assertArrayHasKey('TestSiteId', $body['data']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test create new PARAM type test definition
|
|
||||||
*/
|
|
||||||
public function testCreateParamTypeTest(): void
|
|
||||||
{
|
|
||||||
$paramData = $this->createParamData();
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($paramData)]);
|
|
||||||
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test create new GROUP type test definition
|
|
||||||
*/
|
|
||||||
public function testCreateGroupTypeTest(): void
|
|
||||||
{
|
|
||||||
// First create some member tests
|
|
||||||
$memberIds = $this->getExistingTestIds();
|
|
||||||
|
|
||||||
$groupData = $this->createGroupData($memberIds);
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($groupData)]);
|
|
||||||
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test create new CALC type test definition
|
|
||||||
*/
|
|
||||||
public function testCreateCalcTypeTest(): void
|
|
||||||
{
|
|
||||||
$calcData = $this->createCalcData();
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($calcData)]);
|
|
||||||
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [201, 400, 500]),
|
|
||||||
"Expected 201, 400, or 500, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test update existing test
|
|
||||||
*/
|
|
||||||
public function testUpdateTest(): void
|
|
||||||
{
|
|
||||||
$indexResult = $this->get($this->endpoint);
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data']) && !empty($indexBody['data'])) {
|
|
||||||
$firstItem = $indexBody['data'][0];
|
|
||||||
$testSiteID = $firstItem['TestSiteID'] ?? null;
|
|
||||||
|
|
||||||
if ($testSiteID) {
|
|
||||||
$updateData = [
|
|
||||||
'TestSiteName' => 'Updated Test Name ' . time(),
|
|
||||||
'Description' => 'Updated description'
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->put($this->endpoint . '/' . $testSiteID, ['body' => json_encode($updateData)]);
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [200, 404, 500]),
|
|
||||||
"Expected 200, 404, or 500, got $status"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($status === 200) {
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('success', $body['status']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test soft delete (disable) test
|
|
||||||
*/
|
|
||||||
public function testDeleteTest(): void
|
|
||||||
{
|
|
||||||
// Create a test first to delete
|
|
||||||
$testData = $this->createTestData();
|
|
||||||
$testData['TestSiteCode'] = 'DEL' . substr(time(), -4);
|
|
||||||
|
|
||||||
$createResult = $this->post($this->endpoint, ['body' => json_encode($testData)]);
|
|
||||||
$createStatus = $createResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
if ($createStatus === 201) {
|
|
||||||
$createBody = json_decode($createResult->response()->getBody(), true);
|
|
||||||
$testSiteID = $createBody['data']['TestSiteId'] ?? null;
|
|
||||||
|
|
||||||
if ($testSiteID) {
|
|
||||||
$deleteResult = $this->delete($this->endpoint . '/' . $testSiteID);
|
|
||||||
$deleteStatus = $deleteResult->response()->getStatusCode();
|
|
||||||
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($deleteStatus, [200, 404, 500]),
|
|
||||||
"Expected 200, 404, or 500, got $deleteStatus"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($deleteStatus === 200) {
|
|
||||||
$deleteBody = json_decode($deleteResult->response()->getBody(), true);
|
|
||||||
$this->assertEquals('success', $deleteBody['status']);
|
|
||||||
$this->assertArrayHasKey('EndDate', $deleteBody['data']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test validation - missing required fields
|
|
||||||
*/
|
|
||||||
public function testCreateValidationRequiredFields(): void
|
|
||||||
{
|
|
||||||
$invalidData = [
|
|
||||||
'TestSiteName' => 'Test without required fields'
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
|
||||||
$result->assertStatus(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that TestSiteCode is max 6 characters
|
|
||||||
*/
|
|
||||||
public function testTestSiteCodeLength(): void
|
|
||||||
{
|
|
||||||
$invalidData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'HB123456', // 8 characters - invalid
|
|
||||||
'TestSiteName' => 'Test with too long code',
|
|
||||||
'TestType' => $this::TEST_TYPE_TEST
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
|
||||||
$result->assertStatus(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that TestSiteCode is at least 3 characters
|
|
||||||
*/
|
|
||||||
public function testTestSiteCodeMinLength(): void
|
|
||||||
{
|
|
||||||
$invalidData = [
|
|
||||||
'SiteID' => 1,
|
|
||||||
'TestSiteCode' => 'HB', // 2 characters - invalid
|
|
||||||
'TestSiteName' => 'Test with too short code',
|
|
||||||
'TestType' => $this::TEST_TYPE_TEST
|
|
||||||
];
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
|
||||||
$result->assertStatus(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that duplicate TestSiteCode is rejected
|
|
||||||
*/
|
|
||||||
public function testDuplicateTestSiteCode(): void
|
|
||||||
{
|
|
||||||
// First create a test
|
|
||||||
$testData = $this->createTestData();
|
|
||||||
$testData['TestSiteCode'] = 'DUP' . substr(time(), -3);
|
|
||||||
|
|
||||||
$this->post($this->endpoint, ['body' => json_encode($testData)]);
|
|
||||||
|
|
||||||
// Try to create another test with the same code
|
|
||||||
$duplicateData = $testData;
|
|
||||||
$duplicateData['TestSiteName'] = 'Different Name';
|
|
||||||
|
|
||||||
$result = $this->post($this->endpoint, ['body' => json_encode($duplicateData)]);
|
|
||||||
|
|
||||||
// Should fail with 400 or 500
|
|
||||||
$status = $result->response()->getStatusCode();
|
|
||||||
$this->assertTrue(
|
|
||||||
in_array($status, [400, 500]),
|
|
||||||
"Expected 400 or 500 for duplicate, got $status"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test filtering by multiple parameters
|
|
||||||
*/
|
|
||||||
public function testIndexWithMultipleFilters(): void
|
|
||||||
{
|
|
||||||
$result = $this->get($this->endpoint . '?SiteID=1&TestType=TEST&VisibleScr=1');
|
|
||||||
$result->assertStatus(200);
|
|
||||||
|
|
||||||
$body = json_decode($result->response()->getBody(), true);
|
|
||||||
$this->assertEquals('success', $body['status']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to get existing test IDs for group members
|
|
||||||
*/
|
|
||||||
private function getExistingTestIds(): array
|
|
||||||
{
|
|
||||||
$indexResult = $this->get($this->endpoint);
|
|
||||||
$indexBody = json_decode($indexResult->response()->getBody(), true);
|
|
||||||
|
|
||||||
$ids = [];
|
|
||||||
if (isset($indexBody['data']) && is_array($indexBody['data'])) {
|
|
||||||
foreach ($indexBody['data'] as $item) {
|
|
||||||
if (isset($item['TestSiteID'])) {
|
|
||||||
$ids[] = $item['TestSiteID'];
|
|
||||||
}
|
|
||||||
if (count($ids) >= 2) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $ids ?: [1, 2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -34,13 +34,11 @@ final class HealthTest extends CIUnitTestCase
|
|||||||
$validation->check($config->baseURL, 'valid_url'),
|
$validation->check($config->baseURL, 'valid_url'),
|
||||||
'baseURL "' . $config->baseURL . '" in .env is not valid URL',
|
'baseURL "' . $config->baseURL . '" in .env is not valid URL',
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the baseURL in app/Config/App.php
|
// If no baseURL in .env, check app/Config/App.php
|
||||||
// You can't use Config\App, because phpunit.xml.dist sets app.baseURL
|
|
||||||
$reader = new ConfigReader();
|
$reader = new ConfigReader();
|
||||||
|
|
||||||
// BaseURL in app/Config/App.php is a valid URL?
|
|
||||||
$this->assertTrue(
|
$this->assertTrue(
|
||||||
$validation->check($reader->baseURL, 'valid_url'),
|
$validation->check($reader->baseURL, 'valid_url'),
|
||||||
'baseURL "' . $reader->baseURL . '" in app/Config/App.php is not valid URL',
|
'baseURL "' . $reader->baseURL . '" in app/Config/App.php is not valid URL',
|
||||||
|
|||||||
@ -358,14 +358,16 @@ class ValueSetTest extends CIUnitTestCase
|
|||||||
['Gender' => '1', 'Country' => 'IDN'],
|
['Gender' => '1', 'Country' => 'IDN'],
|
||||||
['Gender' => '2', 'Country' => 'USA']
|
['Gender' => '2', 'Country' => 'USA']
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = ValueSet::transformLabels($data, [
|
$result = ValueSet::transformLabels($data, [
|
||||||
'Gender' => 'sex',
|
'Gender' => 'sex',
|
||||||
'Country' => 'country'
|
'Country' => 'country'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertEquals('Female', $result[0]['GenderText']);
|
$this->assertEquals('Female', $result[0]['Gender']);
|
||||||
$this->assertEquals('Male', $result[1]['GenderText']);
|
$this->assertEquals('1', $result[0]['GenderKey']);
|
||||||
|
$this->assertEquals('Male', $result[1]['Gender']);
|
||||||
|
$this->assertEquals('2', $result[1]['GenderKey']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetOptions()
|
public function testGetOptions()
|
||||||
|
|||||||
@ -1,145 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\v2\master\TestDef;
|
|
||||||
|
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
|
||||||
use App\Models\Test\TestDefCalModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for TestDefCalModel
|
|
||||||
*
|
|
||||||
* Tests the calculation definition model for CALC type tests
|
|
||||||
*/
|
|
||||||
class TestDefCalModelTest extends CIUnitTestCase
|
|
||||||
{
|
|
||||||
protected TestDefCalModel $model;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->model = new TestDefCalModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct table name
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectTableName(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('testdefcal', $this->model->table);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct primary key
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectPrimaryKey(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('TestCalID', $this->model->primaryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses soft deletes
|
|
||||||
*/
|
|
||||||
public function testModelUsesSoftDeletes(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useSoftDeletes);
|
|
||||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct allowed fields
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectAllowedFields(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
|
|
||||||
// Foreign key
|
|
||||||
$this->assertContains('TestSiteID', $allowedFields);
|
|
||||||
|
|
||||||
// Calculation fields
|
|
||||||
$this->assertContains('DisciplineID', $allowedFields);
|
|
||||||
$this->assertContains('DepartmentID', $allowedFields);
|
|
||||||
$this->assertContains('FormulaInput', $allowedFields);
|
|
||||||
$this->assertContains('FormulaCode', $allowedFields);
|
|
||||||
|
|
||||||
// Result fields
|
|
||||||
$this->assertContains('RefType', $allowedFields);
|
|
||||||
$this->assertContains('Unit1', $allowedFields);
|
|
||||||
$this->assertContains('Factor', $allowedFields);
|
|
||||||
$this->assertContains('Unit2', $allowedFields);
|
|
||||||
$this->assertContains('Decimal', $allowedFields);
|
|
||||||
$this->assertContains('Method', $allowedFields);
|
|
||||||
|
|
||||||
// Timestamp fields
|
|
||||||
$this->assertContains('CreateDate', $allowedFields);
|
|
||||||
$this->assertContains('EndDate', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses timestamps
|
|
||||||
*/
|
|
||||||
public function testModelUsesTimestamps(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useTimestamps);
|
|
||||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model return type is array
|
|
||||||
*/
|
|
||||||
public function testModelReturnTypeIsArray(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('array', $this->model->returnType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct skip validation
|
|
||||||
*/
|
|
||||||
public function testModelSkipValidation(): void
|
|
||||||
{
|
|
||||||
$this->assertFalse($this->model->skipValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct useAutoIncrement
|
|
||||||
*/
|
|
||||||
public function testModelUseAutoIncrement(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useAutoIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test FormulaInput field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testFormulaInputFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('FormulaInput', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test FormulaCode field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testFormulaCodeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('FormulaCode', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test RefType field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testRefTypeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('RefType', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Method field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testMethodFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('Method', $allowedFields);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\v2\master\TestDef;
|
|
||||||
|
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
|
||||||
use App\Models\Test\TestDefGrpModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for TestDefGrpModel
|
|
||||||
*
|
|
||||||
* Tests the group definition model for GROUP type tests
|
|
||||||
*/
|
|
||||||
class TestDefGrpModelTest extends CIUnitTestCase
|
|
||||||
{
|
|
||||||
protected TestDefGrpModel $model;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->model = new TestDefGrpModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct table name
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectTableName(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('testdefgrp', $this->model->table);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct primary key
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectPrimaryKey(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('TestGrpID', $this->model->primaryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses soft deletes
|
|
||||||
*/
|
|
||||||
public function testModelUsesSoftDeletes(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useSoftDeletes);
|
|
||||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct allowed fields
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectAllowedFields(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
|
|
||||||
// Foreign keys
|
|
||||||
$this->assertContains('TestSiteID', $allowedFields);
|
|
||||||
$this->assertContains('Member', $allowedFields);
|
|
||||||
|
|
||||||
// Timestamp fields
|
|
||||||
$this->assertContains('CreateDate', $allowedFields);
|
|
||||||
$this->assertContains('EndDate', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses timestamps
|
|
||||||
*/
|
|
||||||
public function testModelUsesTimestamps(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useTimestamps);
|
|
||||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model return type is array
|
|
||||||
*/
|
|
||||||
public function testModelReturnTypeIsArray(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('array', $this->model->returnType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct skip validation
|
|
||||||
*/
|
|
||||||
public function testModelSkipValidation(): void
|
|
||||||
{
|
|
||||||
$this->assertFalse($this->model->skipValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct useAutoIncrement
|
|
||||||
*/
|
|
||||||
public function testModelUseAutoIncrement(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useAutoIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test TestSiteID field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testTestSiteIDFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('TestSiteID', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Member field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testMemberFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('Member', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test CreateDate field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testCreateDateFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('CreateDate', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test EndDate field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testEndDateFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('EndDate', $allowedFields);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\v2\master\TestDef;
|
|
||||||
|
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
|
||||||
use App\Models\Test\TestDefSiteModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for TestDefSiteModel - Master Data Tests CRUD operations
|
|
||||||
*
|
|
||||||
* Tests the model configuration and behavior for test definition management
|
|
||||||
*/
|
|
||||||
class TestDefSiteModelMasterTest extends CIUnitTestCase
|
|
||||||
{
|
|
||||||
protected TestDefSiteModel $model;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->model = new TestDefSiteModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct table name
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectTableName(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('testdefsite', $this->model->table);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct primary key
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectPrimaryKey(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('TestSiteID', $this->model->primaryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses soft deletes
|
|
||||||
*/
|
|
||||||
public function testModelUsesSoftDeletes(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useSoftDeletes);
|
|
||||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct allowed fields for master data
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectAllowedFields(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
|
|
||||||
// Core required fields
|
|
||||||
$this->assertContains('SiteID', $allowedFields);
|
|
||||||
$this->assertContains('TestSiteCode', $allowedFields);
|
|
||||||
$this->assertContains('TestSiteName', $allowedFields);
|
|
||||||
$this->assertContains('TestType', $allowedFields);
|
|
||||||
|
|
||||||
// Display and ordering fields
|
|
||||||
$this->assertContains('Description', $allowedFields);
|
|
||||||
$this->assertContains('SeqScr', $allowedFields);
|
|
||||||
$this->assertContains('SeqRpt', $allowedFields);
|
|
||||||
$this->assertContains('IndentLeft', $allowedFields);
|
|
||||||
$this->assertContains('FontStyle', $allowedFields);
|
|
||||||
|
|
||||||
// Visibility fields
|
|
||||||
$this->assertContains('VisibleScr', $allowedFields);
|
|
||||||
$this->assertContains('VisibleRpt', $allowedFields);
|
|
||||||
$this->assertContains('CountStat', $allowedFields);
|
|
||||||
|
|
||||||
// Timestamp fields
|
|
||||||
$this->assertContains('CreateDate', $allowedFields);
|
|
||||||
$this->assertContains('StartDate', $allowedFields);
|
|
||||||
$this->assertContains('EndDate', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses timestamps
|
|
||||||
*/
|
|
||||||
public function testModelUsesTimestamps(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useTimestamps);
|
|
||||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
|
||||||
$this->assertEquals('StartDate', $this->model->updatedField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model return type is array
|
|
||||||
*/
|
|
||||||
public function testModelReturnTypeIsArray(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('array', $this->model->returnType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct skip validation
|
|
||||||
*/
|
|
||||||
public function testModelSkipValidation(): void
|
|
||||||
{
|
|
||||||
$this->assertFalse($this->model->skipValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct useAutoIncrement
|
|
||||||
*/
|
|
||||||
public function testModelUseAutoIncrement(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useAutoIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test TestSiteCode field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testTestSiteCodeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('TestSiteCode', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test TestSiteName field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testTestSiteNameFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('TestSiteName', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test TestType field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testTestTypeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('TestType', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test SiteID field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testSiteIDFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('SiteID', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Description field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testDescriptionFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('Description', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test SeqScr field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testSeqScrFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('SeqScr', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test SeqRpt field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testSeqRptFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('SeqRpt', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test VisibleScr field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testVisibleScrFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('VisibleScr', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test VisibleRpt field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testVisibleRptFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('VisibleRpt', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test CountStat field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testCountStatFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('CountStat', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test getTests method exists and is callable
|
|
||||||
*/
|
|
||||||
public function testGetTestsMethodExists(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(method_exists($this->model, 'getTests'));
|
|
||||||
$this->assertIsCallable([$this->model, 'getTests']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test getTest method exists and is callable
|
|
||||||
*/
|
|
||||||
public function testGetTestMethodExists(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(method_exists($this->model, 'getTest'));
|
|
||||||
$this->assertIsCallable([$this->model, 'getTest']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\v2\master\TestDef;
|
|
||||||
|
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
|
||||||
use App\Models\Test\TestDefSiteModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for TestDefSiteModel
|
|
||||||
*
|
|
||||||
* Tests the main test definition model configuration and behavior
|
|
||||||
*/
|
|
||||||
class TestDefSiteModelTest extends CIUnitTestCase
|
|
||||||
{
|
|
||||||
protected TestDefSiteModel $model;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->model = new TestDefSiteModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct table name
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectTableName(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('testdefsite', $this->model->table);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct primary key
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectPrimaryKey(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('TestSiteID', $this->model->primaryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses soft deletes
|
|
||||||
*/
|
|
||||||
public function testModelUsesSoftDeletes(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useSoftDeletes);
|
|
||||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct allowed fields
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectAllowedFields(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
|
|
||||||
// Core required fields
|
|
||||||
$this->assertContains('SiteID', $allowedFields);
|
|
||||||
$this->assertContains('TestSiteCode', $allowedFields);
|
|
||||||
$this->assertContains('TestSiteName', $allowedFields);
|
|
||||||
$this->assertContains('TestType', $allowedFields);
|
|
||||||
|
|
||||||
// Optional fields
|
|
||||||
$this->assertContains('Description', $allowedFields);
|
|
||||||
$this->assertContains('SeqScr', $allowedFields);
|
|
||||||
$this->assertContains('SeqRpt', $allowedFields);
|
|
||||||
$this->assertContains('IndentLeft', $allowedFields);
|
|
||||||
$this->assertContains('FontStyle', $allowedFields);
|
|
||||||
$this->assertContains('VisibleScr', $allowedFields);
|
|
||||||
$this->assertContains('VisibleRpt', $allowedFields);
|
|
||||||
$this->assertContains('CountStat', $allowedFields);
|
|
||||||
|
|
||||||
// Timestamp fields
|
|
||||||
$this->assertContains('CreateDate', $allowedFields);
|
|
||||||
$this->assertContains('StartDate', $allowedFields);
|
|
||||||
$this->assertContains('EndDate', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses timestamps
|
|
||||||
*/
|
|
||||||
public function testModelUsesTimestamps(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useTimestamps);
|
|
||||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
|
||||||
$this->assertEquals('StartDate', $this->model->updatedField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model return type is array
|
|
||||||
*/
|
|
||||||
public function testModelReturnTypeIsArray(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('array', $this->model->returnType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct skip validation
|
|
||||||
*/
|
|
||||||
public function testModelSkipValidation(): void
|
|
||||||
{
|
|
||||||
$this->assertFalse($this->model->skipValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct useAutoIncrement
|
|
||||||
*/
|
|
||||||
public function testModelUseAutoIncrement(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useAutoIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test TestSiteCode field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testTestSiteCodeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('TestSiteCode', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test TestSiteName field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testTestSiteNameFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('TestSiteName', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test TestType field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testTestTypeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('TestType', $allowedFields);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,160 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\v2\master\TestDef;
|
|
||||||
|
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
|
||||||
use App\Models\Test\TestDefTechModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for TestDefTechModel
|
|
||||||
*
|
|
||||||
* Tests the technical definition model for TEST and PARAM types
|
|
||||||
*/
|
|
||||||
class TestDefTechModelTest extends CIUnitTestCase
|
|
||||||
{
|
|
||||||
protected TestDefTechModel $model;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->model = new TestDefTechModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct table name
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectTableName(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('testdeftech', $this->model->table);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct primary key
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectPrimaryKey(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('TestTechID', $this->model->primaryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses soft deletes
|
|
||||||
*/
|
|
||||||
public function testModelUsesSoftDeletes(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useSoftDeletes);
|
|
||||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct allowed fields
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectAllowedFields(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
|
|
||||||
// Foreign key
|
|
||||||
$this->assertContains('TestSiteID', $allowedFields);
|
|
||||||
|
|
||||||
// Technical fields
|
|
||||||
$this->assertContains('DisciplineID', $allowedFields);
|
|
||||||
$this->assertContains('DepartmentID', $allowedFields);
|
|
||||||
$this->assertContains('ResultType', $allowedFields);
|
|
||||||
$this->assertContains('RefType', $allowedFields);
|
|
||||||
$this->assertContains('VSet', $allowedFields);
|
|
||||||
|
|
||||||
// Quantity and units
|
|
||||||
$this->assertContains('ReqQty', $allowedFields);
|
|
||||||
$this->assertContains('ReqQtyUnit', $allowedFields);
|
|
||||||
$this->assertContains('Unit1', $allowedFields);
|
|
||||||
$this->assertContains('Factor', $allowedFields);
|
|
||||||
$this->assertContains('Unit2', $allowedFields);
|
|
||||||
$this->assertContains('Decimal', $allowedFields);
|
|
||||||
|
|
||||||
// Collection and method
|
|
||||||
$this->assertContains('CollReq', $allowedFields);
|
|
||||||
$this->assertContains('Method', $allowedFields);
|
|
||||||
$this->assertContains('ExpectedTAT', $allowedFields);
|
|
||||||
|
|
||||||
// Timestamp fields
|
|
||||||
$this->assertContains('CreateDate', $allowedFields);
|
|
||||||
$this->assertContains('EndDate', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses timestamps
|
|
||||||
*/
|
|
||||||
public function testModelUsesTimestamps(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useTimestamps);
|
|
||||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model return type is array
|
|
||||||
*/
|
|
||||||
public function testModelReturnTypeIsArray(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('array', $this->model->returnType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct skip validation
|
|
||||||
*/
|
|
||||||
public function testModelSkipValidation(): void
|
|
||||||
{
|
|
||||||
$this->assertFalse($this->model->skipValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct useAutoIncrement
|
|
||||||
*/
|
|
||||||
public function testModelUseAutoIncrement(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useAutoIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test TestSiteID field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testTestSiteIDFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('TestSiteID', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test ResultType field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testResultTypeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('ResultType', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test RefType field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testRefTypeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('RefType', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Unit1 field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testUnit1FieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('Unit1', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Method field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testMethodFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('Method', $allowedFields);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\v2\master\TestDef;
|
|
||||||
|
|
||||||
use CodeIgniter\Test\CIUnitTestCase;
|
|
||||||
use App\Models\Test\TestMapModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for TestMapModel
|
|
||||||
*
|
|
||||||
* Tests the test mapping model for all test types
|
|
||||||
*/
|
|
||||||
class TestMapModelTest extends CIUnitTestCase
|
|
||||||
{
|
|
||||||
protected TestMapModel $model;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
parent::setUp();
|
|
||||||
$this->model = new TestMapModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct table name
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectTableName(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('testmap', $this->model->table);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct primary key
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectPrimaryKey(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('TestMapID', $this->model->primaryKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses soft deletes
|
|
||||||
*/
|
|
||||||
public function testModelUsesSoftDeletes(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useSoftDeletes);
|
|
||||||
$this->assertEquals('EndDate', $this->model->deletedField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct allowed fields
|
|
||||||
*/
|
|
||||||
public function testModelHasCorrectAllowedFields(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
|
|
||||||
// Foreign key
|
|
||||||
$this->assertContains('TestSiteID', $allowedFields);
|
|
||||||
|
|
||||||
// Host system mapping
|
|
||||||
$this->assertContains('HostType', $allowedFields);
|
|
||||||
$this->assertContains('HostID', $allowedFields);
|
|
||||||
$this->assertContains('HostDataSource', $allowedFields);
|
|
||||||
$this->assertContains('HostTestCode', $allowedFields);
|
|
||||||
$this->assertContains('HostTestName', $allowedFields);
|
|
||||||
|
|
||||||
// Client system mapping
|
|
||||||
$this->assertContains('ClientType', $allowedFields);
|
|
||||||
$this->assertContains('ClientID', $allowedFields);
|
|
||||||
$this->assertContains('ClientDataSource', $allowedFields);
|
|
||||||
$this->assertContains('ConDefID', $allowedFields);
|
|
||||||
$this->assertContains('ClientTestCode', $allowedFields);
|
|
||||||
$this->assertContains('ClientTestName', $allowedFields);
|
|
||||||
|
|
||||||
// Timestamp fields
|
|
||||||
$this->assertContains('CreateDate', $allowedFields);
|
|
||||||
$this->assertContains('EndDate', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model uses timestamps
|
|
||||||
*/
|
|
||||||
public function testModelUsesTimestamps(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useTimestamps);
|
|
||||||
$this->assertEquals('CreateDate', $this->model->createdField);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model return type is array
|
|
||||||
*/
|
|
||||||
public function testModelReturnTypeIsArray(): void
|
|
||||||
{
|
|
||||||
$this->assertEquals('array', $this->model->returnType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct skip validation
|
|
||||||
*/
|
|
||||||
public function testModelSkipValidation(): void
|
|
||||||
{
|
|
||||||
$this->assertFalse($this->model->skipValidation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test model has correct useAutoIncrement
|
|
||||||
*/
|
|
||||||
public function testModelUseAutoIncrement(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue($this->model->useAutoIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test HostType field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testHostTypeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('HostType', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test HostID field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testHostIDFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('HostID', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test HostTestCode field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testHostTestCodeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('HostTestCode', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test ClientType field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testClientTypeFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('ClientType', $allowedFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test TestSiteID field is in allowed fields
|
|
||||||
*/
|
|
||||||
public function testTestSiteIDFieldExists(): void
|
|
||||||
{
|
|
||||||
$allowedFields = $this->model->allowedFields;
|
|
||||||
$this->assertContains('TestSiteID', $allowedFields);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user