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
|
||||
/public/.htaccess
|
||||
|
||||
#-------------------------
|
||||
# Claude
|
||||
#-------------------------
|
||||
.claude
|
||||
.serena/
|
||||
.claude/
|
||||
@ -54,7 +54,10 @@ $routes->group('v2', ['filter' => 'auth'], function ($routes) {
|
||||
|
||||
// Master Data - Tests & ValueSets
|
||||
$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
|
||||
@ -161,12 +164,22 @@ $routes->group('api', function ($routes) {
|
||||
$routes->delete('items/(:num)', 'ValueSetController::deleteItem/$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');
|
||||
$routes->group('result', function ($routes) {
|
||||
$routes->group('valueset', function ($routes) {
|
||||
$routes->get('/', 'Result\ResultValueSetController::index');
|
||||
$routes->get('(:num)', 'Result\ResultValueSetController::show/$1');
|
||||
$routes->post('/', 'Result\ResultValueSetController::create');
|
||||
$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
|
||||
|
||||
@ -32,16 +32,21 @@ class AreaGeoController extends BaseController {
|
||||
|
||||
public function getProvinces() {
|
||||
$rows = $this->model->getProvinces();
|
||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "data not found", 'data' => '' ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
$transformed = array_map(function($row) {
|
||||
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() {
|
||||
$filter = [ 'Parent' => $this->request->getVar('Parent') ?? null ];
|
||||
$rows = $this->model->getCities($filter);
|
||||
|
||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "data not found", 'data' => [] ], 200); }
|
||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||
$transformed = array_map(function($row) {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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', [
|
||||
'pageTitle' => 'Value Sets',
|
||||
'activePage' => 'master-valuesets'
|
||||
return view('v2/valueset/valueset_index', [
|
||||
'pageTitle' => 'Value Set Library',
|
||||
'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) {
|
||||
return [
|
||||
'RefNumID' => $r['RefNumID'],
|
||||
'NumRefType' => $r['NumRefType'],
|
||||
'NumRefTypeVValue' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']),
|
||||
'RangeTypeVValue' => ValueSet::getLabel('range_type', $r['RangeType']),
|
||||
'SexVValue' => ValueSet::getLabel('gender', $r['Sex']),
|
||||
'LowSignVValue' => ValueSet::getLabel('math_sign', $r['LowSign']),
|
||||
'HighSignVValue' => ValueSet::getLabel('math_sign', $r['HighSign']),
|
||||
'NumRefTypeKey' => $r['NumRefType'],
|
||||
'NumRefType' => ValueSet::getLabel('numeric_ref_type', $r['NumRefType']),
|
||||
'RangeType' => ValueSet::getLabel('range_type', $r['RangeType']),
|
||||
'SexKey' => $r['Sex'],
|
||||
'Sex' => ValueSet::getLabel('gender', $r['Sex']),
|
||||
'LowSign' => ValueSet::getLabel('math_sign', $r['LowSign']),
|
||||
'HighSign' => ValueSet::getLabel('math_sign', $r['HighSign']),
|
||||
'High' => $r['High'] !== null ? (int) $r['High'] : null,
|
||||
'Flag' => $r['Flag']
|
||||
];
|
||||
@ -183,10 +184,10 @@ class TestsController extends BaseController
|
||||
$row['reftxt'] = array_map(function ($r) {
|
||||
return [
|
||||
'RefTxtID' => $r['RefTxtID'],
|
||||
'TxtRefType' => $r['TxtRefType'],
|
||||
'TxtRefTypeVValue' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']),
|
||||
'Sex' => $r['Sex'],
|
||||
'SexVValue' => ValueSet::getLabel('gender', $r['Sex']),
|
||||
'TxtRefTypeKey' => $r['TxtRefType'],
|
||||
'TxtRefType' => ValueSet::getLabel('text_ref_type', $r['TxtRefType']),
|
||||
'SexKey' => $r['Sex'],
|
||||
'Sex' => ValueSet::getLabel('gender', $r['Sex']),
|
||||
'AgeStart' => (int) $r['AgeStart'],
|
||||
'AgeEnd' => (int) $r['AgeEnd'],
|
||||
'RefTxt' => $r['RefTxt'],
|
||||
|
||||
@ -19,10 +19,19 @@ class ValueSetController extends \CodeIgniter\Controller
|
||||
|
||||
public function index(?string $lookupName = null)
|
||||
{
|
||||
$search = $this->request->getGet('search') ?? null;
|
||||
|
||||
if ($lookupName === null) {
|
||||
$all = ValueSet::getAll();
|
||||
$result = [];
|
||||
foreach ($all as $name => $entry) {
|
||||
if ($search) {
|
||||
$nameLower = strtolower($name);
|
||||
$searchLower = strtolower($search);
|
||||
if (strpos($nameLower, $searchLower) === false) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$count = count($entry['values'] ?? []);
|
||||
$result[$name] = $count;
|
||||
}
|
||||
|
||||
@ -68,7 +68,9 @@ class ValueSet {
|
||||
foreach ($data as &$row) {
|
||||
foreach ($fieldMappings as $field => $lookupName) {
|
||||
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 = ValueSet::transformLabels($rows, [
|
||||
'Sex' => 'gender',
|
||||
'Sex' => 'sex',
|
||||
]);
|
||||
return $rows;
|
||||
}
|
||||
@ -81,7 +81,7 @@ class PatientModel extends BaseModel {
|
||||
unset($patient['Comment']);
|
||||
|
||||
$patient = ValueSet::transformLabels([$patient], [
|
||||
'Sex' => 'gender',
|
||||
'Sex' => 'sex',
|
||||
'Country' => 'country',
|
||||
'Race' => 'race',
|
||||
'Religion' => 'religion',
|
||||
|
||||
@ -173,14 +173,39 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Value Sets -->
|
||||
<!-- Value Sets (Nested Group) -->
|
||||
<li>
|
||||
<a href="<?= base_url('/v2/master/valuesets') ?>"
|
||||
:class="isActive('master/valuesets') ? 'active' : ''"
|
||||
class="group">
|
||||
<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>
|
||||
</a>
|
||||
<div x-data="{
|
||||
isOpen: valuesetOpen,
|
||||
toggle() { this.isOpen = !this.isOpen; $root.layout().valuesetOpen = this.isOpen }
|
||||
}" x-init="$watch('valuesetOpen', v => isOpen = v)">
|
||||
<button @click="isOpen = !isOpen"
|
||||
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>
|
||||
|
||||
<!-- Settings -->
|
||||
@ -319,6 +344,7 @@
|
||||
lightMode: localStorage.getItem('theme') !== 'dark',
|
||||
orgOpen: false,
|
||||
specimenOpen: false,
|
||||
valuesetOpen: false,
|
||||
currentPath: window.location.pathname,
|
||||
|
||||
init() {
|
||||
@ -333,6 +359,7 @@
|
||||
// Auto-expand menus based on active path
|
||||
this.orgOpen = this.currentPath.includes('organization');
|
||||
this.specimenOpen = this.currentPath.includes('specimen');
|
||||
this.valuesetOpen = this.currentPath.includes('valueset');
|
||||
|
||||
// Watch sidebar state to persist
|
||||
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
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
@ -21,7 +21,6 @@
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<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));">
|
||||
<i class="fa-solid fa-list-plus" style="color: rgb(var(--color-primary));"></i>
|
||||
@ -32,10 +31,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<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 class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-exclamation-triangle text-rose-500"></i>
|
||||
@ -43,8 +40,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Selection (only show if no selectedDef) -->
|
||||
<div x-show="!selectedDef" 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));">Category Assignment</h4>
|
||||
|
||||
<div>
|
||||
@ -63,7 +59,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Information Section -->
|
||||
<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>
|
||||
|
||||
@ -109,7 +104,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Information Section -->
|
||||
<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>
|
||||
|
||||
@ -143,7 +137,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<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-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
|
||||
x-show="showModal"
|
||||
x-cloak
|
||||
@ -21,7 +21,6 @@
|
||||
x-transition:leave-start="opacity-100 transform scale-100"
|
||||
x-transition:leave-end="opacity-0 transform scale-95"
|
||||
>
|
||||
<!-- Header -->
|
||||
<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));">
|
||||
<i class="fa-solid fa-layer-group-plus" style="color: rgb(var(--color-primary));"></i>
|
||||
@ -32,10 +31,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<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 class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-exclamation-triangle text-rose-500"></i>
|
||||
@ -43,7 +40,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Information Section -->
|
||||
<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>
|
||||
|
||||
@ -75,7 +71,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Info Section -->
|
||||
<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>
|
||||
|
||||
@ -109,7 +104,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<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-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'),
|
||||
'baseURL "' . $config->baseURL . '" in .env is not valid URL',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the baseURL in app/Config/App.php
|
||||
// You can't use Config\App, because phpunit.xml.dist sets app.baseURL
|
||||
// If no baseURL in .env, check app/Config/App.php
|
||||
$reader = new ConfigReader();
|
||||
|
||||
// BaseURL in app/Config/App.php is a valid URL?
|
||||
$this->assertTrue(
|
||||
$validation->check($reader->baseURL, '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' => '2', 'Country' => 'USA']
|
||||
];
|
||||
|
||||
|
||||
$result = ValueSet::transformLabels($data, [
|
||||
'Gender' => 'sex',
|
||||
'Country' => 'country'
|
||||
]);
|
||||
|
||||
$this->assertEquals('Female', $result[0]['GenderText']);
|
||||
$this->assertEquals('Male', $result[1]['GenderText']);
|
||||
|
||||
$this->assertEquals('Female', $result[0]['Gender']);
|
||||
$this->assertEquals('1', $result[0]['GenderKey']);
|
||||
$this->assertEquals('Male', $result[1]['Gender']);
|
||||
$this->assertEquals('2', $result[1]['GenderKey']);
|
||||
}
|
||||
|
||||
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