model = new \App\Models\Test\TestDefSiteModel; $this->modelCal = new \App\Models\Test\TestDefCalModel; $this->modelGrp = new \App\Models\Test\TestDefGrpModel; $this->modelMap = new \App\Models\Test\TestMapModel; $this->modelMapDetail = new \App\Models\Test\TestMapDetailModel; $this->modelRefNum = new \App\Models\RefRange\RefNumModel; $this->modelRefTxt = new \App\Models\RefRange\RefTxtModel; $this->rules = [ 'TestSiteCode' => 'required', 'TestSiteName' => 'required', 'TestType' => 'required', ]; } public function index() { $search = $this->request->getGet('search'); $filters = [ 'SiteID' => $this->request->getGet('SiteID'), 'TestType' => $this->request->getGet('TestType'), 'isVisibleScr' => $this->request->getGet('isVisibleScr'), 'isVisibleRpt' => $this->request->getGet('isVisibleRpt'), 'TestSiteName' => $this->request->getGet('TestSiteName'), 'TestSiteCode' => $this->request->getGet('TestSiteCode'), 'search' => $search, ]; $rows = $this->model->getTestsWithRelations($filters); if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => 'No data.', 'data' => [], ], 200); } $rows = ValueSet::transformLabels($rows, [ 'TestType' => 'test_type', ]); return $this->respond([ 'status' => 'success', 'message' => 'Data fetched successfully', 'data' => $rows, ], 200); } public function show($id = null) { if (!$id) { return $this->failValidationErrors('TestSiteID is required'); } $row = $this->model->getTestById($id); if (!$row) { return $this->respond([ 'status' => 'success', 'message' => 'No data.', 'data' => null, ], 200); } $typeCode = $row['TestType'] ?? ''; if ($typeCode === 'CALC') { $row['testdefcal'] = $this->modelCal->getByTestSiteID($id); $row['testdefgrp'] = [ 'members' => $this->modelGrp->getGroupMembers($id), ]; } elseif ($typeCode === 'GROUP') { $row['testdefgrp'] = [ 'members' => $this->modelGrp->getGroupMembers($id), ]; } elseif ($typeCode !== 'TITLE') { $refType = $row['RefType'] ?? ''; $resultType = $row['ResultType'] ?? ''; if (TestValidationService::usesRefNum($resultType, $refType)) { $row['refnum'] = $this->modelRefNum->getFormattedByTestSiteID($id); } if (TestValidationService::usesRefTxt($resultType, $refType)) { $row['reftxt'] = $this->modelRefTxt->getFormattedByTestSiteID($id); } } return $this->respond([ 'status' => 'success', 'message' => 'Data fetched successfully', 'data' => $row, ], 200); } public function create() { $input = $this->request->getJSON(true); if (!$this->validateData($input, $this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } $testType = $input['TestType'] ?? ''; $details = $input['details'] ?? $input; $resultType = $details['ResultType'] ?? ''; $refType = $details['RefType'] ?? ''; if (TestValidationService::isCalc($testType)) { $resultType = 'NMRIC'; $refType = $refType ?: 'RANGE'; } elseif (TestValidationService::isGroup($testType) || TestValidationService::isTitle($testType)) { $resultType = 'NORES'; $refType = 'NOREF'; } if ($resultType && $refType) { $validation = TestValidationService::validate($testType, $resultType, $refType); if (!$validation['valid']) { return $this->failValidationErrors(['type_validation' => $validation['error']]); } } $db = \Config\Database::connect(); $db->transStart(); try { $testSiteData = [ 'SiteID' => array_key_exists('SiteID', $input) ? $input['SiteID'] : null, 'TestSiteCode'=> $input['TestSiteCode'], 'TestSiteName'=> $input['TestSiteName'], 'TestType' => $input['TestType'], 'Description' => $input['Description'] ?? null, 'SeqScr' => array_key_exists('SeqScr', $input) ? $input['SeqScr'] : null, 'SeqRpt' => array_key_exists('SeqRpt', $input) ? $input['SeqRpt'] : null, 'IndentLeft' => $input['IndentLeft'] ?? 0, 'FontStyle' => $input['FontStyle'] ?? null, 'isVisibleScr' => $input['isVisibleScr'] ?? 1, 'isVisibleRpt' => $input['isVisibleRpt'] ?? 1, 'isCountStat' => $input['isCountStat'] ?? 1, 'isRequestable' => $input['isRequestable'] ?? 1, 'StartDate' => $input['StartDate'] ?? date('Y-m-d H:i:s'), ]; $id = $this->model->insert($testSiteData); if (!$id) { $dbError = $db->error(); log_message('error', 'Test insert failed: ' . json_encode($dbError, JSON_UNESCAPED_SLASHES)); $message = $dbError['message'] ?? 'Failed to insert main test definition'; throw new \Exception('Failed to insert main test definition: ' . $message); } $this->handleDetails($id, $input, 'insert'); $db->transComplete(); if ($db->transStatus() === false) { $dbError = $db->error(); $lastQuery = $db->showLastQuery(); log_message('error', 'TestController transaction failed: ' . json_encode([ 'error' => $dbError, 'last_query' => $lastQuery, ], JSON_UNESCAPED_SLASHES)); return $this->failServerError('Transaction failed'); } return $this->respondCreated([ 'status' => 'created', 'message' => 'Test created successfully', 'data' => ['TestSiteId' => $id], ]); } catch (\Exception $e) { $db->transRollback(); return $this->failServerError('Something went wrong: ' . $e->getMessage()); } } public function update($id = null) { $input = $this->request->getJSON(true); if (!$id && isset($input['TestSiteID'])) { $id = $input['TestSiteID']; } if (!$id) { return $this->failValidationErrors('TestSiteID is required.'); } $existing = $this->model->find($id); if (!$existing) { return $this->failNotFound('Test not found'); } $testType = $input['TestType'] ?? $existing['TestType'] ?? ''; $details = $input['details'] ?? $input; $resultType = $details['ResultType'] ?? $existing['ResultType'] ?? ''; $refType = $details['RefType'] ?? $existing['RefType'] ?? ''; if (TestValidationService::isCalc($testType)) { $resultType = 'NMRIC'; $refType = $refType ?: 'RANGE'; } elseif (TestValidationService::isGroup($testType) || TestValidationService::isTitle($testType)) { $resultType = 'NORES'; $refType = 'NOREF'; } if ($resultType && $refType) { $validation = TestValidationService::validate($testType, $resultType, $refType); if (!$validation['valid']) { return $this->failValidationErrors(['type_validation' => $validation['error']]); } } $db = \Config\Database::connect(); $db->transStart(); try { $testSiteData = []; $allowedUpdateFields = [ 'TestSiteCode', 'TestSiteName', 'TestType', 'Description', 'SeqScr', 'SeqRpt', 'IndentLeft', 'FontStyle', 'isVisibleScr', 'isVisibleRpt', 'isCountStat', 'isRequestable', 'StartDate', ]; foreach ($allowedUpdateFields as $field) { if (isset($input[$field])) { $testSiteData[$field] = $input[$field]; } } if (!empty($testSiteData)) { $this->model->update($id, $testSiteData); } $this->handleDetails($id, $input, 'update'); $db->transComplete(); if ($db->transStatus() === false) { return $this->failServerError('Transaction failed'); } return $this->respond([ 'status' => 'success', 'message' => 'Test updated successfully', 'data' => ['TestSiteId' => $id], ]); } catch (\Exception $e) { $db->transRollback(); return $this->failServerError('Something went wrong: ' . $e->getMessage()); } } public function delete($id = null) { $input = $this->request->getJSON(true); if (!$id && isset($input['TestSiteID'])) { $id = $input['TestSiteID']; } if (!$id) { return $this->failValidationErrors('TestSiteID is required.'); } $existing = $this->model->find($id); if (!$existing) { return $this->failNotFound('Test not found'); } if (!empty($existing['EndDate'])) { return $this->failValidationErrors('Test is already disabled'); } $db = \Config\Database::connect(); $db->transStart(); try { $now = date('Y-m-d H:i:s'); $this->model->update($id, ['EndDate' => $now]); $testType = $existing['TestType']; $typeCode = $testType; if (TestValidationService::isCalc($typeCode)) { $this->modelCal->disableByTestSiteID($id); $this->modelGrp->disableByTestSiteID($id); } elseif (TestValidationService::isGroup($typeCode)) { $this->modelGrp->disableByTestSiteID($id); } elseif (TestValidationService::isTechnicalTest($typeCode)) { $this->modelRefNum->disableByTestSiteID($id); $this->modelRefTxt->disableByTestSiteID($id); } // Disable testmap by test code $testSiteCode = $existing['TestSiteCode'] ?? null; if ($testSiteCode) { $existingMaps = $this->modelMap->getMappingsByTestCode($testSiteCode); foreach ($existingMaps as $existingMap) { $this->modelMapDetail->disableByTestMapID($existingMap['TestMapID']); $this->modelMap->update($existingMap['TestMapID'], ['EndDate' => $now]); } } $db->transComplete(); if ($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) { $db->transRollback(); return $this->failServerError('Something went wrong: ' . $e->getMessage()); } } private function handleDetails($testSiteID, $input, $action) { $testTypeID = $input['TestType'] ?? null; $testSiteCode = null; if (!$testTypeID && $action === 'update') { $existing = $this->model->find($testSiteID); $testTypeID = $existing['TestType'] ?? null; $testSiteCode = $existing['TestSiteCode'] ?? null; } if (!$testTypeID) { return; } $typeCode = $testTypeID; $details = $input['details'] ?? $input; $details['TestSiteID'] = $testSiteID; $details['SiteID'] = array_key_exists('SiteID', $input) ? $input['SiteID'] : null; switch ($typeCode) { case 'CALC': $this->saveCalcDetails($testSiteID, $details, $input, $action); break; case 'GROUP': $this->saveGroupDetails($testSiteID, $details, $input, $action); break; case 'TITLE': if (isset($input['testmap']) && is_array($input['testmap'])) { $this->saveTestMap($testSiteID, $testSiteCode, $input['testmap'], $action); } break; case 'TEST': case 'PARAM': default: $this->saveTechDetails($testSiteID, $details, $action, $typeCode); if (in_array($typeCode, ['TEST', 'PARAM']) && isset($details['RefType'])) { $refType = (string) $details['RefType']; $resultType = $details['ResultType'] ?? ''; if (TestValidationService::usesRefNum($resultType, $refType) && isset($input['refnum']) && is_array($input['refnum'])) { $this->saveRefNumRanges($testSiteID, $input['refnum'], $action, array_key_exists('SiteID', $input) ? $input['SiteID'] : null); } if (TestValidationService::usesRefTxt($resultType, $refType) && isset($input['reftxt']) && is_array($input['reftxt'])) { $this->saveRefTxtRanges($testSiteID, $input['reftxt'], $action, array_key_exists('SiteID', $input) ? $input['SiteID'] : null); } } break; } if ((TestValidationService::isTechnicalTest($typeCode) || TestValidationService::isCalc($typeCode)) && isset($input['testmap']) && is_array($input['testmap'])) { $this->saveTestMap($testSiteID, $testSiteCode, $input['testmap'], $action); } } private function saveTechDetails($testSiteID, $data, $action, $typeCode) { $techData = [ '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' => array_key_exists('Decimal', $data) ? $data['Decimal'] : null, 'CollReq' => $data['CollReq'] ?? null, 'Method' => $data['Method'] ?? null, 'ExpectedTAT' => $data['ExpectedTAT'] ?? null, ]; $this->model->update($testSiteID, $techData); } private function saveRefNumRanges($testSiteID, $ranges, $action, $siteID) { if ($action === 'update') { $this->modelRefNum->disableByTestSiteID($testSiteID); } $this->modelRefNum->batchInsert($testSiteID, $siteID, $ranges); } private function saveRefTxtRanges($testSiteID, $ranges, $action, $siteID) { if ($action === 'update') { $this->modelRefTxt->disableByTestSiteID($testSiteID); } $this->modelRefTxt->batchInsert($testSiteID, $siteID, $ranges); } private function saveCalcDetails($testSiteID, $data, $input, $action) { $calcData = [ 'TestSiteID' => $testSiteID, 'DisciplineID' => $data['DisciplineID'] ?? null, 'DepartmentID' => $data['DepartmentID'] ?? null, 'FormulaCode' => $data['FormulaCode'] ?? $data['Formula'] ?? null, 'ResultType' => 'NMRIC', 'RefType' => $data['RefType'] ?? 'RANGE', 'Unit1' => $data['Unit1'] ?? $data['ResultUnit'] ?? null, 'Factor' => $data['Factor'] ?? null, 'Unit2' => $data['Unit2'] ?? null, 'Decimal' => array_key_exists('Decimal', $data) ? $data['Decimal'] : null, 'Method' => $data['Method'] ?? null, ]; if ($action === 'update') { $exists = $this->modelCal->existsByTestSiteID($testSiteID); if ($exists) { $this->modelCal->update($exists['TestCalID'], $calcData); } else { $this->modelCal->insert($calcData); } } else { $this->modelCal->insert($calcData); } if ($action === 'update') { $this->modelGrp->disableByTestSiteID($testSiteID); } $memberIDs = $this->resolveMemberIDs($input); // Validate member IDs before insertion $validation = $this->validateMemberIDs($memberIDs); if (!$validation['valid']) { throw new \Exception('Invalid member TestSiteID(s): ' . implode(', ', $validation['invalid']) . '. Make sure to use TestSiteID, not SeqScr or other values.'); } foreach ($memberIDs as $memberID) { $this->modelGrp->insert([ 'TestSiteID' => $testSiteID, 'Member' => $memberID, ]); } } private function resolveMemberIDs(array $input): array { $memberIDs = []; $rawMembers = $input['testdefgrp']['members'] ?? []; if (is_array($rawMembers)) { foreach ($rawMembers as $member) { if (is_array($member)) { $rawID = $member['TestSiteID'] ?? null; } else { $rawID = is_numeric($member) ? $member : null; } if ($rawID !== null && is_numeric($rawID)) { $memberIDs[] = (int) $rawID; } } } $memberIDs = array_values(array_unique(array_filter($memberIDs))); return $memberIDs; } /** * Validate that member IDs exist in testdefsite table * * @param array $memberIDs Array of TestSiteID values to validate * @return array ['valid' => bool, 'invalid' => array] */ private function validateMemberIDs(array $memberIDs): array { if (empty($memberIDs)) { return ['valid' => true, 'invalid' => []]; } $existing = $this->model->whereIn('TestSiteID', $memberIDs) ->where('EndDate IS NULL') ->findAll(); $existingIDs = array_column($existing, 'TestSiteID'); $invalidIDs = array_diff($memberIDs, $existingIDs); return [ 'valid' => empty($invalidIDs), 'invalid' => array_values($invalidIDs) ]; } private function saveGroupDetails($testSiteID, $data, $input, $action) { if ($action === 'update') { $this->modelGrp->disableByTestSiteID($testSiteID); } $memberIDs = $this->resolveMemberIDs($input); // Validate member IDs before insertion $validation = $this->validateMemberIDs($memberIDs); if (!$validation['valid']) { throw new \Exception('Invalid member TestSiteID(s): ' . implode(', ', $validation['invalid']) . '. Make sure to use TestSiteID, not SeqScr or other values.'); } foreach ($memberIDs as $memberID) { $this->modelGrp->insert([ 'TestSiteID' => $testSiteID, 'Member' => $memberID, ]); } } private function saveTestMap($testSiteID, $testSiteCode, $mappings, $action) { if ($action === 'update' && $testSiteCode) { // Find existing mappings by test code through testmapdetail $existingMaps = $this->modelMap->getMappingsByTestCode($testSiteCode); foreach ($existingMaps as $existingMap) { $this->modelMapDetail->disableByTestMapID($existingMap['TestMapID']); } // Soft delete the testmap headers foreach ($existingMaps as $existingMap) { $this->modelMap->update($existingMap['TestMapID'], ['EndDate' => date('Y-m-d H:i:s')]); } } foreach ($this->normalizeTestMapPayload($mappings) as $map) { $mapData = [ 'HostType' => $map['HostType'] ?? null, 'HostID' => $map['HostID'] ?? null, 'ClientType' => $map['ClientType'] ?? null, 'ClientID' => $map['ClientID'] ?? null, ]; $testMapID = $this->modelMap->insert($mapData); if (!$testMapID) { continue; } foreach ($this->extractTestMapDetails($map) as $detail) { $detailData = [ 'TestMapID' => $testMapID, 'HostTestCode' => $detail['HostTestCode'] ?? null, 'HostTestName' => $detail['HostTestName'] ?? null, 'ConDefID' => $detail['ConDefID'] ?? null, 'ClientTestCode' => $detail['ClientTestCode'] ?? null, 'ClientTestName' => $detail['ClientTestName'] ?? null, ]; $this->modelMapDetail->insert($detailData); } } } private function normalizeTestMapPayload($mappings): array { if (!is_array($mappings)) { return []; } if ($this->isAssoc($mappings)) { return [$mappings]; } return array_values(array_filter($mappings, static fn ($map) => is_array($map))); } private function extractTestMapDetails(array $map): array { if (isset($map['details']) && is_array($map['details'])) { return array_values(array_filter($map['details'], static fn ($detail) => is_array($detail))); } $flatDetail = [ 'HostTestCode' => $map['HostTestCode'] ?? null, 'HostTestName' => $map['HostTestName'] ?? null, 'ConDefID' => $map['ConDefID'] ?? null, 'ClientTestCode' => $map['ClientTestCode'] ?? null, 'ClientTestName' => $map['ClientTestName'] ?? null, ]; foreach ($flatDetail as $value) { if ($value !== null && $value !== '') { return [$flatDetail]; } } return []; } private function isAssoc(array $array): bool { if ($array === []) { return false; } return array_keys($array) !== range(0, count($array) - 1); } }