- Refactor Tests.php controller with updated logic and error handling - Update Test migration with schema improvements - Enhance TestDefCalModel, TestDefGrpModel, TestDefTechModel with CRUD operations - Improve TestMapModel with better test mapping relationships - Redesign test dialog views (calc, group, param) with improved UX - Update tests_index view with better data presentation - Add CSS styles for test management UI components - Add TestDefSiteTest feature test for site-based test definitions - Add TestDefModelsTest unit test for model validation - Remove obsolete Test Management.docx documentation
524 lines
18 KiB
PHP
524 lines
18 KiB
PHP
<?php
|
|
namespace App\Controllers;
|
|
|
|
use CodeIgniter\API\ResponseTrait;
|
|
use App\Controllers\BaseController;
|
|
|
|
class Tests extends BaseController {
|
|
use ResponseTrait;
|
|
|
|
protected $db;
|
|
protected $rules;
|
|
protected $model;
|
|
protected $modelCal;
|
|
protected $modelTech;
|
|
protected $modelGrp;
|
|
protected $modelMap;
|
|
protected $modelValueSet;
|
|
|
|
public function __construct() {
|
|
$this->db = \Config\Database::connect();
|
|
$this->model = new \App\Models\Test\TestDefSiteModel;
|
|
$this->modelCal = new \App\Models\Test\TestDefCalModel;
|
|
$this->modelTech = new \App\Models\Test\TestDefTechModel;
|
|
$this->modelGrp = new \App\Models\Test\TestDefGrpModel;
|
|
$this->modelMap = new \App\Models\Test\TestMapModel;
|
|
$this->modelValueSet = new \App\Models\ValueSet\ValueSetModel;
|
|
|
|
// Validation rules for main test definition
|
|
$this->rules = [
|
|
'TestSiteCode' => 'required|min_length[3]|max_length[6]',
|
|
'TestSiteName' => 'required',
|
|
'TestType' => 'required',
|
|
'SiteID' => 'required'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* GET /v1/tests
|
|
* GET /v1/tests/site
|
|
* List all tests with optional filtering
|
|
*/
|
|
public function index() {
|
|
$siteId = $this->request->getGet('SiteID');
|
|
$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);
|
|
}
|
|
|
|
/**
|
|
* GET /v1/tests/{id}
|
|
* GET /v1/tests/site/{id}
|
|
* Get single test by ID with all related details
|
|
*/
|
|
public function show($id = null) {
|
|
if (!$id) return $this->failValidationErrors('TestSiteID is required');
|
|
|
|
$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')
|
|
->join('discipline d', 'd.DisciplineID=testdeftech.DisciplineID', 'left')
|
|
->join('department dept', 'dept.DepartmentID=testdeftech.DepartmentID', '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);
|
|
}
|
|
|
|
/**
|
|
* POST /v1/tests
|
|
* POST /v1/tests/site
|
|
* Create new test definition
|
|
*/
|
|
public function create() {
|
|
$input = $this->request->getJSON(true);
|
|
|
|
if (!$this->validateData($input, $this->rules)) {
|
|
return $this->failValidationErrors($this->validator->getErrors());
|
|
}
|
|
|
|
$this->db->transStart();
|
|
|
|
try {
|
|
// 1. Insert into Main Table (testdefsite)
|
|
$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) {
|
|
throw new \Exception("Failed to insert main test definition");
|
|
}
|
|
|
|
// 2. Handle Details based on TestType
|
|
$this->handleDetails($id, $input, 'insert');
|
|
|
|
$this->db->transComplete();
|
|
|
|
if ($this->db->transStatus() === false) {
|
|
return $this->failServerError('Transaction failed');
|
|
}
|
|
|
|
return $this->respondCreated([
|
|
'status' => 'created',
|
|
'message' => "Test created successfully",
|
|
'data' => ['TestSiteId' => $id]
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$this->db->transRollback();
|
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PUT/PATCH /v1/tests/{id}
|
|
* PUT/PATCH /v1/tests/site/{id}
|
|
* Update existing test definition
|
|
*/
|
|
public function update($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');
|
|
}
|
|
|
|
$this->db->transStart();
|
|
|
|
try {
|
|
// 1. Update Main Table
|
|
$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
|
|
$this->handleDetails($id, $input, 'update');
|
|
|
|
$this->db->transComplete();
|
|
|
|
if ($this->db->transStatus() === false) {
|
|
return $this->failServerError('Transaction failed');
|
|
}
|
|
|
|
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 /v1/tests/{id}
|
|
* DELETE /v1/tests/site/{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) {
|
|
$this->db->transRollback();
|
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to handle inserting/updating sub-tables based on TestType
|
|
*/
|
|
private function handleDetails($testSiteID, $input, $action) {
|
|
$testTypeID = $input['TestType'] ?? null;
|
|
|
|
// If update and TestType not in payload, fetch from DB
|
|
if (!$testTypeID && $action === 'update') {
|
|
$existing = $this->model->find($testSiteID);
|
|
$testTypeID = $existing['TestType'] ?? null;
|
|
}
|
|
|
|
if (!$testTypeID) return;
|
|
|
|
// Get Type Code (TEST, PARAM, CALC, GROUP, TITLE)
|
|
$vs = $this->modelValueSet->find($testTypeID);
|
|
$typeCode = $vs['VValue'] ?? '';
|
|
|
|
// Get details data from input
|
|
$details = $input['details'] ?? $input;
|
|
$details['TestSiteID'] = $testSiteID;
|
|
$details['SiteID'] = $input['SiteID'] ?? 1;
|
|
|
|
switch ($typeCode) {
|
|
case 'CALC':
|
|
$this->saveCalcDetails($testSiteID, $details, $action);
|
|
break;
|
|
|
|
case 'GROUP':
|
|
$this->saveGroupDetails($testSiteID, $details, $input, $action);
|
|
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 'PARAM':
|
|
default:
|
|
$this->saveTechDetails($testSiteID, $details, $action, $typeCode);
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save technical details for TEST and PARAM types
|
|
*/
|
|
private function saveTechDetails($testSiteID, $data, $action, $typeCode) {
|
|
$techData = [
|
|
'TestSiteID' => $testSiteID,
|
|
'DisciplineID' => $data['DisciplineID'] ?? null,
|
|
'DepartmentID' => $data['DepartmentID'] ?? null,
|
|
'ResultType' => $data['ResultType'] ?? null,
|
|
'RefType' => $data['RefType'] ?? null,
|
|
'VSet' => $data['VSet'] ?? 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') {
|
|
$exists = $this->db->table('testdeftech')
|
|
->where('TestSiteID', $testSiteID)
|
|
->where('EndDate IS NULL')
|
|
->get()->getRowArray();
|
|
|
|
if ($exists) {
|
|
$this->modelTech->update($exists['TestTechID'], $techData);
|
|
} else {
|
|
$this->modelTech->insert($techData);
|
|
}
|
|
} else {
|
|
$this->modelTech->insert($techData);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save calculation details for CALC type
|
|
*/
|
|
private function saveCalcDetails($testSiteID, $data, $action) {
|
|
$calcData = [
|
|
'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([
|
|
'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);
|
|
}
|
|
}
|
|
}
|
|
}
|