feat(tests): enhance Test Management module with v2 UI dialogs
- Add new dialog forms for test calc, group, param, and title management - Refactor test_dialog.php to new location (master/tests/) - Update TestDefCalModel, TestDefSiteModel, TestDefTechModel, TestMapModel - Modify Tests controller and Routes for new dialog handlers - Update migration schema for test definitions - Add new styles for v2 test management interface - Include Test Management documentation files
This commit is contained in:
parent
a94df3b5f7
commit
97451496c3
BIN
Test Management.docx
Normal file
BIN
Test Management.docx
Normal file
Binary file not shown.
@ -150,7 +150,7 @@ $routes->group('api', function ($routes) {
|
|||||||
$routes->group('valueset', function ($routes) {
|
$routes->group('valueset', function ($routes) {
|
||||||
$routes->get('/', 'ValueSet\ValueSet::index');
|
$routes->get('/', 'ValueSet\ValueSet::index');
|
||||||
$routes->get('(:num)', 'ValueSet\ValueSet::show/$1');
|
$routes->get('(:num)', 'ValueSet\ValueSet::show/$1');
|
||||||
$routes->get('valuesetdef/(:segment)', 'ValueSet\ValueSet::showByValueSetDef/$1');
|
$routes->get('valuesetdef/(:num)', 'ValueSet\ValueSet::showByValueSetDef/$1');
|
||||||
$routes->post('/', 'ValueSet\ValueSet::create');
|
$routes->post('/', 'ValueSet\ValueSet::create');
|
||||||
$routes->patch('/', 'ValueSet\ValueSet::update');
|
$routes->patch('/', 'ValueSet\ValueSet::update');
|
||||||
$routes->delete('/', 'ValueSet\ValueSet::delete');
|
$routes->delete('/', 'ValueSet\ValueSet::delete');
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class Tests extends BaseController {
|
|||||||
protected $modelCal;
|
protected $modelCal;
|
||||||
protected $modelTech;
|
protected $modelTech;
|
||||||
protected $modelGrp;
|
protected $modelGrp;
|
||||||
|
protected $modelMap;
|
||||||
protected $modelValueSet;
|
protected $modelValueSet;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
@ -21,29 +22,138 @@ class Tests extends BaseController {
|
|||||||
$this->modelCal = new \App\Models\Test\TestDefCalModel;
|
$this->modelCal = new \App\Models\Test\TestDefCalModel;
|
||||||
$this->modelTech = new \App\Models\Test\TestDefTechModel;
|
$this->modelTech = new \App\Models\Test\TestDefTechModel;
|
||||||
$this->modelGrp = new \App\Models\Test\TestDefGrpModel;
|
$this->modelGrp = new \App\Models\Test\TestDefGrpModel;
|
||||||
|
$this->modelMap = new \App\Models\Test\TestMapModel;
|
||||||
$this->modelValueSet = new \App\Models\ValueSet\ValueSetModel;
|
$this->modelValueSet = new \App\Models\ValueSet\ValueSetModel;
|
||||||
|
|
||||||
// Basic validation for the main part
|
// Validation rules for main test definition
|
||||||
$this->rules = [
|
$this->rules = [
|
||||||
'TestSiteCode' => 'required',
|
'TestSiteCode' => 'required|min_length[3]|max_length[6]',
|
||||||
'TestSiteName' => 'required',
|
'TestSiteName' => 'required',
|
||||||
'TestType' => 'required'
|
'TestType' => 'required',
|
||||||
|
'SiteID' => 'required'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/tests
|
||||||
|
* List all tests with optional filtering
|
||||||
|
*/
|
||||||
public function index() {
|
public function index() {
|
||||||
$rows = $this->model->getTests();
|
$siteId = $this->request->getGet('SiteID');
|
||||||
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
|
$testType = $this->request->getGet('TestType');
|
||||||
|
$visibleScr = $this->request->getGet('VisibleScr');
|
||||||
|
$visibleRpt = $this->request->getGet('VisibleRpt');
|
||||||
|
$keyword = $this->request->getGet('TestSiteName');
|
||||||
|
|
||||||
|
$builder = $this->db->table('testdefsite')
|
||||||
|
->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
|
||||||
|
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
|
||||||
|
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate,
|
||||||
|
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||||
|
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||||
|
->where('testdefsite.EndDate IS NULL');
|
||||||
|
|
||||||
|
if ($siteId) {
|
||||||
|
$builder->where('testdefsite.SiteID', $siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($testType) {
|
||||||
|
$builder->where('testdefsite.TestType', $testType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($visibleScr !== null) {
|
||||||
|
$builder->where('testdefsite.VisibleScr', $visibleScr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($visibleRpt !== null) {
|
||||||
|
$builder->where('testdefsite.VisibleRpt', $visibleRpt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($keyword) {
|
||||||
|
$builder->like('testdefsite.TestSiteName', $keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $builder->orderBy('testdefsite.SeqScr', 'ASC')->get()->getResultArray();
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
return $this->respond([ 'status' => 'success', 'message' => "No data.", 'data' => [] ], 200);
|
||||||
|
}
|
||||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/tests/{id}
|
||||||
|
* Get single test by ID with all related details
|
||||||
|
*/
|
||||||
public function show($id = null) {
|
public function show($id = null) {
|
||||||
if (!$id) return $this->failValidationErrors('ID is required');
|
if (!$id) return $this->failValidationErrors('ID is required');
|
||||||
$row = $this->model->getTest($id);
|
|
||||||
if (empty($row)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); }
|
$row = $this->model->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||||
|
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||||
|
->where("testdefsite.TestSiteID", $id)
|
||||||
|
->find($id);
|
||||||
|
|
||||||
|
if (!$row) {
|
||||||
|
return $this->respond([ 'status' => 'success', 'message' => "No data.", 'data' => null ], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load related details based on TestType
|
||||||
|
$typeCode = $row['TypeCode'] ?? '';
|
||||||
|
|
||||||
|
if ($typeCode === 'CALC') {
|
||||||
|
// Load calculation details
|
||||||
|
$row['testdefcal'] = $this->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', $id)
|
||||||
|
->where('testdefcal.EndDate IS NULL')
|
||||||
|
->get()->getResultArray();
|
||||||
|
|
||||||
|
// Load test mappings
|
||||||
|
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||||
|
|
||||||
|
} elseif ($typeCode === 'GROUP') {
|
||||||
|
// Load group members with test details
|
||||||
|
$row['testdefgrp'] = $this->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', $id)
|
||||||
|
->where('testdefgrp.EndDate IS NULL')
|
||||||
|
->orderBy('testdefgrp.TestGrpID', 'ASC')
|
||||||
|
->get()->getResultArray();
|
||||||
|
|
||||||
|
// Load test mappings
|
||||||
|
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||||
|
|
||||||
|
} elseif ($typeCode === 'TITLE') {
|
||||||
|
// Load test mappings only for TITLE type
|
||||||
|
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// TEST or PARAM - load technical details
|
||||||
|
$row['testdeftech'] = $this->db->table('testdeftech')
|
||||||
|
->select('testdeftech.*, d.DisciplineName, dept.DepartmentName, w.WorkstationName, e.EquipmentName')
|
||||||
|
->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();
|
||||||
|
|
||||||
|
// Load test mappings
|
||||||
|
$row['testmap'] = $this->modelMap->where('TestSiteID', $id)->where('EndDate IS NULL')->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/tests
|
||||||
|
* Create new test definition
|
||||||
|
*/
|
||||||
public function create() {
|
public function create() {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
@ -55,7 +165,23 @@ class Tests extends BaseController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Insert into Main Table (testdefsite)
|
// 1. Insert into Main Table (testdefsite)
|
||||||
$id = $this->model->insert($input);
|
$testSiteData = [
|
||||||
|
'SiteID' => $input['SiteID'],
|
||||||
|
'TestSiteCode' => $input['TestSiteCode'],
|
||||||
|
'TestSiteName' => $input['TestSiteName'],
|
||||||
|
'TestType' => $input['TestType'],
|
||||||
|
'Description' => $input['Description'] ?? null,
|
||||||
|
'SeqScr' => $input['SeqScr'] ?? 0,
|
||||||
|
'SeqRpt' => $input['SeqRpt'] ?? 0,
|
||||||
|
'IndentLeft' => $input['IndentLeft'] ?? 0,
|
||||||
|
'FontStyle' => $input['FontStyle'] ?? null,
|
||||||
|
'VisibleScr' => $input['VisibleScr'] ?? 1,
|
||||||
|
'VisibleRpt' => $input['VisibleRpt'] ?? 1,
|
||||||
|
'CountStat' => $input['CountStat'] ?? 1,
|
||||||
|
'StartDate' => $input['StartDate'] ?? date('Y-m-d H:i:s')
|
||||||
|
];
|
||||||
|
|
||||||
|
$id = $this->model->insert($testSiteData);
|
||||||
if (!$id) {
|
if (!$id) {
|
||||||
throw new \Exception("Failed to insert main test definition");
|
throw new \Exception("Failed to insert main test definition");
|
||||||
}
|
}
|
||||||
@ -69,28 +195,52 @@ class Tests extends BaseController {
|
|||||||
return $this->failServerError('Transaction failed');
|
return $this->failServerError('Transaction failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => "data created successfully", 'data'=> $id ]);
|
return $this->respondCreated([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => "Test created successfully",
|
||||||
|
'data' => ['TestSiteID' => $id]
|
||||||
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->db->transRollback();
|
$this->db->transRollback();
|
||||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT/PATCH /api/tests/{id}
|
||||||
|
* Update existing test definition
|
||||||
|
*/
|
||||||
public function update($id = null) {
|
public function update($id = null) {
|
||||||
$input = $this->request->getJSON(true);
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
// Determine ID
|
// Determine ID
|
||||||
if (!$id && isset($input["TestSiteID"])) { $id = $input["TestSiteID"]; }
|
if (!$id && isset($input["TestSiteID"])) { $id = $input["TestSiteID"]; }
|
||||||
if (!$id) { return $this->failValidationErrors('TestSiteID is required.'); }
|
if (!$id) { return $this->failValidationErrors('TestSiteID is required.'); }
|
||||||
|
|
||||||
// Optional validation
|
// Verify record exists
|
||||||
// if (!$this->validateData($input, $this->rules)) { ... }
|
$existing = $this->model->find($id);
|
||||||
|
if (!$existing) {
|
||||||
|
return $this->failNotFound('Test not found');
|
||||||
|
}
|
||||||
|
|
||||||
$this->db->transStart();
|
$this->db->transStart();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Update Main Table
|
// 1. Update Main Table
|
||||||
$this->model->update($id, $input);
|
$testSiteData = [];
|
||||||
|
$allowedUpdateFields = ['TestSiteCode', 'TestSiteName', 'TestType', 'Description',
|
||||||
|
'SeqScr', 'SeqRpt', 'IndentLeft', 'FontStyle',
|
||||||
|
'VisibleScr', 'VisibleRpt', 'CountStat', 'StartDate'];
|
||||||
|
|
||||||
|
foreach ($allowedUpdateFields as $field) {
|
||||||
|
if (isset($input[$field])) {
|
||||||
|
$testSiteData[$field] = $input[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($testSiteData)) {
|
||||||
|
$this->model->update($id, $testSiteData);
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Handle Details
|
// 2. Handle Details
|
||||||
$this->handleDetails($id, $input, 'update');
|
$this->handleDetails($id, $input, 'update');
|
||||||
@ -101,7 +251,83 @@ class Tests extends BaseController {
|
|||||||
return $this->failServerError('Transaction failed');
|
return $this->failServerError('Transaction failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->respondCreated([ 'status' => 'success', 'message' => "data updated successfully", 'data'=> $id ]);
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => "Test updated successfully",
|
||||||
|
'data' => ['TestSiteID' => $id]
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->db->transRollback();
|
||||||
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/tests/{id}
|
||||||
|
* Soft delete test by setting EndDate
|
||||||
|
*/
|
||||||
|
public function delete($id = null) {
|
||||||
|
$input = $this->request->getJSON(true);
|
||||||
|
|
||||||
|
// Determine ID
|
||||||
|
if (!$id && isset($input["TestSiteID"])) { $id = $input["TestSiteID"]; }
|
||||||
|
if (!$id) { return $this->failValidationErrors('TestSiteID is required.'); }
|
||||||
|
|
||||||
|
// Verify record exists
|
||||||
|
$existing = $this->model->find($id);
|
||||||
|
if (!$existing) {
|
||||||
|
return $this->failNotFound('Test not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already disabled
|
||||||
|
if (!empty($existing['EndDate'])) {
|
||||||
|
return $this->failValidationErrors('Test is already disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->transStart();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
// 1. Soft delete main record
|
||||||
|
$this->model->update($id, ['EndDate' => $now]);
|
||||||
|
|
||||||
|
// 2. Get TestType to handle related records
|
||||||
|
$testType = $existing['TestType'];
|
||||||
|
$vs = $this->modelValueSet->find($testType);
|
||||||
|
$typeCode = $vs['VValue'] ?? '';
|
||||||
|
|
||||||
|
// 3. Soft delete related records based on TestType
|
||||||
|
if ($typeCode === 'CALC') {
|
||||||
|
$this->db->table('testdefcal')
|
||||||
|
->where('TestSiteID', $id)
|
||||||
|
->update(['EndDate' => $now]);
|
||||||
|
} elseif ($typeCode === 'GROUP') {
|
||||||
|
$this->db->table('testdefgrp')
|
||||||
|
->where('TestSiteID', $id)
|
||||||
|
->update(['EndDate' => $now]);
|
||||||
|
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
||||||
|
$this->db->table('testdeftech')
|
||||||
|
->where('TestSiteID', $id)
|
||||||
|
->update(['EndDate' => $now]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Soft delete test mappings
|
||||||
|
$this->db->table('testmap')
|
||||||
|
->where('TestSiteID', $id)
|
||||||
|
->update(['EndDate' => $now]);
|
||||||
|
|
||||||
|
$this->db->transComplete();
|
||||||
|
|
||||||
|
if ($this->db->transStatus() === false) {
|
||||||
|
return $this->failServerError('Transaction failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => "Test disabled successfully",
|
||||||
|
'data' => ['TestSiteID' => $id, 'EndDate' => $now]
|
||||||
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->db->transRollback();
|
$this->db->transRollback();
|
||||||
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
||||||
@ -120,67 +346,182 @@ class Tests extends BaseController {
|
|||||||
$testTypeID = $existing['TestType'] ?? null;
|
$testTypeID = $existing['TestType'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$testTypeID) return; // Should not happen if required
|
if (!$testTypeID) return;
|
||||||
|
|
||||||
// Get Type Code (TEST, PARAM, CALC, GROUP, TITLE)
|
// Get Type Code (TEST, PARAM, CALC, GROUP, TITLE)
|
||||||
$vs = $this->modelValueSet->find($testTypeID);
|
$vs = $this->modelValueSet->find($testTypeID);
|
||||||
$typeCode = $vs['VValue'] ?? '';
|
$typeCode = $vs['VValue'] ?? '';
|
||||||
|
|
||||||
// Get details data if present (for 'details' key in unified JSON)
|
// Get details data from input
|
||||||
// We accept both flat (top-level) and nested 'details' for flexibility, prefer 'details'
|
|
||||||
$details = $input['details'] ?? $input;
|
$details = $input['details'] ?? $input;
|
||||||
$details['TestSiteID'] = $testSiteID; // Ensure foreign key is set
|
$details['TestSiteID'] = $testSiteID;
|
||||||
$details['SiteID'] = $input['SiteID'] ?? 1;
|
$details['SiteID'] = $input['SiteID'] ?? 1;
|
||||||
|
|
||||||
switch ($typeCode) {
|
switch ($typeCode) {
|
||||||
case 'CALC':
|
case 'CALC':
|
||||||
$this->saveSubTable($this->modelCal, $testSiteID, $details, $action, 'TestCalID');
|
$this->saveCalcDetails($testSiteID, $details, $action);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'GROUP':
|
case 'GROUP':
|
||||||
// Groups are special: List of members
|
$this->saveGroupDetails($testSiteID, $details, $input, $action);
|
||||||
// Payload expected: details: { members: [{Member: 1}, {Member: 2}] }
|
|
||||||
if ($action === 'update') {
|
|
||||||
$this->modelGrp->where('TestSiteID', $testSiteID)->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
$members = $details['members'] ?? ($input['Members'] ?? []);
|
|
||||||
if (is_array($members)) {
|
|
||||||
foreach ($members as $m) {
|
|
||||||
$memberID = is_array($m) ? ($m['Member'] ?? null) : $m;
|
|
||||||
if ($memberID) {
|
|
||||||
$this->modelGrp->insert([
|
|
||||||
'SiteID' => $details['SiteID'],
|
|
||||||
'TestSiteID' => $testSiteID,
|
|
||||||
'Member' => $memberID
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'TITLE':
|
||||||
|
// TITLE type only has testdefsite, no additional details needed
|
||||||
|
// But we should save test mappings if provided
|
||||||
|
if (isset($input['testmap']) && is_array($input['testmap'])) {
|
||||||
|
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'TEST':
|
case 'TEST':
|
||||||
case 'PARAM':
|
case 'PARAM':
|
||||||
default:
|
default:
|
||||||
// Default to TestDefTech for 'TEST' and 'PARAM'
|
$this->saveTechDetails($testSiteID, $details, $action, $typeCode);
|
||||||
$this->saveSubTable($this->modelTech, $testSiteID, $details, $action, 'TestTechID');
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save test mappings for TEST and CALC types as well
|
||||||
|
if (in_array($typeCode, ['TEST', 'CALC']) && isset($input['testmap']) && is_array($input['testmap'])) {
|
||||||
|
$this->saveTestMap($testSiteID, $input['testmap'], $action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function saveSubTable($model, $testSiteID, $data, $action, $pkName) {
|
/**
|
||||||
|
* Save technical details for TEST and PARAM types
|
||||||
|
*/
|
||||||
|
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),
|
||||||
|
'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,
|
||||||
|
'Factor' => $data['Factor'] ?? null,
|
||||||
|
'Unit2' => $data['Unit2'] ?? null,
|
||||||
|
'Decimal' => $data['Decimal'] ?? 2,
|
||||||
|
'CollReq' => $data['CollReq'] ?? null,
|
||||||
|
'Method' => $data['Method'] ?? null,
|
||||||
|
'ExpectedTAT' => $data['ExpectedTAT'] ?? null
|
||||||
|
];
|
||||||
|
|
||||||
if ($action === 'update') {
|
if ($action === 'update') {
|
||||||
// Check existence
|
$exists = $this->db->table('testdeftech')
|
||||||
$exists = $model->where('TestSiteID', $testSiteID)->first();
|
->where('TestSiteID', $testSiteID)
|
||||||
|
->where('EndDate IS NULL')
|
||||||
|
->get()->getRowArray();
|
||||||
|
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
$model->update($exists[$pkName], $data);
|
$this->modelTech->update($exists['TestTechID'], $techData);
|
||||||
} else {
|
} else {
|
||||||
$model->insert($data);
|
$this->modelTech->insert($techData);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$model->insert($data);
|
$this->modelTech->insert($techData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Save calculation details for CALC type
|
||||||
|
*/
|
||||||
|
private function saveCalcDetails($testSiteID, $data, $action) {
|
||||||
|
$calcData = [
|
||||||
|
'SiteID' => $data['SiteID'],
|
||||||
|
'TestSiteID' => $testSiteID,
|
||||||
|
'DisciplineID' => $data['DisciplineID'] ?? null,
|
||||||
|
'DepartmentID' => $data['DepartmentID'] ?? null,
|
||||||
|
'FormulaInput' => $data['FormulaInput'] ?? null,
|
||||||
|
'FormulaCode' => $data['FormulaCode'] ?? $data['Formula'] ?? null,
|
||||||
|
'RefType' => $data['RefType'] ?? 'NMRC',
|
||||||
|
'Unit1' => $data['Unit1'] ?? $data['ResultUnit'] ?? null,
|
||||||
|
'Factor' => $data['Factor'] ?? null,
|
||||||
|
'Unit2' => $data['Unit2'] ?? null,
|
||||||
|
'Decimal' => $data['Decimal'] ?? 2,
|
||||||
|
'Method' => $data['Method'] ?? null
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($action === 'update') {
|
||||||
|
$exists = $this->db->table('testdefcal')
|
||||||
|
->where('TestSiteID', $testSiteID)
|
||||||
|
->where('EndDate IS NULL')
|
||||||
|
->get()->getRowArray();
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
$this->modelCal->update($exists['TestCalID'], $calcData);
|
||||||
|
} else {
|
||||||
|
$this->modelCal->insert($calcData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->modelCal->insert($calcData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save group details for GROUP type
|
||||||
|
*/
|
||||||
|
private function saveGroupDetails($testSiteID, $data, $input, $action) {
|
||||||
|
if ($action === 'update') {
|
||||||
|
// Soft delete existing members
|
||||||
|
$this->db->table('testdefgrp')
|
||||||
|
->where('TestSiteID', $testSiteID)
|
||||||
|
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get members from details or input
|
||||||
|
$members = $data['members'] ?? ($input['Members'] ?? []);
|
||||||
|
|
||||||
|
if (is_array($members)) {
|
||||||
|
foreach ($members as $m) {
|
||||||
|
$memberID = is_array($m) ? ($m['Member'] ?? ($m['TestSiteID'] ?? null)) : $m;
|
||||||
|
if ($memberID) {
|
||||||
|
$this->modelGrp->insert([
|
||||||
|
'SiteID' => $data['SiteID'],
|
||||||
|
'TestSiteID' => $testSiteID,
|
||||||
|
'Member' => $memberID
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save test mappings
|
||||||
|
*/
|
||||||
|
private function saveTestMap($testSiteID, $mappings, $action) {
|
||||||
|
if ($action === 'update') {
|
||||||
|
// Soft delete existing mappings
|
||||||
|
$this->db->table('testmap')
|
||||||
|
->where('TestSiteID', $testSiteID)
|
||||||
|
->update(['EndDate' => date('Y-m-d H:i:s')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($mappings)) {
|
||||||
|
foreach ($mappings as $map) {
|
||||||
|
$mapData = [
|
||||||
|
'TestSiteID' => $testSiteID,
|
||||||
|
'HostType' => $map['HostType'] ?? null,
|
||||||
|
'HostID' => $map['HostID'] ?? null,
|
||||||
|
'HostDataSource' => $map['HostDataSource'] ?? null,
|
||||||
|
'HostTestCode' => $map['HostTestCode'] ?? null,
|
||||||
|
'HostTestName' => $map['HostTestName'] ?? null,
|
||||||
|
'ClientType' => $map['ClientType'] ?? null,
|
||||||
|
'ClientID' => $map['ClientID'] ?? null,
|
||||||
|
'ClientDataSource' => $map['ClientDataSource'] ?? null,
|
||||||
|
'ConDefID' => $map['ConDefID'] ?? null,
|
||||||
|
'ClientTestCode' => $map['ClientTestCode'] ?? null,
|
||||||
|
'ClientTestName' => $map['ClientTestName'] ?? null
|
||||||
|
];
|
||||||
|
$this->modelMap->insert($mapData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -6,95 +6,114 @@ use CodeIgniter\Database\Migration;
|
|||||||
|
|
||||||
class CreateTestsTable extends Migration {
|
class CreateTestsTable extends Migration {
|
||||||
public function up() {
|
public function up() {
|
||||||
|
// testdefsite - Main test definition table
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'TestSiteID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
'TestSiteID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||||
'SiteID' => ['type' => 'INT', 'null' => false],
|
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||||
'TestSiteCode' => ['type' => 'varchar', 'constraint'=> 6, 'null' => false],
|
'TestSiteCode' => ['type' => 'varchar', 'constraint'=> 6, 'null' => false],
|
||||||
'TestSiteName' => ['type' => 'varchar', 'constraint'=> 50, 'null' => false],
|
'TestSiteName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => false],
|
||||||
'TestType' => ['type' => 'int', 'null' => false],
|
'TestType' => ['type' => 'int', 'null' => false],
|
||||||
'Description' => ['type' => 'varchar', 'constraint'=> 150, 'null' => true],
|
'Description' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
|
||||||
'SeqScr' => ['type' => 'int', 'null' => true],
|
'SeqScr' => ['type' => 'int', 'null' => true],
|
||||||
'SeqRpt' => ['type' => 'int', 'null' => true],
|
'SeqRpt' => ['type' => 'int', 'null' => true],
|
||||||
'IndentLeft' => ['type' => 'int', 'null' => true],
|
'IndentLeft' => ['type' => 'int', 'null' => true, 'default' => 0],
|
||||||
'VisibleScr' => ['type' => 'int', 'null' => true],
|
'FontStyle' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||||
'VisibleRpt' => ['type' => 'int', 'null' => true],
|
'VisibleScr' => ['type' => 'int', 'null' => true, 'default' => 1],
|
||||||
'CountStat' => ['type' => 'int', 'null' => true],
|
'VisibleRpt' => ['type' => 'int', 'null' => true, 'default' => 1],
|
||||||
|
'CountStat' => ['type' => 'int', 'null' => true, 'default' => 1],
|
||||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||||
'StartDate' => ['type' => 'Datetime', 'null' => true],
|
'StartDate' => ['type' => 'Datetime', 'null' => true],
|
||||||
'EndDate' => ['type' => 'Datetime', 'null' => true],
|
'EndDate' => ['type' => 'Datetime', 'null' => true],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('TestSiteID', true);
|
$this->forge->addKey('TestSiteID', true);
|
||||||
$this->forge->createTable('testdefsite');
|
$this->forge->createTable('testdefsite');
|
||||||
|
|
||||||
|
// testdeftech - Technical definition for TEST and PARAM types
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'TestTechID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
'TestTechID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||||
|
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||||
'DisciplineID' => ['type' => 'int', 'null' => true],
|
'DisciplineID' => ['type' => 'int', 'null' => true],
|
||||||
'DepartmentID' => ['type' => 'int', 'null' => true],
|
'DepartmentID' => ['type' => 'int', 'null' => true],
|
||||||
'ResultType' => ['type' => 'int', 'null' => true],
|
'WorkstationID' => ['type' => 'INT', 'null' => true],
|
||||||
'RefType' => ['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],
|
'VSet' => ['type' => 'int', 'null' => true],
|
||||||
'SpcType' => ['type' => 'int', 'null' => true],
|
'SpcType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true],
|
||||||
'ReqQty' => ['type' => 'int', 'null' => true],
|
'SpcDesc' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true],
|
||||||
'ReqQtyUnit' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
'ReqQty' => ['type' => 'DECIMAL', 'constraint'=> '10,2', 'null' => true],
|
||||||
'Unit1' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
'ReqQtyUnit' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||||
'Factor' => ['type' => 'int', 'null' => true],
|
'Unit1' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||||
'Unit2' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
'Factor' => ['type' => 'DECIMAL', 'constraint'=> '10,4', 'null' => true],
|
||||||
'Decimal' => ['type' => 'int', 'null' => true],
|
'Unit2' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||||
'CollReq' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
'Decimal' => ['type' => 'int', 'null' => true, 'default' => 2],
|
||||||
'Method' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
'CollReq' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
|
||||||
|
'Method' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||||
'ExpectedTAT' => ['type' => 'INT', 'null' => true],
|
'ExpectedTAT' => ['type' => 'INT', 'null' => true],
|
||||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||||
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('TestTechID', true);
|
$this->forge->addKey('TestTechID', true);
|
||||||
|
$this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||||
$this->forge->createTable('testdeftech');
|
$this->forge->createTable('testdeftech');
|
||||||
|
|
||||||
|
// testdefcal - Calculation definition for CALC type
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'TestCalID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
'TestCalID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||||
|
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||||
'DisciplineID' => ['type' => 'INT', 'null' => true],
|
'DisciplineID' => ['type' => 'INT', 'null' => true],
|
||||||
'DepartmentID' => ['type' => 'INT', 'null' => true],
|
'DepartmentID' => ['type' => 'INT', 'null' => true],
|
||||||
'FormulaInput' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
'FormulaInput' => ['type' => 'varchar', 'constraint'=> 255, 'null' => true],
|
||||||
'FormulaCode' => ['type' => 'varchar', 'constraint'=>150, 'null' => true],
|
'FormulaCode' => ['type' => 'text', 'null' => true],
|
||||||
'Unit1' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
'RefType' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true, 'default' => 'NMRC'],
|
||||||
'Factor' => ['type' => 'int', 'null' => true],
|
'Unit1' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||||
'Unit2' => ['type' => 'varchar', 'constraint'=>20, 'null' => true],
|
'Factor' => ['type' => 'DECIMAL', 'constraint'=> '10,4', 'null' => true],
|
||||||
'Decimal' => ['type' => 'int', 'null' => true],
|
'Unit2' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||||
|
'Decimal' => ['type' => 'int', 'null' => true, 'default' => 2],
|
||||||
|
'Method' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||||
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('TestCalID', true);
|
$this->forge->addKey('TestCalID', true);
|
||||||
|
$this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||||
$this->forge->createTable('testdefcal');
|
$this->forge->createTable('testdefcal');
|
||||||
|
|
||||||
|
// testdefgrp - Group definition for GROUP type
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'TestGrpID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
'TestGrpID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||||
|
'SiteID' => ['type' => 'INT', 'null' => false],
|
||||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||||
'Member' => ['type' => 'INT', 'null' => true],
|
'Member' => ['type' => 'INT', 'null' => true],
|
||||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||||
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('TestGrpID', true);
|
$this->forge->addKey('TestGrpID', true);
|
||||||
|
$this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||||
|
$this->forge->addForeignKey('Member', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||||
$this->forge->createTable('testdefgrp');
|
$this->forge->createTable('testdefgrp');
|
||||||
|
|
||||||
|
// testmap - Test mapping for all types
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'TestMapID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
'TestMapID' => ['type' => 'INT', 'auto_increment' => true, 'unsigned' => true],
|
||||||
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
'TestSiteID' => ['type' => 'INT', 'null' => false],
|
||||||
'HostType' => ['type' => 'int', 'null' => true],
|
'HostType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||||
'HostID' => ['type' => 'int', 'null' => true],
|
'HostID' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||||
'HostDataSource' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
'HostDataSource' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||||
'HostTestCode' => ['type' => 'varchar', 'constraint'=>10, 'null' => true],
|
'HostTestCode' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true],
|
||||||
'HostTestName' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
'HostTestName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true],
|
||||||
'ClientType' => ['type' => 'int', 'null' => true],
|
'ClientType' => ['type' => 'varchar', 'constraint'=> 20, 'null' => true],
|
||||||
'ClientID' => ['type' => 'int', 'null' => true],
|
'ClientID' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||||
'ClientDataSource' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
'ClientDataSource' => ['type' => 'varchar', 'constraint'=> 50, 'null' => true],
|
||||||
'ConDefID' => ['type' => 'int', 'null' => true],
|
'ConDefID' => ['type' => 'INT', 'null' => true],
|
||||||
'ClientTestCode' => ['type' => 'varchar', 'constraint'=>10, 'null' => true],
|
'ClientTestCode' => ['type' => 'varchar', 'constraint'=> 10, 'null' => true],
|
||||||
'ClientTestName' => ['type' => 'varchar', 'constraint'=>50, 'null' => true],
|
'ClientTestName' => ['type' => 'varchar', 'constraint'=> 100, 'null' => true],
|
||||||
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
'CreateDate' => ['type' => 'Datetime', 'null' => true],
|
||||||
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
'EndDate' => ['type' => 'Datetime', 'null' => true]
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('TestMapID', true);
|
$this->forge->addKey('TestMapID', true);
|
||||||
|
$this->forge->addForeignKey('TestSiteID', 'testdefsite', 'TestSiteID', 'CASCADE', 'CASCADE');
|
||||||
$this->forge->createTable('testmap');
|
$this->forge->createTable('testmap');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,4 +124,4 @@ class CreateTestsTable extends Migration {
|
|||||||
$this->forge->dropTable('testdefgrp');
|
$this->forge->dropTable('testdefgrp');
|
||||||
$this->forge->dropTable('testmap');
|
$this->forge->dropTable('testmap');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,22 @@ use App\Models\BaseModel;
|
|||||||
class TestDefCalModel extends BaseModel {
|
class TestDefCalModel extends BaseModel {
|
||||||
protected $table = 'testdefcal';
|
protected $table = 'testdefcal';
|
||||||
protected $primaryKey = 'TestCalID';
|
protected $primaryKey = 'TestCalID';
|
||||||
protected $allowedFields = ['SiteID', 'TestSiteID', 'DisciplineID', 'DepartmentID','FormulaCode', 'FormulaInput',
|
protected $allowedFields = [
|
||||||
'Unit1', 'Factor', 'Unit2', 'Decimal' ,'CreateDate', 'EndDate'];
|
'SiteID',
|
||||||
|
'TestSiteID',
|
||||||
|
'DisciplineID',
|
||||||
|
'DepartmentID',
|
||||||
|
'FormulaInput',
|
||||||
|
'FormulaCode',
|
||||||
|
'RefType',
|
||||||
|
'Unit1',
|
||||||
|
'Factor',
|
||||||
|
'Unit2',
|
||||||
|
'Decimal',
|
||||||
|
'Method',
|
||||||
|
'CreateDate',
|
||||||
|
'EndDate'
|
||||||
|
];
|
||||||
|
|
||||||
protected $useTimestamps = true;
|
protected $useTimestamps = true;
|
||||||
protected $createdField = 'CreateDate';
|
protected $createdField = 'CreateDate';
|
||||||
|
|||||||
@ -7,23 +7,70 @@ use App\Models\BaseModel;
|
|||||||
class TestDefSiteModel extends BaseModel {
|
class TestDefSiteModel extends BaseModel {
|
||||||
protected $table = 'testdefsite';
|
protected $table = 'testdefsite';
|
||||||
protected $primaryKey = 'TestSiteID';
|
protected $primaryKey = 'TestSiteID';
|
||||||
protected $allowedFields = ['SiteID', 'TestSiteCode', 'TestSiteName', 'TestType', 'Description', 'SeqScr', 'SeqRpt', 'IndentLeft',
|
protected $allowedFields = [
|
||||||
'VisibleScr', 'VisibleRpt', 'CountStat', 'CreateDate', 'EndDate'];
|
'SiteID',
|
||||||
|
'TestSiteCode',
|
||||||
|
'TestSiteName',
|
||||||
|
'TestType',
|
||||||
|
'Description',
|
||||||
|
'SeqScr',
|
||||||
|
'SeqRpt',
|
||||||
|
'IndentLeft',
|
||||||
|
'FontStyle',
|
||||||
|
'VisibleScr',
|
||||||
|
'VisibleRpt',
|
||||||
|
'CountStat',
|
||||||
|
'CreateDate',
|
||||||
|
'StartDate',
|
||||||
|
'EndDate'
|
||||||
|
];
|
||||||
|
|
||||||
protected $useTimestamps = true;
|
protected $useTimestamps = true;
|
||||||
protected $createdField = 'CreateDate';
|
protected $createdField = 'CreateDate';
|
||||||
protected $updatedField = '';
|
protected $updatedField = 'StartDate';
|
||||||
protected $useSoftDeletes = true;
|
protected $useSoftDeletes = true;
|
||||||
protected $deletedField = "EndDate";
|
protected $deletedField = "EndDate";
|
||||||
|
|
||||||
public function getTests() {
|
/**
|
||||||
$rows = $this->select("TestSiteID, TestSiteCode, TestSiteName, TestType, valueset.VValue as TypeCode, valueset.VDesc as TypeName ")
|
* Get all tests with type information
|
||||||
|
*/
|
||||||
|
public function getTests($siteId = null, $testType = null, $visibleScr = null, $visibleRpt = null, $keyword = null) {
|
||||||
|
$builder = $this->select("testdefsite.TestSiteID, testdefsite.TestSiteCode, testdefsite.TestSiteName, testdefsite.TestType,
|
||||||
|
testdefsite.SeqScr, testdefsite.SeqRpt, testdefsite.VisibleScr, testdefsite.VisibleRpt,
|
||||||
|
testdefsite.CountStat, testdefsite.StartDate, testdefsite.EndDate,
|
||||||
|
valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||||
->findAll();
|
->where('testdefsite.EndDate IS NULL');
|
||||||
return $rows;
|
|
||||||
|
if ($siteId) {
|
||||||
|
$builder->where('testdefsite.SiteID', $siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($testType) {
|
||||||
|
$builder->where('testdefsite.TestType', $testType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($visibleScr !== null) {
|
||||||
|
$builder->where('testdefsite.VisibleScr', $visibleScr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($visibleRpt !== null) {
|
||||||
|
$builder->where('testdefsite.VisibleRpt', $visibleRpt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($keyword) {
|
||||||
|
$builder->like('testdefsite.TestSiteName', $keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $builder->orderBy('testdefsite.SeqScr', 'ASC')->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single test with all related details based on TestType
|
||||||
|
*/
|
||||||
public function getTest($TestSiteID) {
|
public function getTest($TestSiteID) {
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
|
||||||
$row = $this->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
$row = $this->select("testdefsite.*, valueset.VValue as TypeCode, valueset.VDesc as TypeName")
|
||||||
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
->join("valueset", "valueset.VID=testdefsite.TestType", "left")
|
||||||
->where("testdefsite.TestSiteID", $TestSiteID)
|
->where("testdefsite.TestSiteID", $TestSiteID)
|
||||||
@ -31,14 +78,58 @@ class TestDefSiteModel extends BaseModel {
|
|||||||
|
|
||||||
if (!$row) return null;
|
if (!$row) return null;
|
||||||
|
|
||||||
if ($row['TypeCode'] == 'Calculated') {
|
$typeCode = $row['TypeCode'] ?? '';
|
||||||
$row['testdefcal'] = $this->db->query("select * from testdefcal where TestSiteID='$TestSiteID'")->getResultArray();
|
|
||||||
} elseif ($row['TypeCode'] == 'GROUP') {
|
// Load related details based on TestType
|
||||||
$row['testdefgrp'] = $this->db->query("select testdefgrp.*, t.TestSiteCode, t.TestSiteName from testdefgrp left join testdefsite t on t.TestSiteID=testdefgrp.Member where testdefgrp.TestSiteID='$TestSiteID'")->getResultArray();
|
if ($typeCode === 'CALC') {
|
||||||
} else {
|
// Load calculation details with joined discipline and department
|
||||||
$row['testdeftech'] = $this->db->query("select * from testdeftech where TestSiteID='$TestSiteID'")->getResultArray();
|
$row['testdefcal'] = $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();
|
||||||
|
|
||||||
|
// Load test mappings
|
||||||
|
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||||
|
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||||
|
|
||||||
|
} elseif ($typeCode === 'GROUP') {
|
||||||
|
// Load group members with test details
|
||||||
|
$row['testdefgrp'] = $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();
|
||||||
|
|
||||||
|
// Load test mappings
|
||||||
|
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||||
|
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||||
|
|
||||||
|
} elseif ($typeCode === 'TITLE') {
|
||||||
|
// Load test mappings only for TITLE type
|
||||||
|
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||||
|
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||||
|
|
||||||
|
} elseif (in_array($typeCode, ['TEST', 'PARAM'])) {
|
||||||
|
// TEST or PARAM - load technical details with joined tables
|
||||||
|
$row['testdeftech'] = $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();
|
||||||
|
|
||||||
|
// Load test mappings
|
||||||
|
$testMapModel = new \App\Models\Test\TestMapModel();
|
||||||
|
$row['testmap'] = $testMapModel->where('TestSiteID', $TestSiteID)->where('EndDate IS NULL')->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $row;
|
return $row;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,30 @@ use App\Models\BaseModel;
|
|||||||
class TestDefTechModel extends BaseModel {
|
class TestDefTechModel extends BaseModel {
|
||||||
protected $table = 'testdeftech';
|
protected $table = 'testdeftech';
|
||||||
protected $primaryKey = 'TestTechID';
|
protected $primaryKey = 'TestTechID';
|
||||||
protected $allowedFields = ['SiteID', 'TestSiteID', 'DisciplineID', 'DepartmentID', 'WorkstationID', 'EquipmentID', 'VSet', 'SpcType',
|
protected $allowedFields = [
|
||||||
'ReqQty', 'ReqQtyUnit', 'Unit1', 'Factor', 'Unit2', 'Decimal', 'CollReq', 'Method', 'ExpectedTAT', 'CreateDate', 'EndDate'];
|
'SiteID',
|
||||||
|
'TestSiteID',
|
||||||
|
'DisciplineID',
|
||||||
|
'DepartmentID',
|
||||||
|
'WorkstationID',
|
||||||
|
'EquipmentID',
|
||||||
|
'ResultType',
|
||||||
|
'RefType',
|
||||||
|
'VSet',
|
||||||
|
'SpcType',
|
||||||
|
'SpcDesc',
|
||||||
|
'ReqQty',
|
||||||
|
'ReqQtyUnit',
|
||||||
|
'Unit1',
|
||||||
|
'Factor',
|
||||||
|
'Unit2',
|
||||||
|
'Decimal',
|
||||||
|
'CollReq',
|
||||||
|
'Method',
|
||||||
|
'ExpectedTAT',
|
||||||
|
'CreateDate',
|
||||||
|
'EndDate'
|
||||||
|
];
|
||||||
|
|
||||||
protected $useTimestamps = true;
|
protected $useTimestamps = true;
|
||||||
protected $createdField = 'CreateDate';
|
protected $createdField = 'CreateDate';
|
||||||
|
|||||||
@ -7,8 +7,22 @@ use App\Models\BaseModel;
|
|||||||
class TestMapModel extends BaseModel {
|
class TestMapModel extends BaseModel {
|
||||||
protected $table = 'testmap';
|
protected $table = 'testmap';
|
||||||
protected $primaryKey = 'TestMapID';
|
protected $primaryKey = 'TestMapID';
|
||||||
protected $allowedFields = ['HostType', 'HostID', 'HostDataSource', 'HostTestCode', 'HostTestName',
|
protected $allowedFields = [
|
||||||
'ClientType', 'ClientID', 'ClientTestCode', 'ClientTestName', 'CreateDate', 'EndDate' ];
|
'TestSiteID',
|
||||||
|
'HostType',
|
||||||
|
'HostID',
|
||||||
|
'HostDataSource',
|
||||||
|
'HostTestCode',
|
||||||
|
'HostTestName',
|
||||||
|
'ClientType',
|
||||||
|
'ClientID',
|
||||||
|
'ClientDataSource',
|
||||||
|
'ConDefID',
|
||||||
|
'ClientTestCode',
|
||||||
|
'ClientTestName',
|
||||||
|
'CreateDate',
|
||||||
|
'EndDate'
|
||||||
|
];
|
||||||
|
|
||||||
protected $useTimestamps = true;
|
protected $useTimestamps = true;
|
||||||
protected $createdField = 'CreateDate';
|
protected $createdField = 'CreateDate';
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/all.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/all.min.js"></script>
|
||||||
|
|
||||||
<!-- Alpine.js -->
|
<!-- Alpine.js -->
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen flex" style="background: rgb(var(--color-bg));" x-data="layout()">
|
<body class="min-h-screen flex" style="background: rgb(var(--color-bg));" x-data="layout()">
|
||||||
|
|||||||
249
app/Views/v2/master/tests/calc_dialog.php
Normal file
249
app/Views/v2/master/tests/calc_dialog.php
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
<!-- Calculated Test Form Modal -->
|
||||||
|
<div
|
||||||
|
x-show="showModal && currentDialogType === 'CALC'"
|
||||||
|
x-cloak
|
||||||
|
class="modal-overlay"
|
||||||
|
@click.self="closeModal()"
|
||||||
|
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-6 max-w-3xl 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"
|
||||||
|
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="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-calculator" style="color: rgb(var(--color-secondary));"></i>
|
||||||
|
<span x-text="isEditing ? 'Edit Calculated Test' : 'New Calculated Test'"></span>
|
||||||
|
</h3>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Test Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:class="errors.TestSiteName && 'input-error'"
|
||||||
|
x-model="form.TestSiteName"
|
||||||
|
placeholder="Absolute Neutrophils"
|
||||||
|
/>
|
||||||
|
<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">Test Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input font-mono"
|
||||||
|
:class="errors.TestSiteCode && 'input-error'"
|
||||||
|
x-model="form.TestSiteCode"
|
||||||
|
placeholder="ANC"
|
||||||
|
/>
|
||||||
|
<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 & Result Unit -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Test Type</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="input input-disabled bg-base-200" x-model="form.TestTypeName" readonly />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Result Unit</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
x-model="form.ResultUnit"
|
||||||
|
placeholder="% or cells/µL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formula Section -->
|
||||||
|
<div class="border rounded-xl p-4 bg-base-50">
|
||||||
|
<h4 class="font-semibold flex items-center gap-2 mb-4">
|
||||||
|
<i class="fa-solid fa-function"></i>
|
||||||
|
Calculation Formula
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Formula Expression <span style="color: rgb(var(--color-error));">*</span></span>
|
||||||
|
<span class="label-text-alt text-base-content/60">Use test codes as variables in curly braces</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="input font-mono h-20"
|
||||||
|
:class="errors.Formula && 'input-error'"
|
||||||
|
x-model="form.Formula"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formula Variables/Tests -->
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Used Test Variables</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>
|
||||||
|
<template x-for="v in form.formulaVars" :key="v">
|
||||||
|
<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>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Variable -->
|
||||||
|
<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">
|
||||||
|
<option :value="t.TestSiteCode" x-text="t.TestSiteCode + ' - ' + t.TestSiteName"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-outline" @click="addFormulaVar()">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result Options -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Decimal Places</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input text-center"
|
||||||
|
x-model="form.DecimalPlaces"
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
placeholder="2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Rounding Method</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>
|
||||||
|
</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="Calculation description..."
|
||||||
|
></textarea>
|
||||||
|
</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" />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Site</span>
|
||||||
|
</label>
|
||||||
|
<select class="select" 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>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</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-secondary 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 Test'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
241
app/Views/v2/master/tests/group_dialog.php
Normal file
241
app/Views/v2/master/tests/group_dialog.php
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
<!-- Group Test Form Modal -->
|
||||||
|
<div
|
||||||
|
x-show="showModal && currentDialogType === 'GROUP'"
|
||||||
|
x-cloak
|
||||||
|
class="modal-overlay"
|
||||||
|
@click.self="closeModal()"
|
||||||
|
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-6 max-w-4xl 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"
|
||||||
|
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="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" style="color: rgb(var(--color-primary));"></i>
|
||||||
|
<span x-text="isEditing ? 'Edit Test Group' : 'New Test Group'"></span>
|
||||||
|
</h3>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Group Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
: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>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input 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">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Test Type</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="input input-disabled bg-base-200" x-model="form.TestTypeName" readonly />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">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>
|
||||||
|
</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)
|
||||||
|
</h4>
|
||||||
|
<button class="btn btn-primary btn-sm" @click="openTestSelector()">
|
||||||
|
<i class="fa-solid fa-plus mr-1"></i> Add Tests
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected Members List -->
|
||||||
|
<div class="space-y-2 max-h-60 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>
|
||||||
|
</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>
|
||||||
|
</button>
|
||||||
|
</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" />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Site</span>
|
||||||
|
</label>
|
||||||
|
<select class="select" 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>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input mb-4"
|
||||||
|
placeholder="Search tests..."
|
||||||
|
x-model="testSearch"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="max-h-96 overflow-y-auto space-y-2">
|
||||||
|
<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">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox 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>
|
||||||
|
</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
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
223
app/Views/v2/master/tests/param_dialog.php
Normal file
223
app/Views/v2/master/tests/param_dialog.php
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<!-- Parameter Test Form Modal -->
|
||||||
|
<div
|
||||||
|
x-show="showModal && currentDialogType === 'PARAM'"
|
||||||
|
x-cloak
|
||||||
|
class="modal-overlay"
|
||||||
|
@click.self="closeModal()"
|
||||||
|
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-6 max-w-3xl 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"
|
||||||
|
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="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-flask" style="color: rgb(var(--color-info));"></i>
|
||||||
|
<span x-text="isEditing ? 'Edit Parameter Test' : 'New Parameter Test'"></span>
|
||||||
|
</h3>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Test Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:class="errors.TestSiteName && 'input-error'"
|
||||||
|
x-model="form.TestSiteName"
|
||||||
|
placeholder="WBC Count"
|
||||||
|
/>
|
||||||
|
<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">Test Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input font-mono"
|
||||||
|
:class="errors.TestSiteCode && 'input-error'"
|
||||||
|
x-model="form.TestSiteCode"
|
||||||
|
placeholder="WBC"
|
||||||
|
/>
|
||||||
|
<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 & Method -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Test Type</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="input input-disabled bg-base-200" x-model="form.TestTypeName" readonly />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Method</span>
|
||||||
|
</label>
|
||||||
|
<select class="select" x-model="form.MethodID">
|
||||||
|
<option value="">Select Method</option>
|
||||||
|
<template x-for="m in methodsList" :key="m.MethodID">
|
||||||
|
<option :value="m.MethodID" x-text="m.MethodName"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Specimen & Container -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Specimen Type</span>
|
||||||
|
</label>
|
||||||
|
<select class="select" x-model="form.SpecimenTypeID">
|
||||||
|
<option value="">Select Specimen</option>
|
||||||
|
<template x-for="s in specimenTypesList" :key="s.SpecimenTypeID">
|
||||||
|
<option :value="s.SpecimenTypeID" x-text="s.SpecimenTypeName"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Container</span>
|
||||||
|
</label>
|
||||||
|
<select class="select" x-model="form.ContainerID">
|
||||||
|
<option value="">Select Container</option>
|
||||||
|
<template x-for="c in containersList" :key="c.ContainerID">
|
||||||
|
<option :value="c.ContainerID" x-text="c.ContainerName"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Volume & Unit -->
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Volume Required</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
class="input"
|
||||||
|
x-model="form.VolumeRequired"
|
||||||
|
placeholder="2.0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Volume Unit</span>
|
||||||
|
</label>
|
||||||
|
<select class="select" x-model="form.VolumeUnit">
|
||||||
|
<option value="mL">mL</option>
|
||||||
|
<option value="uL">µL</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Result Unit</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
x-model="form.ResultUnit"
|
||||||
|
placeholder="cells/µL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Description</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="input h-20 pt-2"
|
||||||
|
x-model="form.Description"
|
||||||
|
placeholder="Test description..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sequence & Visibility -->
|
||||||
|
<div class="grid grid-cols-2 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" />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Site</span>
|
||||||
|
</label>
|
||||||
|
<select class="select" 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>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</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-info 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 Test'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<!-- Lab Test Form Modal -->
|
<!-- Lab Test Form Modal -->
|
||||||
<div
|
<div
|
||||||
x-show="showModal"
|
x-show="showModal && currentDialogType === 'TEST'"
|
||||||
x-cloak
|
x-cloak
|
||||||
class="modal-overlay"
|
class="modal-overlay"
|
||||||
@click.self="closeModal()"
|
@click.self="closeModal()"
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<?= $this->section("content") ?>
|
<?= $this->section("content") ?>
|
||||||
<div x-data="labTests()" x-init="init()">
|
<div x-data="labTests()" x-init="init()">
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="card-glass p-6 animate-fadeIn mb-6">
|
<div class="card-glass p-6 animate-fadeIn mb-6">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
@ -84,13 +84,13 @@
|
|||||||
<span class="badge badge-ghost font-mono text-xs" x-text="test.TestSiteID || '-'"></span>
|
<span class="badge badge-ghost font-mono text-xs" x-text="test.TestSiteID || '-'"></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="test.TestSiteName || '-'"></div>
|
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="test.TestSiteName || '-'"</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-sm" x-text="test.TestSiteCode || '-'"></td>
|
<td class="font-mono text-sm" x-text="test.TestSiteCode || '-'"></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge badge-sm" :class="test.TypeCode == 'GROUP' ? 'badge-primary' : 'badge-ghost'" x-text="test.TypeName || '-'"></span>
|
<span class="badge badge-sm" :class="getTestTypeBadgeClass(test.TypeCode)" x-text="test.TypeName || '-'"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-sm font-mono" x-text="`${test.SeqScr || 0} / ${test.SeqRpt || 0}`"></td>
|
<td class="text-sm font-mono" x-text="test.SeqScr !== undefined ? test.SeqScr + ' / ' + test.SeqRpt : '-'"></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<div class="flex items-center justify-center gap-1">
|
<div class="flex items-center justify-center gap-1">
|
||||||
<button class="btn btn-ghost btn-sm btn-square" @click="editTest(test.TestSiteID)" title="Edit">
|
<button class="btn btn-ghost btn-sm btn-square" @click="editTest(test.TestSiteID)" title="Edit">
|
||||||
@ -108,8 +108,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Include Form Dialog -->
|
<!-- Include Form Dialogs (all types) -->
|
||||||
<?= $this->include('v2/master/lab_tests/test_dialog') ?>
|
<?= $this->include('v2/master/tests/test_dialog') ?>
|
||||||
|
<?= $this->include('v2/master/tests/param_dialog') ?>
|
||||||
|
<?= $this->include('v2/master/tests/group_dialog') ?>
|
||||||
|
<?= $this->include('v2/master/tests/calc_dialog') ?>
|
||||||
|
<?= $this->include('v2/master/tests/title_dialog') ?>
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
<div
|
<div
|
||||||
@ -149,10 +153,19 @@ function labTests() {
|
|||||||
list: [],
|
list: [],
|
||||||
sitesList: [],
|
sitesList: [],
|
||||||
typesList: [],
|
typesList: [],
|
||||||
|
disciplinesList: [],
|
||||||
|
departmentsList: [],
|
||||||
|
methodsList: [],
|
||||||
|
specimenTypesList: [],
|
||||||
|
workstationsList: [],
|
||||||
|
equipmentList: [],
|
||||||
|
containersList: [],
|
||||||
|
availableTests: [],
|
||||||
keyword: "",
|
keyword: "",
|
||||||
|
|
||||||
// Form Modal
|
// Form Modal
|
||||||
showModal: false,
|
showModal: false,
|
||||||
|
currentDialogType: 'TEST', // TEST, PARAM, GROUP, CALC, TITLE
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
errors: {},
|
errors: {},
|
||||||
@ -162,14 +175,50 @@ function labTests() {
|
|||||||
TestSiteCode: "",
|
TestSiteCode: "",
|
||||||
TestSiteName: "",
|
TestSiteName: "",
|
||||||
TestType: "",
|
TestType: "",
|
||||||
|
TestTypeName: "",
|
||||||
|
TypeCode: "TEST",
|
||||||
Description: "",
|
Description: "",
|
||||||
SeqScr: 0,
|
SeqScr: 0,
|
||||||
SeqRpt: 0,
|
SeqRpt: 0,
|
||||||
|
IndentLeft: 0,
|
||||||
|
FontStyle: "",
|
||||||
VisibleScr: 1,
|
VisibleScr: 1,
|
||||||
VisibleRpt: 1,
|
VisibleRpt: 1,
|
||||||
CountStat: 1
|
CountStat: 1,
|
||||||
|
StartDate: "",
|
||||||
|
// Technical fields (TEST, PARAM)
|
||||||
|
DisciplineID: "",
|
||||||
|
DepartmentID: "",
|
||||||
|
WorkstationID: "",
|
||||||
|
EquipmentID: "",
|
||||||
|
ResultType: "",
|
||||||
|
RefType: "",
|
||||||
|
VSet: "",
|
||||||
|
SpcType: "",
|
||||||
|
SpcDesc: "",
|
||||||
|
ReqQty: "",
|
||||||
|
ReqQtyUnit: "mL",
|
||||||
|
Unit1: "",
|
||||||
|
Factor: "",
|
||||||
|
Unit2: "",
|
||||||
|
Decimal: 2,
|
||||||
|
CollReq: "",
|
||||||
|
Method: "",
|
||||||
|
ExpectedTAT: "",
|
||||||
|
// GROUP fields
|
||||||
|
members: [],
|
||||||
|
// CALC fields
|
||||||
|
FormulaInput: "",
|
||||||
|
FormulaCode: "",
|
||||||
|
// Mapping fields
|
||||||
|
testmap: []
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Test selector for GROUP dialog
|
||||||
|
showTestSelector: false,
|
||||||
|
testSearch: "",
|
||||||
|
selectedTestIds: [],
|
||||||
|
|
||||||
// Delete Modal
|
// Delete Modal
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
deleteTarget: null,
|
deleteTarget: null,
|
||||||
@ -180,6 +229,69 @@ function labTests() {
|
|||||||
await this.fetchList();
|
await this.fetchList();
|
||||||
await this.fetchSites();
|
await this.fetchSites();
|
||||||
await this.fetchTypes();
|
await this.fetchTypes();
|
||||||
|
await this.fetchDisciplines();
|
||||||
|
await this.fetchDepartments();
|
||||||
|
await this.fetchMethods();
|
||||||
|
// Additional data can be loaded on demand when specific dialogs are opened
|
||||||
|
|
||||||
|
// Watch for typesList changes to ensure dropdown is populated
|
||||||
|
this.$watch('typesList', (newVal) => {
|
||||||
|
if (newVal.length > 0 && this.isEditing && this.form.TestType) {
|
||||||
|
const found = newVal.find(t => String(t.VID) === String(this.form.TestType));
|
||||||
|
if (!found) {
|
||||||
|
const byVid = newVal.find(t => t.VID == this.form.TestType);
|
||||||
|
if (byVid) {
|
||||||
|
this.form.TestType = byVid.VID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fetch disciplines
|
||||||
|
async fetchDisciplines() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASEURL}api/organization/discipline`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
this.disciplinesList = data.data || [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fetch departments
|
||||||
|
async fetchDepartments() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASEURL}api/organization/department`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
this.departmentsList = data.data || [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Fetch methods
|
||||||
|
async fetchMethods() {
|
||||||
|
try {
|
||||||
|
// Methods could be fetched from a valueset or endpoint
|
||||||
|
const res = await fetch(`${BASEURL}api/valueset/valuesetdef/28`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
this.methodsList = data.data || [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fetch lab test list
|
// Fetch lab test list
|
||||||
@ -192,12 +304,13 @@ function labTests() {
|
|||||||
const res = await fetch(`${BASEURL}api/tests?${params}`, {
|
const res = await fetch(`${BASEURL}api/tests?${params}`, {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error("HTTP error");
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
this.list = data.data || [];
|
||||||
this.list = data.data || [];
|
} else {
|
||||||
|
this.list = [];
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
|
||||||
this.list = [];
|
this.list = [];
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
@ -210,46 +323,106 @@ function labTests() {
|
|||||||
const res = await fetch(`${BASEURL}api/organization/site`, {
|
const res = await fetch(`${BASEURL}api/organization/site`, {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
if (res.ok) {
|
||||||
this.sitesList = data.data || [];
|
const data = await res.json();
|
||||||
|
this.sitesList = data.data || [];
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch sites:', err);
|
// Silently fail - will use default site
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fetch test types from valueset
|
// Fetch test types from valueset
|
||||||
async fetchTypes() {
|
async fetchTypes() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${BASEURL}api/valueset/TestType`, {
|
const res = await fetch(`${BASEURL}api/valueset/valuesetdef/27`, {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
if (res.ok) {
|
||||||
this.typesList = data.data || [];
|
const data = await res.json();
|
||||||
|
this.typesList = data.data || [];
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch test types:', err);
|
// Silently fail - types will use default mapping
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Show form for new test
|
// Show form for new test
|
||||||
showForm() {
|
showForm(typeCode = 'TEST') {
|
||||||
this.isEditing = false;
|
this.isEditing = false;
|
||||||
|
this.currentDialogType = typeCode;
|
||||||
this.form = {
|
this.form = {
|
||||||
TestSiteID: null,
|
TestSiteID: null,
|
||||||
SiteID: 1,
|
SiteID: 1,
|
||||||
TestSiteCode: "",
|
TestSiteCode: "",
|
||||||
TestSiteName: "",
|
TestSiteName: "",
|
||||||
TestType: "",
|
TestType: this.getTypeIdByCode(typeCode),
|
||||||
|
TestTypeName: this.getTypeNameByCode(typeCode),
|
||||||
|
TypeCode: typeCode,
|
||||||
Description: "",
|
Description: "",
|
||||||
SeqScr: 0,
|
SeqScr: 0,
|
||||||
SeqRpt: 0,
|
SeqRpt: 0,
|
||||||
|
IndentLeft: 0,
|
||||||
|
FontStyle: "",
|
||||||
VisibleScr: 1,
|
VisibleScr: 1,
|
||||||
VisibleRpt: 1,
|
VisibleRpt: 1,
|
||||||
CountStat: 1
|
CountStat: 1,
|
||||||
|
StartDate: new Date().toISOString().split('T')[0],
|
||||||
|
// Technical fields
|
||||||
|
DisciplineID: "",
|
||||||
|
DepartmentID: "",
|
||||||
|
WorkstationID: "",
|
||||||
|
EquipmentID: "",
|
||||||
|
ResultType: "",
|
||||||
|
RefType: "",
|
||||||
|
VSet: "",
|
||||||
|
SpcType: "",
|
||||||
|
SpcDesc: "",
|
||||||
|
ReqQty: "",
|
||||||
|
ReqQtyUnit: "mL",
|
||||||
|
Unit1: "",
|
||||||
|
Factor: "",
|
||||||
|
Unit2: "",
|
||||||
|
Decimal: 2,
|
||||||
|
CollReq: "",
|
||||||
|
Method: "",
|
||||||
|
ExpectedTAT: "",
|
||||||
|
// GROUP fields
|
||||||
|
members: [],
|
||||||
|
// CALC fields
|
||||||
|
FormulaInput: "",
|
||||||
|
FormulaCode: "",
|
||||||
|
// Mapping fields
|
||||||
|
testmap: []
|
||||||
};
|
};
|
||||||
this.errors = {};
|
this.errors = {};
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Get type ID by code
|
||||||
|
getTypeIdByCode(typeCode) {
|
||||||
|
const typeMap = {
|
||||||
|
'TEST': 100,
|
||||||
|
'PARAM': 101,
|
||||||
|
'GROUP': 102,
|
||||||
|
'CALC': 103,
|
||||||
|
'TITLE': 104
|
||||||
|
};
|
||||||
|
return typeMap[typeCode] || 100;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get type name by code
|
||||||
|
getTypeNameByCode(typeCode) {
|
||||||
|
const nameMap = {
|
||||||
|
'TEST': 'Test',
|
||||||
|
'PARAM': 'Parameter',
|
||||||
|
'GROUP': 'Group',
|
||||||
|
'CALC': 'Calculated',
|
||||||
|
'TITLE': 'Title'
|
||||||
|
};
|
||||||
|
return nameMap[typeCode] || 'Test';
|
||||||
|
},
|
||||||
|
|
||||||
// Edit test
|
// Edit test
|
||||||
async editTest(id) {
|
async editTest(id) {
|
||||||
this.isEditing = true;
|
this.isEditing = true;
|
||||||
@ -260,7 +433,113 @@ function labTests() {
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.data) {
|
if (data.data) {
|
||||||
this.form = { ...this.form, ...data.data };
|
const testData = data.data;
|
||||||
|
const testType = testData.TestType;
|
||||||
|
const typeCode = testData.TypeCode || this.getTypeCodeById(testType);
|
||||||
|
this.currentDialogType = typeCode;
|
||||||
|
|
||||||
|
// Merge with default form structure
|
||||||
|
this.form = {
|
||||||
|
TestSiteID: testData.TestSiteID,
|
||||||
|
SiteID: testData.SiteID || 1,
|
||||||
|
TestSiteCode: testData.TestSiteCode || "",
|
||||||
|
TestSiteName: testData.TestSiteName || "",
|
||||||
|
TestType: testType,
|
||||||
|
TestTypeName: testData.TypeName || this.getTypeNameByCode(typeCode),
|
||||||
|
TypeCode: typeCode,
|
||||||
|
Description: testData.Description || "",
|
||||||
|
SeqScr: testData.SeqScr || 0,
|
||||||
|
SeqRpt: testData.SeqRpt || 0,
|
||||||
|
IndentLeft: testData.IndentLeft || 0,
|
||||||
|
FontStyle: testData.FontStyle || "",
|
||||||
|
VisibleScr: testData.VisibleScr !== undefined ? testData.VisibleScr : 1,
|
||||||
|
VisibleRpt: testData.VisibleRpt !== undefined ? testData.VisibleRpt : 1,
|
||||||
|
CountStat: testData.CountStat !== undefined ? testData.CountStat : 1,
|
||||||
|
StartDate: testData.StartDate ? testData.StartDate.split('T')[0] : "",
|
||||||
|
// Technical fields
|
||||||
|
DisciplineID: "",
|
||||||
|
DepartmentID: "",
|
||||||
|
WorkstationID: "",
|
||||||
|
EquipmentID: "",
|
||||||
|
ResultType: "",
|
||||||
|
RefType: "",
|
||||||
|
VSet: "",
|
||||||
|
SpcType: "",
|
||||||
|
SpcDesc: "",
|
||||||
|
ReqQty: "",
|
||||||
|
ReqQtyUnit: "mL",
|
||||||
|
Unit1: "",
|
||||||
|
Factor: "",
|
||||||
|
Unit2: "",
|
||||||
|
Decimal: 2,
|
||||||
|
CollReq: "",
|
||||||
|
Method: "",
|
||||||
|
ExpectedTAT: "",
|
||||||
|
// GROUP fields
|
||||||
|
members: [],
|
||||||
|
// CALC fields
|
||||||
|
FormulaInput: "",
|
||||||
|
FormulaCode: "",
|
||||||
|
// Mapping fields
|
||||||
|
testmap: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load technical/calculation/group details based on type
|
||||||
|
if (typeCode === 'CALC' && testData.testdefcal && testData.testdefcal.length > 0) {
|
||||||
|
const calData = testData.testdefcal[0];
|
||||||
|
this.form.DisciplineID = calData.DisciplineID || "";
|
||||||
|
this.form.DepartmentID = calData.DepartmentID || "";
|
||||||
|
this.form.FormulaInput = calData.FormulaInput || "";
|
||||||
|
this.form.FormulaCode = calData.FormulaCode || "";
|
||||||
|
this.form.Unit1 = calData.Unit1 || "";
|
||||||
|
this.form.Unit2 = calData.Unit2 || "";
|
||||||
|
this.form.Decimal = calData.Decimal || 2;
|
||||||
|
this.form.Method = calData.Method || "";
|
||||||
|
} else if (typeCode === 'GROUP' && testData.testdefgrp) {
|
||||||
|
this.form.members = testData.testdefgrp.map(m => ({
|
||||||
|
TestSiteID: m.Member,
|
||||||
|
TestSiteCode: m.TestSiteCode || "",
|
||||||
|
TestSiteName: m.TestSiteName || "",
|
||||||
|
MemberTypeCode: m.MemberTypeCode || ""
|
||||||
|
}));
|
||||||
|
} else if (['TEST', 'PARAM'].includes(typeCode) && testData.testdeftech && testData.testdeftech.length > 0) {
|
||||||
|
const techData = testData.testdeftech[0];
|
||||||
|
this.form.DisciplineID = techData.DisciplineID || "";
|
||||||
|
this.form.DepartmentID = techData.DepartmentID || "";
|
||||||
|
this.form.WorkstationID = techData.WorkstationID || "";
|
||||||
|
this.form.EquipmentID = techData.EquipmentID || "";
|
||||||
|
this.form.ResultType = techData.ResultType || "";
|
||||||
|
this.form.RefType = techData.RefType || "";
|
||||||
|
this.form.VSet = techData.VSet || "";
|
||||||
|
this.form.SpcType = techData.SpcType || "";
|
||||||
|
this.form.SpcDesc = techData.SpcDesc || "";
|
||||||
|
this.form.ReqQty = techData.ReqQty || "";
|
||||||
|
this.form.ReqQtyUnit = techData.ReqQtyUnit || "mL";
|
||||||
|
this.form.Unit1 = techData.Unit1 || "";
|
||||||
|
this.form.Factor = techData.Factor || "";
|
||||||
|
this.form.Unit2 = techData.Unit2 || "";
|
||||||
|
this.form.Decimal = techData.Decimal || 2;
|
||||||
|
this.form.CollReq = techData.CollReq || "";
|
||||||
|
this.form.Method = techData.Method || "";
|
||||||
|
this.form.ExpectedTAT = techData.ExpectedTAT || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load test mappings
|
||||||
|
if (testData.testmap) {
|
||||||
|
this.form.testmap = testData.testmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.typesList.length > 0) {
|
||||||
|
const found = this.typesList.find(t => String(t.VID) === String(testType));
|
||||||
|
if (found) {
|
||||||
|
this.form.TestType = found.VID;
|
||||||
|
this.form.TestTypeName = found.VDesc;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.form.TestType = testType;
|
||||||
|
}
|
||||||
|
});
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -269,12 +548,41 @@ function labTests() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Get type code by ID
|
||||||
|
getTypeCodeById(typeId) {
|
||||||
|
const idMap = {
|
||||||
|
100: 'TEST',
|
||||||
|
101: 'PARAM',
|
||||||
|
102: 'GROUP',
|
||||||
|
103: 'CALC',
|
||||||
|
104: 'TITLE'
|
||||||
|
};
|
||||||
|
// Try to find in typesList
|
||||||
|
if (this.typesList.length > 0) {
|
||||||
|
const found = this.typesList.find(t => t.VID == typeId);
|
||||||
|
if (found) return found.VCode;
|
||||||
|
}
|
||||||
|
return idMap[typeId] || 'TEST';
|
||||||
|
},
|
||||||
|
|
||||||
// Validate form
|
// Validate form
|
||||||
validate() {
|
validate() {
|
||||||
const e = {};
|
const e = {};
|
||||||
if (!this.form.TestSiteName?.trim()) e.TestSiteName = "Test name is required";
|
if (!this.form.TestSiteName?.trim()) e.TestSiteName = "Test name is required";
|
||||||
if (!this.form.TestSiteCode?.trim()) e.TestSiteCode = "Test code is required";
|
if (!this.form.TestSiteCode?.trim()) e.TestSiteCode = "Test code is required";
|
||||||
if (!this.form.TestType) e.TestType = "Test type is required";
|
if (!this.form.TestType) e.TestType = "Test type is required";
|
||||||
|
if (!this.form.SiteID) e.SiteID = "Site is required";
|
||||||
|
|
||||||
|
// Validate formula for CALC type
|
||||||
|
if (this.form.TypeCode === 'CALC' && !this.form.FormulaCode?.trim()) {
|
||||||
|
e.FormulaCode = "Formula is required for calculated tests";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate members for GROUP type
|
||||||
|
if (this.form.TypeCode === 'GROUP' && (!this.form.members || this.form.members.length === 0)) {
|
||||||
|
e.members = "At least one member is required for group tests";
|
||||||
|
}
|
||||||
|
|
||||||
this.errors = e;
|
this.errors = e;
|
||||||
return Object.keys(e).length === 0;
|
return Object.keys(e).length === 0;
|
||||||
},
|
},
|
||||||
@ -291,11 +599,77 @@ function labTests() {
|
|||||||
|
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
try {
|
try {
|
||||||
const method = this.isEditing ? 'PATCH' : 'POST';
|
const method = this.isEditing ? 'PUT' : 'POST';
|
||||||
const res = await fetch(`${BASEURL}api/tests`, {
|
|
||||||
|
// Prepare payload based on test type
|
||||||
|
const payload = {
|
||||||
|
SiteID: this.form.SiteID,
|
||||||
|
TestSiteCode: this.form.TestSiteCode,
|
||||||
|
TestSiteName: this.form.TestSiteName,
|
||||||
|
TestType: this.form.TestType,
|
||||||
|
Description: this.form.Description,
|
||||||
|
SeqScr: this.form.SeqScr,
|
||||||
|
SeqRpt: this.form.SeqRpt,
|
||||||
|
IndentLeft: this.form.IndentLeft,
|
||||||
|
FontStyle: this.form.FontStyle,
|
||||||
|
VisibleScr: this.form.VisibleScr,
|
||||||
|
VisibleRpt: this.form.VisibleRpt,
|
||||||
|
CountStat: this.form.CountStat,
|
||||||
|
StartDate: this.form.StartDate
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add type-specific details
|
||||||
|
if (this.form.TypeCode === 'CALC') {
|
||||||
|
payload.details = {
|
||||||
|
DisciplineID: this.form.DisciplineID,
|
||||||
|
DepartmentID: this.form.DepartmentID,
|
||||||
|
FormulaInput: this.form.FormulaInput,
|
||||||
|
FormulaCode: this.form.FormulaCode,
|
||||||
|
RefType: this.form.RefType || 'NMRC',
|
||||||
|
Unit1: this.form.Unit1,
|
||||||
|
Factor: this.form.Factor,
|
||||||
|
Unit2: this.form.Unit2,
|
||||||
|
Decimal: this.form.Decimal,
|
||||||
|
Method: this.form.Method
|
||||||
|
};
|
||||||
|
} else if (this.form.TypeCode === 'GROUP') {
|
||||||
|
payload.details = {
|
||||||
|
members: this.form.members.map(m => ({
|
||||||
|
Member: m.TestSiteID
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
} else if (['TEST', 'PARAM'].includes(this.form.TypeCode)) {
|
||||||
|
payload.details = {
|
||||||
|
DisciplineID: this.form.DisciplineID,
|
||||||
|
DepartmentID: this.form.DepartmentID,
|
||||||
|
WorkstationID: this.form.WorkstationID,
|
||||||
|
EquipmentID: this.form.EquipmentID,
|
||||||
|
ResultType: this.form.ResultType,
|
||||||
|
RefType: this.form.RefType,
|
||||||
|
VSet: this.form.VSet,
|
||||||
|
SpcType: this.form.SpcType,
|
||||||
|
SpcDesc: this.form.SpcDesc,
|
||||||
|
ReqQty: this.form.ReqQty,
|
||||||
|
ReqQtyUnit: this.form.ReqQtyUnit,
|
||||||
|
Unit1: this.form.Unit1,
|
||||||
|
Factor: this.form.Factor,
|
||||||
|
Unit2: this.form.Unit2,
|
||||||
|
Decimal: this.form.Decimal,
|
||||||
|
CollReq: this.form.CollReq,
|
||||||
|
Method: this.form.Method,
|
||||||
|
ExpectedTAT: this.form.ExpectedTAT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add test mappings if present
|
||||||
|
if (this.form.testmap && this.form.testmap.length > 0) {
|
||||||
|
payload.testmap = this.form.testmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`${BASEURL}api/tests${this.isEditing ? '/' + this.form.TestSiteID : ''}`, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(this.form),
|
body: JSON.stringify(payload),
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -321,6 +695,18 @@ function labTests() {
|
|||||||
this.showDeleteModal = true;
|
this.showDeleteModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Get badge class for test type
|
||||||
|
getTestTypeBadgeClass(typeCode) {
|
||||||
|
const colorMap = {
|
||||||
|
'GROUP': 'badge-primary',
|
||||||
|
'CALC': 'badge-secondary',
|
||||||
|
'TEST': 'badge-accent',
|
||||||
|
'PARAM': 'badge-info',
|
||||||
|
'TITLE': 'badge-warning'
|
||||||
|
};
|
||||||
|
return colorMap[typeCode] || 'badge-ghost';
|
||||||
|
},
|
||||||
|
|
||||||
// Delete test
|
// Delete test
|
||||||
async deleteTest() {
|
async deleteTest() {
|
||||||
if (!this.deleteTarget) return;
|
if (!this.deleteTarget) return;
|
||||||
@ -347,6 +733,62 @@ function labTests() {
|
|||||||
this.deleting = false;
|
this.deleting = false;
|
||||||
this.deleteTarget = null;
|
this.deleteTarget = null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ========== GROUP Dialog Helper Functions ==========
|
||||||
|
|
||||||
|
// Open test selector for group members
|
||||||
|
async openTestSelector() {
|
||||||
|
// Use existing list of tests, filter for TEST and PARAM types only
|
||||||
|
this.availableTests = this.list.filter(t =>
|
||||||
|
t.TypeCode === 'TEST' || t.TypeCode === 'PARAM'
|
||||||
|
);
|
||||||
|
this.selectedTestIds = this.form.members?.map(m => m.TestSiteID) || [];
|
||||||
|
this.showTestSelector = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if test is selected
|
||||||
|
isTestSelected(testId) {
|
||||||
|
return this.selectedTestIds.includes(testId);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Toggle test selection
|
||||||
|
toggleTestSelection(test) {
|
||||||
|
const index = this.selectedTestIds.indexOf(test.TestSiteID);
|
||||||
|
if (index > -1) {
|
||||||
|
this.selectedTestIds.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.selectedTestIds.push(test.TestSiteID);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Confirm test selection and add to members
|
||||||
|
confirmTestSelection() {
|
||||||
|
const newMembers = this.availableTests.filter(t =>
|
||||||
|
this.selectedTestIds.includes(t.TestSiteID) &&
|
||||||
|
!this.form.members?.some(m => m.TestSiteID === t.TestSiteID)
|
||||||
|
);
|
||||||
|
|
||||||
|
newMembers.forEach(test => {
|
||||||
|
this.form.members.push({
|
||||||
|
TestSiteID: test.TestSiteID,
|
||||||
|
TestSiteCode: test.TestSiteCode,
|
||||||
|
TestSiteName: test.TestSiteName,
|
||||||
|
SeqScr: test.SeqScr || 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.showTestSelector = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove member from group
|
||||||
|
removeMember(index) {
|
||||||
|
this.form.members.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Helper function to check if value is in array
|
||||||
|
inArray(value, array) {
|
||||||
|
return array.includes(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
120
app/Views/v2/master/tests/title_dialog.php
Normal file
120
app/Views/v2/master/tests/title_dialog.php
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
<!-- Title Test Form Modal -->
|
||||||
|
<div
|
||||||
|
x-show="showModal && currentDialogType === 'TITLE'"
|
||||||
|
x-cloak
|
||||||
|
class="modal-overlay"
|
||||||
|
@click.self="closeModal()"
|
||||||
|
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-6 max-w-2xl w-full"
|
||||||
|
@click.stop
|
||||||
|
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="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-heading" style="color: rgb(var(--color-warning));"></i>
|
||||||
|
<span x-text="isEditing ? 'Edit Report Title' : 'New Report Title'"></span>
|
||||||
|
</h3>
|
||||||
|
<button class="btn btn-ghost btn-sm btn-square" @click="closeModal()">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text font-medium">Title Name <span style="color: rgb(var(--color-error));">*</span></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
:class="errors.TestSiteName && 'input-error'"
|
||||||
|
x-model="form.TestSiteName"
|
||||||
|
placeholder="Hematology Results"
|
||||||
|
/>
|
||||||
|
<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">Title Code <span style="color: rgb(var(--color-error));">*</span></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input font-mono"
|
||||||
|
:class="errors.TestSiteCode && 'input-error'"
|
||||||
|
x-model="form.TestSiteCode"
|
||||||
|
placeholder="HEMO"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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="Title description..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<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" />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</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-warning 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 Title'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
BIN
docs/prj_clinical laboratory quality management system_3a.docx
Normal file
BIN
docs/prj_clinical laboratory quality management system_3a.docx
Normal file
Binary file not shown.
@ -50,11 +50,11 @@
|
|||||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
|
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
|
||||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||||
|
|
||||||
/* Border Radius - Softened for modern aesthetic */
|
/* Border Radius - Less rounded for modern aesthetic */
|
||||||
--radius-sm: 0.625rem;
|
--radius-sm: 0.375rem;
|
||||||
--radius-md: 1rem;
|
--radius-md: 0.625rem;
|
||||||
--radius-lg: 1.5rem;
|
--radius-lg: 0.75rem;
|
||||||
--radius-xl: 2.5rem;
|
--radius-xl: 1rem;
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions */
|
||||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user