feat: enhance Test Management module with improved UI and tests
- Refactor Tests.php controller with updated logic and error handling - Update Test migration with schema improvements - Enhance TestDefCalModel, TestDefGrpModel, TestDefTechModel with CRUD operations - Improve TestMapModel with better test mapping relationships - Redesign test dialog views (calc, group, param) with improved UX - Update tests_index view with better data presentation - Add CSS styles for test management UI components - Add TestDefSiteTest feature test for site-based test definitions - Add TestDefModelsTest unit test for model validation - Remove obsolete Test Management.docx documentation
This commit is contained in:
parent
97451496c3
commit
97edfe50a8
Binary file not shown.
@ -35,7 +35,8 @@ class Tests extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/tests
|
||||
* GET /v1/tests
|
||||
* GET /v1/tests/site
|
||||
* List all tests with optional filtering
|
||||
*/
|
||||
public function index() {
|
||||
@ -82,11 +83,12 @@ class Tests extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/tests/{id}
|
||||
* GET /v1/tests/{id}
|
||||
* GET /v1/tests/site/{id}
|
||||
* Get single test by ID with all related details
|
||||
*/
|
||||
public function show($id = null) {
|
||||
if (!$id) return $this->failValidationErrors('ID is required');
|
||||
if (!$id) return $this->failValidationErrors('TestSiteID is required');
|
||||
|
||||
$row = $this->model->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||
@ -134,11 +136,9 @@ class Tests extends BaseController {
|
||||
} else {
|
||||
// TEST or PARAM - load technical details
|
||||
$row['testdeftech'] = $this->db->table('testdeftech')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName, w.WorkstationName, e.EquipmentName')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left')
|
||||
->join('workstation w', 'w.WorkstationID=testdeftech.WorkstationID', 'left')
|
||||
->join('equipment e', 'e.EquipmentID=testdeftech.EquipmentID', 'left')
|
||||
->where('testdeftech.TestSiteID', $id)
|
||||
->where('testdeftech.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
@ -151,7 +151,8 @@ class Tests extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/tests
|
||||
* POST /v1/tests
|
||||
* POST /v1/tests/site
|
||||
* Create new test definition
|
||||
*/
|
||||
public function create() {
|
||||
@ -196,9 +197,9 @@ class Tests extends BaseController {
|
||||
}
|
||||
|
||||
return $this->respondCreated([
|
||||
'status' => 'success',
|
||||
'status' => 'created',
|
||||
'message' => "Test created successfully",
|
||||
'data' => ['TestSiteID' => $id]
|
||||
'data' => ['TestSiteId' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
@ -207,7 +208,8 @@ class Tests extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT/PATCH /api/tests/{id}
|
||||
* PUT/PATCH /v1/tests/{id}
|
||||
* PUT/PATCH /v1/tests/site/{id}
|
||||
* Update existing test definition
|
||||
*/
|
||||
public function update($id = null) {
|
||||
@ -254,7 +256,7 @@ class Tests extends BaseController {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Test updated successfully",
|
||||
'data' => ['TestSiteID' => $id]
|
||||
'data' => ['TestSiteId' => $id]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
@ -263,7 +265,8 @@ class Tests extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/tests/{id}
|
||||
* DELETE /v1/tests/{id}
|
||||
* DELETE /v1/tests/site/{id}
|
||||
* Soft delete test by setting EndDate
|
||||
*/
|
||||
public function delete($id = null) {
|
||||
@ -326,7 +329,7 @@ class Tests extends BaseController {
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => "Test disabled successfully",
|
||||
'data' => ['TestSiteID' => $id, 'EndDate' => $now]
|
||||
'data' => ['TestSiteId' => $id, 'EndDate' => $now]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
@ -392,17 +395,12 @@ class Tests extends BaseController {
|
||||
*/
|
||||
private function saveTechDetails($testSiteID, $data, $action, $typeCode) {
|
||||
$techData = [
|
||||
'SiteID' => $data['SiteID'],
|
||||
'TestSiteID' => $testSiteID,
|
||||
'DisciplineID' => $data['DisciplineID'] ?? null,
|
||||
'DepartmentID' => $data['DepartmentID'] ?? null,
|
||||
'WorkstationID' => $data['WorkstationID'] ?? null,
|
||||
'EquipmentID' => $data['EquipmentID'] ?? null,
|
||||
'ResultType' => $data['ResultType'] ?? ($typeCode === 'PARAM' ? 'Numeric' : null),
|
||||
'ResultType' => $data['ResultType'] ?? null,
|
||||
'RefType' => $data['RefType'] ?? null,
|
||||
'VSet' => $data['VSet'] ?? null,
|
||||
'SpcType' => $data['SpcType'] ?? null,
|
||||
'SpcDesc' => $data['SpcDesc'] ?? null,
|
||||
'ReqQty' => $data['ReqQty'] ?? null,
|
||||
'ReqQtyUnit' => $data['ReqQtyUnit'] ?? null,
|
||||
'Unit1' => $data['Unit1'] ?? null,
|
||||
@ -435,7 +433,6 @@ class Tests extends BaseController {
|
||||
*/
|
||||
private function saveCalcDetails($testSiteID, $data, $action) {
|
||||
$calcData = [
|
||||
'SiteID' => $data['SiteID'],
|
||||
'TestSiteID' => $testSiteID,
|
||||
'DisciplineID' => $data['DisciplineID'] ?? null,
|
||||
'DepartmentID' => $data['DepartmentID'] ?? null,
|
||||
@ -484,7 +481,6 @@ class Tests extends BaseController {
|
||||
$memberID = is_array($m) ? ($m['Member'] ?? ($m['TestSiteID'] ?? null)) : $m;
|
||||
if ($memberID) {
|
||||
$this->modelGrp->insert([
|
||||
'SiteID' => $data['SiteID'],
|
||||
'TestSiteID' => $testSiteID,
|
||||
'Member' => $memberID
|
||||
]);
|
||||
|
||||
@ -6,7 +6,7 @@ use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateTestsTable extends Migration {
|
||||
public function up() {
|
||||
// testdefsite - Main test definition table
|
||||
// testdefsite - Main test definition table per site
|
||||
$this->forge->addField([
|
||||
'TestSiteID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||
@ -31,17 +31,12 @@ class CreateTestsTable extends Migration {
|
||||
// testdeftech - Technical definition for TEST and PARAM types
|
||||
$this->forge->addField([
|
||||
'TestTechID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'DisciplineID' => ['type' => 'int', 'null' => true],
|
||||
'DepartmentID' => ['type' => 'int', 'null' => true],
|
||||
'WorkstationID' => ['type' => 'INT', 'null' => true],
|
||||
'EquipmentID' => ['type' => 'INT', 'null' => true],
|
||||
'ResultType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'RefType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true],
|
||||
'VSet' => ['type' => 'int', 'null' => true],
|
||||
'SpcType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true],
|
||||
'SpcDesc' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true],
|
||||
'ReqQty' => ['type' => 'DECIMAL', 'constraint'=> '10,2', 'null' => true],
|
||||
'ReqQtyUnit' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'Unit1' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
@ -61,12 +56,11 @@ class CreateTestsTable extends Migration {
|
||||
// testdefcal - Calculation definition for CALC type
|
||||
$this->forge->addField([
|
||||
'TestCalID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'DisciplineID' => ['type' => 'INT', 'null' => true],
|
||||
'DepartmentID' => ['type' => 'INT', 'null' => true],
|
||||
'FormulaInput' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
|
||||
'FormulaCode' => ['type' => 'text', 'null' => true],
|
||||
'FormulaInput' => ['type' => 'text', 'null' => true],
|
||||
'FormulaCode' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
|
||||
'RefType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true, 'default' => 'NMRC'],
|
||||
'Unit1' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||
'Factor' => ['type' => 'DECIMAL', 'constraint'=> '10,4', 'null' => true],
|
||||
@ -83,7 +77,6 @@ class CreateTestsTable extends Migration {
|
||||
// testdefgrp - Group definition for GROUP type
|
||||
$this->forge->addField([
|
||||
'TestGrpID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||
'Member' => ['type' => 'INT', 'null' => true],
|
||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||
|
||||
@ -8,7 +8,6 @@ class TestDefCalModel extends BaseModel {
|
||||
protected $table = 'testdefcal';
|
||||
protected $primaryKey = 'TestCalID';
|
||||
protected $allowedFields = [
|
||||
'SiteID',
|
||||
'TestSiteID',
|
||||
'DisciplineID',
|
||||
'DepartmentID',
|
||||
@ -30,4 +29,50 @@ class TestDefCalModel extends BaseModel {
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
/**
|
||||
* Get calculation details for a test
|
||||
*/
|
||||
public function getCalcDetails($testSiteID) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
return $db->table('testdefcal')
|
||||
->select('testdefcal.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdefcal.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdefcal.DepartmentID', 'left')
|
||||
->where('testdefcal.TestSiteID', $testSiteID)
|
||||
->where('testdefcal.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculated tests by discipline
|
||||
*/
|
||||
public function getCalcsByDiscipline($disciplineID, $siteID = null) {
|
||||
$builder = $this->select('testdefcal.*, testdefsite.TestSiteCode, testdefsite.TestSiteName')
|
||||
->join('testdefsite', 'testdefsite.TestSiteID=testdefcal.TestSiteID', 'left')
|
||||
->where('testdefcal.DisciplineID', $disciplineID)
|
||||
->where('testdefcal.EndDate IS NULL');
|
||||
|
||||
if ($siteID) {
|
||||
$builder->where('testdefsite.SiteID', $siteID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calculated tests by department
|
||||
*/
|
||||
public function getCalcsByDepartment($departmentID, $siteID = null) {
|
||||
$builder = $this->select('testdefcal.*, testdefsite.TestSiteCode, testdefsite.TestSiteName')
|
||||
->join('testdefsite', 'testdefsite.TestSiteID=testdefcal.TestSiteID', 'left')
|
||||
->where('testdefcal.DepartmentID', $departmentID)
|
||||
->where('testdefcal.EndDate IS NULL');
|
||||
|
||||
if ($siteID) {
|
||||
$builder->where('testdefsite.SiteID', $siteID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,12 @@ use App\Models\BaseModel;
|
||||
class TestDefGrpModel extends BaseModel {
|
||||
protected $table = 'testdefgrp';
|
||||
protected $primaryKey = 'TestGrpID';
|
||||
protected $allowedFields = ['SiteID', 'TestSiteID', 'Member', 'CreateDate', 'EndDate'];
|
||||
protected $allowedFields = [
|
||||
'TestSiteID',
|
||||
'Member',
|
||||
'CreateDate',
|
||||
'EndDate'
|
||||
];
|
||||
|
||||
protected $useTimestamps = true;
|
||||
protected $createdField = 'CreateDate';
|
||||
@ -15,4 +20,30 @@ class TestDefGrpModel extends BaseModel {
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
/**
|
||||
* Get group members for a test group
|
||||
*/
|
||||
public function getGroupMembers($testSiteID) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
return $db->table('testdefgrp')
|
||||
->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName, t.TestType, vs.VValue as MemberTypeCode')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.Member', 'left')
|
||||
->join('valueset vs', 'vs.VID=t.TestType', 'left')
|
||||
->where('testdefgrp.TestSiteID', $testSiteID)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all groups that contain a specific test
|
||||
*/
|
||||
public function getGroupsContainingTest($memberTestSiteID) {
|
||||
return $this->select('testdefgrp.*, t.TestSiteCode, t.TestSiteName')
|
||||
->join('testdefsite t', 't.TestSiteID=testdefgrp.TestSiteID', 'left')
|
||||
->where('testdefgrp.Member', $memberTestSiteID)
|
||||
->where('testdefgrp.EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
}
|
||||
@ -8,17 +8,12 @@ class TestDefTechModel extends BaseModel {
|
||||
protected $table = 'testdeftech';
|
||||
protected $primaryKey = 'TestTechID';
|
||||
protected $allowedFields = [
|
||||
'SiteID',
|
||||
'TestSiteID',
|
||||
'DisciplineID',
|
||||
'DepartmentID',
|
||||
'WorkstationID',
|
||||
'EquipmentID',
|
||||
'ResultType',
|
||||
'RefType',
|
||||
'VSet',
|
||||
'SpcType',
|
||||
'SpcDesc',
|
||||
'ReqQty',
|
||||
'ReqQtyUnit',
|
||||
'Unit1',
|
||||
@ -38,4 +33,50 @@ class TestDefTechModel extends BaseModel {
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
/**
|
||||
* Get technical details for a test
|
||||
*/
|
||||
public function getTechDetails($testSiteID) {
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
return $db->table('testdeftech')
|
||||
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName')
|
||||
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
||||
->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', 'left')
|
||||
->where('testdeftech.TestSiteID', $testSiteID)
|
||||
->where('testdeftech.EndDate IS NULL')
|
||||
->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tests by discipline
|
||||
*/
|
||||
public function getTestsByDiscipline($disciplineID, $siteID = null) {
|
||||
$builder = $this->select('testdeftech.*, testdefsite.TestSiteCode, testdefsite.TestSiteName')
|
||||
->join('testdefsite', 'testdefsite.TestSiteID=testdeftech.TestSiteID', 'left')
|
||||
->where('testdeftech.DisciplineID', $disciplineID)
|
||||
->where('testdeftech.EndDate IS NULL');
|
||||
|
||||
if ($siteID) {
|
||||
$builder->where('testdefsite.SiteID', $siteID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tests by department
|
||||
*/
|
||||
public function getTestsByDepartment($departmentID, $siteID = null) {
|
||||
$builder = $this->select('testdeftech.*, testdefsite.TestSiteCode, testdefsite.TestSiteName')
|
||||
->join('testdefsite', 'testdefsite.TestSiteID=testdeftech.TestSiteID', 'left')
|
||||
->where('testdeftech.DepartmentID', $departmentID)
|
||||
->where('testdeftech.EndDate IS NULL');
|
||||
|
||||
if ($siteID) {
|
||||
$builder->where('testdefsite.SiteID', $siteID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
}
|
||||
@ -30,4 +30,67 @@ class TestMapModel extends BaseModel {
|
||||
protected $useSoftDeletes = true;
|
||||
protected $deletedField = "EndDate";
|
||||
|
||||
/**
|
||||
* Get test mappings by test site
|
||||
*/
|
||||
public function getMappingsByTestSite($testSiteID) {
|
||||
return $this->where('TestSiteID', $testSiteID)
|
||||
->where('EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test mappings by client (equipment/workstation)
|
||||
*/
|
||||
public function getMappingsByClient($clientType, $clientID) {
|
||||
return $this->where('ClientType', $clientType)
|
||||
->where('ClientID', $clientID)
|
||||
->where('EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test mappings by host (site/HIS)
|
||||
*/
|
||||
public function getMappingsByHost($hostType, $hostID) {
|
||||
return $this->where('HostType', $hostType)
|
||||
->where('HostID', $hostID)
|
||||
->where('EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test mapping by client test code and container
|
||||
*/
|
||||
public function getMappingByClientCode($clientTestCode, $conDefID = null) {
|
||||
$builder = $this->where('ClientTestCode', $clientTestCode)
|
||||
->where('EndDate IS NULL');
|
||||
|
||||
if ($conDefID) {
|
||||
$builder->where('ConDefID', $conDefID);
|
||||
}
|
||||
|
||||
return $builder->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test mapping by host test code
|
||||
*/
|
||||
public function getMappingByHostCode($hostTestCode) {
|
||||
return $this->where('HostTestCode', $hostTestCode)
|
||||
->where('EndDate IS NULL')
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if mapping exists for client and host
|
||||
*/
|
||||
public function mappingExists($testSiteID, $clientType, $clientID, $clientTestCode) {
|
||||
return $this->where('TestSiteID', $testSiteID)
|
||||
->where('ClientType', $clientType)
|
||||
->where('ClientID', $clientID)
|
||||
->where('ClientTestCode', $clientTestCode)
|
||||
->where('EndDate IS NULL')
|
||||
->countAllResults() > 0;
|
||||
}
|
||||
}
|
||||
@ -84,7 +84,7 @@
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.ResultUnit"
|
||||
x-model="form.Unit1"
|
||||
placeholder="% or cells/µL"
|
||||
/>
|
||||
</div>
|
||||
@ -105,31 +105,28 @@
|
||||
</label>
|
||||
<textarea
|
||||
class="input font-mono h-20"
|
||||
:class="errors.Formula && 'input-error'"
|
||||
x-model="form.Formula"
|
||||
:class="errors.FormulaCode && 'input-error'"
|
||||
x-model="form.FormulaCode"
|
||||
placeholder="({WBC} * {NEU%}) / 100"
|
||||
></textarea>
|
||||
<label class="label" x-show="errors.Formula">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.Formula"></span>
|
||||
<label class="label" x-show="errors.FormulaCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.FormulaCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Formula Variables/Tests -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Used Test Variables</span>
|
||||
<span class="label-text font-medium">Input Parameters</span>
|
||||
<span class="label-text-alt text-base-content/60">Tests referenced in formula</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<template x-if="!form.formulaVars || form.formulaVars.length === 0">
|
||||
<span class="text-sm text-base-content/50 italic">No variables defined. Add tests below.</span>
|
||||
<template x-if="!form.FormulaInput || form.FormulaInput.length === 0">
|
||||
<span class="text-sm text-base-content/50 italic">No parameters defined.</span>
|
||||
</template>
|
||||
<template x-for="v in form.formulaVars" :key="v">
|
||||
<template x-for="(v, idx) in (form.FormulaInput ? form.FormulaInput.split('^') : [])" :key="idx">
|
||||
<span class="badge badge-primary gap-1">
|
||||
<code x-text="'{'+v+'}'"></code>
|
||||
<button class="btn btn-ghost btn-xs btn-square" @click="removeFormulaVar(v)">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<code x-text="v"></code>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
@ -139,7 +136,7 @@
|
||||
<div class="flex gap-2">
|
||||
<select class="select select-bordered flex-1" x-model="form.newFormulaVar">
|
||||
<option value="">Select test variable...</option>
|
||||
<template x-for="t in availableTests" :key="t.TestSiteID">
|
||||
<template x-for="(t, idx) in availableTests" :key="idx">
|
||||
<option :value="t.TestSiteCode" x-text="t.TestSiteCode + ' - ' + t.TestSiteName"></option>
|
||||
</template>
|
||||
</select>
|
||||
@ -151,7 +148,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Result Options -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Decimal Places</span>
|
||||
@ -159,7 +156,7 @@
|
||||
<input
|
||||
type="number"
|
||||
class="input text-center"
|
||||
x-model="form.DecimalPlaces"
|
||||
x-model="form.Decimal"
|
||||
min="0"
|
||||
max="10"
|
||||
placeholder="2"
|
||||
@ -167,14 +164,25 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Rounding Method</span>
|
||||
<span class="label-text font-medium">Conversion Factor</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.RoundingMethod">
|
||||
<option value="round">Standard Round</option>
|
||||
<option value="floor">Floor (Down)</option>
|
||||
<option value="ceil">Ceiling (Up)</option>
|
||||
<option value="truncate">Truncate</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Factor"
|
||||
placeholder="Optional conversion"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Secondary Unit</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
x-model="form.Unit2"
|
||||
placeholder="Optional secondary unit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
x-transition:leave-end="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="modal-content p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto"
|
||||
class="modal-content p-5 max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||
@click.stop
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform scale-95"
|
||||
@ -22,8 +22,8 @@
|
||||
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));">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg flex items-center gap-2" style="color: rgb(var(--color-text));">
|
||||
<i class="fa-solid fa-layer-group" style="color: rgb(var(--color-primary));"></i>
|
||||
<span x-text="isEditing ? 'Edit Test Group' : 'New Test Group'"></span>
|
||||
</h3>
|
||||
@ -33,206 +33,176 @@
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Group Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
<label class="label py-1">
|
||||
<span class="label-text font-medium text-sm">Group Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
class="input input-sm input-bordered"
|
||||
:class="errors.TestSiteName && 'input-error'"
|
||||
x-model="form.TestSiteName"
|
||||
placeholder="CBC Panel"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteName">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteName"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Group Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
<label class="label py-1">
|
||||
<span class="label-text font-medium text-sm">Group Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input font-mono"
|
||||
class="input input-sm input-bordered font-mono"
|
||||
:class="errors.TestSiteCode && 'input-error'"
|
||||
x-model="form.TestSiteCode"
|
||||
placeholder="CBC"
|
||||
/>
|
||||
<label class="label" x-show="errors.TestSiteCode">
|
||||
<span class="label-text-alt" style="color: rgb(var(--color-error));" x-text="errors.TestSiteCode"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Type & Specimen -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Test Type & Default Specimen -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Test Type</span>
|
||||
<label class="label py-1">
|
||||
<span class="label-text font-medium text-sm">Test Type</span>
|
||||
</label>
|
||||
<input type="text" class="input input-disabled bg-base-200" x-model="form.TestTypeName" readonly />
|
||||
<input type="text" class="input input-sm input-bordered bg-base-200" x-model="form.TestTypeName" readonly />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Default Specimen</span>
|
||||
<label class="label py-1">
|
||||
<span class="label-text font-medium text-sm">Default Specimen</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.DefaultSpecimenID">
|
||||
<option value="">Select Specimen</option>
|
||||
<template x-for="s in specimenTypesList" :key="s.SpecimenTypeID">
|
||||
<option :value="s.SpecimenTypeID" x-text="s.SpecimenTypeName"></option>
|
||||
<select class="select select-sm select-bordered" x-model="form.SpcType">
|
||||
<option value="">Select</option>
|
||||
<template x-for="s in specimenTypesList" :key="s.VID || s.id">
|
||||
<option :value="s.VValue" x-text="s.VDesc || s.VValue"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Description</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="input h-16 pt-2"
|
||||
x-model="form.Description"
|
||||
placeholder="Group description..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Test Members Selection -->
|
||||
<div class="border rounded-xl p-4 bg-base-50">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-semibold flex items-center gap-2">
|
||||
<i class="fa-solid fa-list-check"></i>
|
||||
Group Members (Tests)
|
||||
<div class="border rounded-lg p-3 bg-base-50">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h4 class="font-semibold text-sm flex items-center gap-1">
|
||||
<i class="fa-solid fa-list-check text-sm"></i>
|
||||
Group Members
|
||||
</h4>
|
||||
<button class="btn btn-primary btn-sm" @click="openTestSelector()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add Tests
|
||||
<button class="btn btn-primary btn-xs" @click="openTestSelector()">
|
||||
<i class="fa-solid fa-plus mr-1"></i> Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Selected Members List -->
|
||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
||||
<div class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<template x-if="!form.members || form.members.length === 0">
|
||||
<div class="text-center py-8 text-base-content/50">
|
||||
<i class="fa-solid fa-inbox text-3xl mb-2"></i>
|
||||
<p>No tests added yet</p>
|
||||
<p class="text-sm">Click "Add Tests" to select tests for this group</p>
|
||||
<div class="text-center py-4 text-base-content/50">
|
||||
<i class="fa-solid fa-inbox text-xl mb-1"></i>
|
||||
<p class="text-xs">No tests added</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-for="(member, index) in form.members" :key="index">
|
||||
<div class="flex items-center gap-3 p-3 bg-white rounded-lg border">
|
||||
<span class="badge badge-sm font-mono" x-text="member.TestSiteCode"></span>
|
||||
<span class="flex-1 font-medium" x-text="member.TestSiteName"></span>
|
||||
<input
|
||||
type="number"
|
||||
class="input input-bordered input-sm w-16 text-center"
|
||||
x-model="member.SeqScr"
|
||||
placeholder="Seq"
|
||||
title="Sequence"
|
||||
/>
|
||||
<button class="btn btn-ghost btn-sm btn-square text-error" @click="removeMember(index)">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
<div class="grid grid-cols-[1fr_auto] gap-2 p-2 bg-white rounded border items-center">
|
||||
<span class="text-xs font-medium truncate" x-text="member.TestSiteCode+' - '+member.TestSiteName"></span>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="number" class="input input-xs py-1 text-center w-8" x-model="member.SeqScr" placeholder="#" title="Order"/>
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error" @click="removeMember(index)">
|
||||
<i class="fa-solid fa-times text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sequence & Site -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Screen)</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqScr" />
|
||||
<!-- Sequence & Site - Very Compact -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-xs text-base-content/60">Scr:</span>
|
||||
<input type="number" class="input input-xs w-12 text-center" x-model="form.SeqScr" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Seq (Report)</span>
|
||||
</label>
|
||||
<input type="number" class="input text-center" x-model="form.SeqRpt" />
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-xs text-base-content/60">Rpt:</span>
|
||||
<input type="number" class="input input-xs w-12 text-center" x-model="form.SeqRpt" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Site</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.SiteID">
|
||||
<select class="select select-xs flex-1" x-model="form.SiteID">
|
||||
<template x-for="s in sitesList" :key="s.SiteID">
|
||||
<option :value="s.SiteID" x-text="s.SiteName"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div class="flex items-center gap-6 p-4 rounded-xl border border-slate-100 bg-slate-50/50">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleScr" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Screen</span>
|
||||
<!-- Options - Compact -->
|
||||
<div class="flex items-center gap-3 p-2 rounded border border-slate-100 bg-slate-50/50">
|
||||
<label class="flex items-center gap-1 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" x-model="form.VisibleScr" :true-value="1" :false-value="0" />
|
||||
<span class="label-text text-xs">Screen</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.VisibleRpt" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Visible in Report</span>
|
||||
<label class="flex items-center gap-1 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" x-model="form.VisibleRpt" :true-value="1" :false-value="0" />
|
||||
<span class="label-text text-xs">Report</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox" x-model="form.CountStat" :true-value="1" :false-value="0" />
|
||||
<span class="label-text">Count in Statistics</span>
|
||||
<label class="flex items-center gap-1 cursor-pointer">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" x-model="form.CountStat" :true-value="1" :false-value="0" />
|
||||
<span class="label-text text-xs">Stats</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</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">
|
||||
<span x-show="saving" class="spinner spinner-sm"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-2"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save Group'"></span>
|
||||
<div class="flex gap-2 mt-4 pt-3" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-sm btn-ghost flex-1" @click="closeModal()">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary flex-1" @click="save()" :disabled="saving">
|
||||
<span x-show="saving" class="spinner spinner-xs"></span>
|
||||
<i x-show="!saving" class="fa-solid fa-save mr-1"></i>
|
||||
<span x-text="saving ? 'Saving...' : 'Save'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Selector Modal -->
|
||||
<div x-show="showTestSelector" x-cloak class="modal-overlay" style="z-index: 100;">
|
||||
<div class="modal-content p-6 max-w-3xl" @click.stop>
|
||||
<h4 class="font-bold text-lg mb-4">Select Tests to Add</h4>
|
||||
<div class="modal-content p-4 max-w-lg" @click.stop>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h4 class="font-bold">Select Tests</h4>
|
||||
<button class="btn btn-ghost btn-sm btn-square" @click="showTestSelector = false">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="input mb-4"
|
||||
class="input input-sm mb-2"
|
||||
placeholder="Search tests..."
|
||||
x-model="testSearch"
|
||||
/>
|
||||
|
||||
<div class="max-h-96 overflow-y-auto space-y-2">
|
||||
<div class="max-h-48 overflow-y-auto space-y-1">
|
||||
<template x-for="test in availableTests" :key="test.TestSiteID">
|
||||
<label class="flex items-center gap-3 p-3 hover:bg-base-200 rounded-lg cursor-pointer">
|
||||
<label class="flex items-center gap-2 p-2 hover:bg-base-200 rounded-lg cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-primary"
|
||||
class="checkbox checkbox-sm checkbox-primary"
|
||||
:checked="isTestSelected(test.TestSiteID)"
|
||||
@change="toggleTestSelection(test)"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium" x-text="test.TestSiteName"></div>
|
||||
<div class="text-sm text-base-content/60 font-mono" x-text="test.TestSiteCode"></div>
|
||||
<div class="font-medium text-sm" x-text="test.TestSiteName"></div>
|
||||
<div class="text-xs text-base-content/60 font-mono" x-text="test.TestSiteCode"></div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button class="btn btn-ghost flex-1" @click="showTestSelector = false">Cancel</button>
|
||||
<button class="btn btn-primary flex-1" @click="confirmTestSelection()">
|
||||
<i class="fa-solid fa-check mr-2"></i> Confirm Selection
|
||||
<div class="flex gap-2 mt-3 pt-2" style="border-top: 1px solid rgb(var(--color-border));">
|
||||
<button class="btn btn-sm btn-ghost flex-1" @click="showTestSelector = false">Cancel</button>
|
||||
<button class="btn btn-sm btn-primary flex-1" @click="confirmTestSelection()">
|
||||
<i class="fa-solid fa-check mr-1"></i> Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -81,10 +81,10 @@
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Method</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.MethodID">
|
||||
<select class="select" x-model="form.Method">
|
||||
<option value="">Select Method</option>
|
||||
<template x-for="m in methodsList" :key="m.MethodID">
|
||||
<option :value="m.MethodID" x-text="m.MethodName"></option>
|
||||
<template x-for="m in methodsList" :key="m.VID">
|
||||
<option :value="m.VValue" x-text="m.VDesc || m.VValue"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
@ -96,10 +96,10 @@
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Specimen Type</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.SpecimenTypeID">
|
||||
<select class="select" x-model="form.SpcType">
|
||||
<option value="">Select Specimen</option>
|
||||
<template x-for="s in specimenTypesList" :key="s.SpecimenTypeID">
|
||||
<option :value="s.SpecimenTypeID" x-text="s.SpecimenTypeName"></option>
|
||||
<template x-for="s in specimenTypesList" :key="s.VID || s.id">
|
||||
<option :value="s.VValue" x-text="s.VDesc || s.VValue"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
@ -107,10 +107,10 @@
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Container</span>
|
||||
</label>
|
||||
<select class="select" x-model="form.ContainerID">
|
||||
<select class="select" x-model="form.ConDefID">
|
||||
<option value="">Select Container</option>
|
||||
<template x-for="c in containersList" :key="c.ContainerID">
|
||||
<option :value="c.ContainerID" x-text="c.ContainerName"></option>
|
||||
<template x-for="c in containersList" :key="c.ConDefID || c.id">
|
||||
<option :value="c.ConDefID" x-text="c.ConCode || c.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -232,6 +232,8 @@ function labTests() {
|
||||
await this.fetchDisciplines();
|
||||
await this.fetchDepartments();
|
||||
await this.fetchMethods();
|
||||
await this.fetchSpecimenTypes();
|
||||
await this.fetchContainers();
|
||||
// Additional data can be loaded on demand when specific dialogs are opened
|
||||
|
||||
// Watch for typesList changes to ensure dropdown is populated
|
||||
@ -290,7 +292,38 @@ function labTests() {
|
||||
this.methodsList = data.data || [];
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently fail
|
||||
// Silently fail - use empty array
|
||||
this.methodsList = [];
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch specimen types from valueset
|
||||
async fetchSpecimenTypes() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/valueset/valuesetdef/29`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.specimenTypesList = data.data || [];
|
||||
}
|
||||
} catch (err) {
|
||||
this.specimenTypesList = [];
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch containers from endpoint
|
||||
async fetchContainers() {
|
||||
try {
|
||||
const res = await fetch(`${BASEURL}api/specimen/containerdef`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.containersList = data.data || [];
|
||||
}
|
||||
} catch (err) {
|
||||
this.containersList = [];
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -247,6 +247,11 @@ body {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 0.875rem 1.75rem;
|
||||
font-size: 1rem;
|
||||
@ -395,6 +400,19 @@ body {
|
||||
box-shadow: 0 0 0 3px rgba(var(--color-error), 0.15);
|
||||
}
|
||||
|
||||
/* Input Sizes */
|
||||
.input-sm {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.input-xs {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
.checkbox {
|
||||
width: 1.25rem;
|
||||
|
||||
259
tests/feature/TestDef/TestDefSiteTest.php
Normal file
259
tests/feature/TestDef/TestDefSiteTest.php
Normal file
@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\TestDef;
|
||||
|
||||
use CodeIgniter\Test\FeatureTestTrait;
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
|
||||
class TestDefSiteTest extends CIUnitTestCase
|
||||
{
|
||||
use FeatureTestTrait;
|
||||
|
||||
protected $endpoint = 'api/tests';
|
||||
protected $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// Generate Token
|
||||
$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'
|
||||
];
|
||||
$this->token = \Firebase\JWT\JWT::encode($payload, $key, 'HS256');
|
||||
}
|
||||
|
||||
public function get(string $path, array $options = []) {
|
||||
$this->withHeaders(['Cookie' => 'token=' . $this->token]);
|
||||
return $this->call('get', $path, $options);
|
||||
}
|
||||
|
||||
public function post(string $path, array $options = []) {
|
||||
$this->withHeaders(['Cookie' => 'token=' . $this->token]);
|
||||
return $this->call('post', $path, $options);
|
||||
}
|
||||
|
||||
public function put(string $path, array $options = []) {
|
||||
$this->withHeaders(['Cookie' => 'token=' . $this->token]);
|
||||
return $this->call('put', $path, $options);
|
||||
}
|
||||
|
||||
public function delete(string $path, array $options = []) {
|
||||
$this->withHeaders(['Cookie' => 'token=' . $this->token]);
|
||||
return $this->call('delete', $path, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test index endpoint returns list of tests
|
||||
*/
|
||||
public function testIndexReturnsTestList()
|
||||
{
|
||||
$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()
|
||||
{
|
||||
$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 testIndexWithTypeFilter()
|
||||
{
|
||||
$result = $this->get($this->endpoint . '?TestType=TEST');
|
||||
$result->assertStatus(200);
|
||||
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test show endpoint returns single test
|
||||
*/
|
||||
public function testShowReturnsSingleTest()
|
||||
{
|
||||
// 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'] ?? 1;
|
||||
|
||||
$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()
|
||||
{
|
||||
$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 definition
|
||||
*/
|
||||
public function testCreateTest()
|
||||
{
|
||||
$testData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'HB',
|
||||
'TestSiteName' => 'Hemoglobin',
|
||||
'TestType' => 'TEST',
|
||||
'Description' => 'Hemoglobin concentration test',
|
||||
'SeqScr' => 3,
|
||||
'SeqRpt' => 3,
|
||||
'IndentLeft' => 0,
|
||||
'FontStyle' => 'Bold',
|
||||
'VisibleScr' => 1,
|
||||
'VisibleRpt' => 1,
|
||||
'CountStat' => 1,
|
||||
'StartDate' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($testData)]);
|
||||
|
||||
// If validation fails due to duplicate code, that's expected
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(in_array($status, [201, 400]), "Expected 201 or 400, got $status");
|
||||
|
||||
if ($status === 201) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('created', $body['status']);
|
||||
$this->assertArrayHasKey('TestSiteId', $body['data']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test update existing test
|
||||
*/
|
||||
public function testUpdateTest()
|
||||
{
|
||||
// Get a valid test ID first
|
||||
$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',
|
||||
'Description' => 'Updated description'
|
||||
];
|
||||
|
||||
$result = $this->put($this->endpoint . '/' . $testSiteID, ['body' => json_encode($updateData)]);
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(in_array($status, [200, 404]), "Expected 200 or 404, 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()
|
||||
{
|
||||
// Get a valid test ID first
|
||||
$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) {
|
||||
$result = $this->delete($this->endpoint . '/' . $testSiteID);
|
||||
$status = $result->response()->getStatusCode();
|
||||
$this->assertTrue(in_array($status, [200, 404]), "Expected 200 or 404, got $status");
|
||||
|
||||
if ($status === 200) {
|
||||
$body = json_decode($result->response()->getBody(), true);
|
||||
$this->assertEquals('success', $body['status']);
|
||||
$this->assertArrayHasKey('EndDate', $body['data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validation - missing required fields
|
||||
*/
|
||||
public function testCreateValidation()
|
||||
{
|
||||
$invalidData = [
|
||||
'TestSiteName' => 'Test without code'
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
||||
$result->assertStatus(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that TestSiteCode is max 6 characters
|
||||
*/
|
||||
public function testTestSiteCodeLength()
|
||||
{
|
||||
$invalidData = [
|
||||
'SiteID' => 1,
|
||||
'TestSiteCode' => 'HB123456', // 8 characters - invalid
|
||||
'TestSiteName' => 'Test with too long code',
|
||||
'TestType' => 'TEST'
|
||||
];
|
||||
|
||||
$result = $this->post($this->endpoint, ['body' => json_encode($invalidData)]);
|
||||
$result->assertStatus(400);
|
||||
}
|
||||
}
|
||||
237
tests/unit/TestDef/TestDefModelsTest.php
Normal file
237
tests/unit/TestDef/TestDefModelsTest.php
Normal file
@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\TestDef;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Models\Test\TestDefSiteModel;
|
||||
use App\Models\Test\TestDefTechModel;
|
||||
use App\Models\Test\TestDefCalModel;
|
||||
use App\Models\Test\TestDefGrpModel;
|
||||
use App\Models\Test\TestMapModel;
|
||||
|
||||
class TestDefModelsTest extends CIUnitTestCase
|
||||
{
|
||||
protected $testDefSiteModel;
|
||||
protected $testDefTechModel;
|
||||
protected $testDefCalModel;
|
||||
protected $testDefGrpModel;
|
||||
protected $testMapModel;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->testDefSiteModel = new TestDefSiteModel();
|
||||
$this->testDefTechModel = new TestDefTechModel();
|
||||
$this->testDefCalModel = new TestDefCalModel();
|
||||
$this->testDefGrpModel = new TestDefGrpModel();
|
||||
$this->testMapModel = new TestMapModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefSiteModel has correct table name
|
||||
*/
|
||||
public function testTestDefSiteModelTable()
|
||||
{
|
||||
$this->assertEquals('testdefsite', $this->testDefSiteModel->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefSiteModel has correct primary key
|
||||
*/
|
||||
public function testTestDefSiteModelPrimaryKey()
|
||||
{
|
||||
$this->assertEquals('TestSiteID', $this->testDefSiteModel->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefSiteModel has correct allowed fields
|
||||
*/
|
||||
public function testTestDefSiteModelAllowedFields()
|
||||
{
|
||||
$allowedFields = $this->testDefSiteModel->allowedFields;
|
||||
|
||||
$this->assertContains('SiteID', $allowedFields);
|
||||
$this->assertContains('TestSiteCode', $allowedFields);
|
||||
$this->assertContains('TestSiteName', $allowedFields);
|
||||
$this->assertContains('TestType', $allowedFields);
|
||||
$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);
|
||||
$this->assertContains('CreateDate', $allowedFields);
|
||||
$this->assertContains('StartDate', $allowedFields);
|
||||
$this->assertContains('EndDate', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefSiteModel uses soft deletes
|
||||
*/
|
||||
public function testTestDefSiteModelSoftDeletes()
|
||||
{
|
||||
$this->assertTrue($this->testDefSiteModel->useSoftDeletes);
|
||||
$this->assertEquals('EndDate', $this->testDefSiteModel->deletedField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefTechModel has correct table name
|
||||
*/
|
||||
public function testTestDefTechModelTable()
|
||||
{
|
||||
$this->assertEquals('testdeftech', $this->testDefTechModel->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefTechModel has correct primary key
|
||||
*/
|
||||
public function testTestDefTechModelPrimaryKey()
|
||||
{
|
||||
$this->assertEquals('TestTechID', $this->testDefTechModel->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefTechModel has correct allowed fields
|
||||
*/
|
||||
public function testTestDefTechModelAllowedFields()
|
||||
{
|
||||
$allowedFields = $this->testDefTechModel->allowedFields;
|
||||
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
$this->assertContains('DisciplineID', $allowedFields);
|
||||
$this->assertContains('DepartmentID', $allowedFields);
|
||||
$this->assertContains('ResultType', $allowedFields);
|
||||
$this->assertContains('RefType', $allowedFields);
|
||||
$this->assertContains('VSet', $allowedFields);
|
||||
$this->assertContains('Unit1', $allowedFields);
|
||||
$this->assertContains('Factor', $allowedFields);
|
||||
$this->assertContains('Unit2', $allowedFields);
|
||||
$this->assertContains('Decimal', $allowedFields);
|
||||
$this->assertContains('Method', $allowedFields);
|
||||
$this->assertContains('ExpectedTAT', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefCalModel has correct table name
|
||||
*/
|
||||
public function testTestDefCalModelTable()
|
||||
{
|
||||
$this->assertEquals('testdefcal', $this->testDefCalModel->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefCalModel has correct primary key
|
||||
*/
|
||||
public function testTestDefCalModelPrimaryKey()
|
||||
{
|
||||
$this->assertEquals('TestCalID', $this->testDefCalModel->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefCalModel has correct allowed fields
|
||||
*/
|
||||
public function testTestDefCalModelAllowedFields()
|
||||
{
|
||||
$allowedFields = $this->testDefCalModel->allowedFields;
|
||||
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
$this->assertContains('DisciplineID', $allowedFields);
|
||||
$this->assertContains('DepartmentID', $allowedFields);
|
||||
$this->assertContains('FormulaInput', $allowedFields);
|
||||
$this->assertContains('FormulaCode', $allowedFields);
|
||||
$this->assertContains('RefType', $allowedFields);
|
||||
$this->assertContains('Unit1', $allowedFields);
|
||||
$this->assertContains('Factor', $allowedFields);
|
||||
$this->assertContains('Unit2', $allowedFields);
|
||||
$this->assertContains('Decimal', $allowedFields);
|
||||
$this->assertContains('Method', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefGrpModel has correct table name
|
||||
*/
|
||||
public function testTestDefGrpModelTable()
|
||||
{
|
||||
$this->assertEquals('testdefgrp', $this->testDefGrpModel->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefGrpModel has correct primary key
|
||||
*/
|
||||
public function testTestDefGrpModelPrimaryKey()
|
||||
{
|
||||
$this->assertEquals('TestGrpID', $this->testDefGrpModel->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestDefGrpModel has correct allowed fields
|
||||
*/
|
||||
public function testTestDefGrpModelAllowedFields()
|
||||
{
|
||||
$allowedFields = $this->testDefGrpModel->allowedFields;
|
||||
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
$this->assertContains('Member', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestMapModel has correct table name
|
||||
*/
|
||||
public function testTestMapModelTable()
|
||||
{
|
||||
$this->assertEquals('testmap', $this->testMapModel->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestMapModel has correct primary key
|
||||
*/
|
||||
public function testTestMapModelPrimaryKey()
|
||||
{
|
||||
$this->assertEquals('TestMapID', $this->testMapModel->primaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test TestMapModel has correct allowed fields
|
||||
*/
|
||||
public function testTestMapModelAllowedFields()
|
||||
{
|
||||
$allowedFields = $this->testMapModel->allowedFields;
|
||||
|
||||
$this->assertContains('TestSiteID', $allowedFields);
|
||||
$this->assertContains('HostType', $allowedFields);
|
||||
$this->assertContains('HostID', $allowedFields);
|
||||
$this->assertContains('HostDataSource', $allowedFields);
|
||||
$this->assertContains('HostTestCode', $allowedFields);
|
||||
$this->assertContains('HostTestName', $allowedFields);
|
||||
$this->assertContains('ClientType', $allowedFields);
|
||||
$this->assertContains('ClientID', $allowedFields);
|
||||
$this->assertContains('ClientDataSource', $allowedFields);
|
||||
$this->assertContains('ConDefID', $allowedFields);
|
||||
$this->assertContains('ClientTestCode', $allowedFields);
|
||||
$this->assertContains('ClientTestName', $allowedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test all models use soft deletes
|
||||
*/
|
||||
public function testAllModelsUseSoftDeletes()
|
||||
{
|
||||
$this->assertTrue($this->testDefTechModel->useSoftDeletes);
|
||||
$this->assertTrue($this->testDefCalModel->useSoftDeletes);
|
||||
$this->assertTrue($this->testDefGrpModel->useSoftDeletes);
|
||||
$this->assertTrue($this->testMapModel->useSoftDeletes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test all models have EndDate as deleted field
|
||||
*/
|
||||
public function testAllModelsUseEndDateAsDeletedField()
|
||||
{
|
||||
$this->assertEquals('EndDate', $this->testDefTechModel->deletedField);
|
||||
$this->assertEquals('EndDate', $this->testDefCalModel->deletedField);
|
||||
$this->assertEquals('EndDate', $this->testDefGrpModel->deletedField);
|
||||
$this->assertEquals('EndDate', $this->testMapModel->deletedField);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user