525 lines
19 KiB
PHP
Executable File
525 lines
19 KiB
PHP
Executable File
<?php
|
|
namespace App\Controllers\Test;
|
|
|
|
use App\Traits\PatchValidationTrait;
|
|
use App\Traits\ResponseTrait;
|
|
use App\Controllers\BaseController;
|
|
use App\Libraries\ValueSet;
|
|
use App\Models\Test\TestMapModel;
|
|
use App\Models\Test\TestMapDetailModel;
|
|
|
|
class TestMapController extends BaseController {
|
|
use ResponseTrait;
|
|
use PatchValidationTrait;
|
|
|
|
protected $db;
|
|
protected $rules;
|
|
protected $patchRules;
|
|
protected $model;
|
|
protected $modelDetail;
|
|
protected array $headerFields = ['HostType', 'HostID', 'ClientType', 'ClientID'];
|
|
protected array $detailFields = ['HostTestCode', 'HostTestName', 'ConDefID', 'ClientTestCode', 'ClientTestName'];
|
|
protected array $detailRules;
|
|
protected array $detailPatchRules;
|
|
|
|
public function __construct() {
|
|
$this->db = \Config\Database::connect();
|
|
$this->model = new TestMapModel;
|
|
$this->modelDetail = new TestMapDetailModel;
|
|
$this->rules = [
|
|
'HostID' => 'required|integer',
|
|
'ClientID' => 'required|integer',
|
|
];
|
|
$this->patchRules = [
|
|
'HostID' => 'permit_empty|integer',
|
|
'ClientID' => 'permit_empty|integer',
|
|
'HostType' => 'permit_empty|string',
|
|
'ClientType' => 'permit_empty|string',
|
|
];
|
|
$this->detailRules = [
|
|
'HostTestCode' => 'permit_empty|max_length[10]',
|
|
'HostTestName' => 'permit_empty|max_length[100]',
|
|
'ConDefID' => 'permit_empty|integer',
|
|
'ClientTestCode' => 'permit_empty|max_length[10]',
|
|
'ClientTestName' => 'permit_empty|max_length[100]',
|
|
];
|
|
$this->detailPatchRules = $this->detailRules;
|
|
}
|
|
|
|
public function index() {
|
|
$rows = $this->model->getUniqueGroupings();
|
|
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
|
|
|
|
$rows = ValueSet::transformLabels($rows, [
|
|
'HostType' => 'entity_type',
|
|
'ClientType' => 'entity_type',
|
|
]);
|
|
|
|
$rows = array_map([$this, 'sanitizeTopLevelPayload'], $rows);
|
|
|
|
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
|
}
|
|
|
|
public function show($id = null) {
|
|
$row = $this->model->where('TestMapID',$id)->where('EndDate', null)->first();
|
|
if (empty($row)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => null ], 200); }
|
|
|
|
$row = ValueSet::transformLabels([$row], [
|
|
'HostType' => 'entity_type',
|
|
'ClientType' => 'entity_type',
|
|
])[0];
|
|
|
|
$row = $this->sanitizeTopLevelPayload($row);
|
|
|
|
// Include testmapdetail records
|
|
$row['details'] = $this->modelDetail->getDetailsByTestMap($id);
|
|
|
|
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $row ], 200);
|
|
}
|
|
|
|
public function create() {
|
|
$input = $this->request->getJSON(true);
|
|
$detailsPayload = null;
|
|
if (array_key_exists('details', $input)) {
|
|
$detailsPayload = $this->resolveDetailOperations($input['details']);
|
|
if ($detailsPayload === null) { return; }
|
|
}
|
|
|
|
$headerInput = array_intersect_key($input, array_flip($this->headerFields));
|
|
if (!$this->validateData($headerInput, $this->rules)) {
|
|
log_message('error', 'TestMap create validation failed: ' . json_encode($this->validator->getErrors()));
|
|
return $this->failValidationErrors($this->validator->getErrors());
|
|
}
|
|
|
|
$this->db->transStart();
|
|
try {
|
|
$id = $this->model->insert($headerInput);
|
|
|
|
if ($detailsPayload !== null && !empty($detailsPayload['new'])) {
|
|
if (!$this->insertDetailRows($id, $detailsPayload['new'])) {
|
|
$this->db->transRollback();
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->db->transComplete();
|
|
if ($this->db->transStatus() === false) {
|
|
return $this->failServerError('Something went wrong while saving the test map.');
|
|
}
|
|
|
|
return $this->respondCreated([ 'status' => 'success', 'message' => "data created successfully", 'data' => $id ]);
|
|
} catch (\Exception $e) {
|
|
$this->db->transRollback();
|
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function update($TestMapID = null) {
|
|
$input = $this->requirePatchPayload($this->request->getJSON(true));
|
|
if ($input === null) {
|
|
return;
|
|
}
|
|
|
|
$detailsPayload = null;
|
|
if (array_key_exists('details', $input)) {
|
|
$detailsPayload = $this->resolveDetailOperations($input['details']);
|
|
if ($detailsPayload === null) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$id = $this->requirePatchId($TestMapID, 'TestMapID');
|
|
if ($id === null) {
|
|
return;
|
|
}
|
|
|
|
$existing = $this->model->where('TestMapID', $id)->where('EndDate', null)->first();
|
|
if (!$existing) {
|
|
return $this->respond([ 'status' => 'failed', 'message' => 'Test map not found', 'data' => [] ], 404);
|
|
}
|
|
|
|
if (isset($input['TestMapID']) && (string) $input['TestMapID'] !== (string) $id) {
|
|
return $this->failValidationErrors('TestMapID in URL does not match body.');
|
|
}
|
|
|
|
$validationInput = array_intersect_key($headerInput = array_intersect_key($input, array_flip($this->headerFields)), $this->patchRules);
|
|
if (!empty($validationInput) && !$this->validateData($validationInput, $this->patchRules)) {
|
|
return $this->failValidationErrors($this->validator->getErrors());
|
|
}
|
|
|
|
$input['TestMapID'] = $id;
|
|
$this->db->transStart();
|
|
try {
|
|
if (!empty($headerInput)) {
|
|
$this->model->update($id, $headerInput);
|
|
}
|
|
|
|
if ($detailsPayload !== null && !$this->applyDetailOperations($id, $detailsPayload)) {
|
|
$this->db->transRollback();
|
|
return;
|
|
}
|
|
|
|
$this->db->transComplete();
|
|
if ($this->db->transStatus() === false) {
|
|
return $this->failServerError('Something went wrong while updating the test map.');
|
|
}
|
|
|
|
return $this->respond([ 'status' => 'success', 'message' => 'data updated successfully', 'data' => $id ], 200);
|
|
} catch (\Exception $e) {
|
|
$this->db->transRollback();
|
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function delete() {
|
|
$input = $this->request->getJSON(true);
|
|
$id = $input["TestMapID"] ?? null;
|
|
if (!$id) { return $this->failValidationErrors('TestMapID is required.'); }
|
|
|
|
try {
|
|
$row = $this->model->where('TestMapID', $id)->where('EndDate', null)->first();
|
|
if (empty($row)) { return $this->respond([ 'status' => 'failed', 'message' => "Data not found or already deleted.", 'data' => null ], 404); }
|
|
|
|
$this->db->transStart();
|
|
$timestamp = date('Y-m-d H:i:s');
|
|
$this->model->update($id, ['EndDate' => $timestamp]);
|
|
|
|
$this->modelDetail->where('TestMapID', $id)
|
|
->where('EndDate', null)
|
|
->set('EndDate', $timestamp)
|
|
->update();
|
|
|
|
$this->db->transComplete();
|
|
|
|
if ($this->db->transStatus() === false) {
|
|
return $this->failServerError('Something went wrong while deleting the test map.');
|
|
}
|
|
|
|
return $this->respond([ 'status' => 'success', 'message' => "data deleted successfully", 'data' => $id ], 200);
|
|
} catch (\Exception $e) {
|
|
return $this->failServerError('Something went wrong: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
public function showByTestCode($testCode = null) {
|
|
if (!$testCode) { return $this->failValidationErrors('TestCode is required.'); }
|
|
|
|
$rows = $this->model->getMappingsByTestCode($testCode);
|
|
if (empty($rows)) { return $this->respond([ 'status' => 'success', 'message' => "no Data.", 'data' => [] ], 200); }
|
|
|
|
$rows = ValueSet::transformLabels($rows, [
|
|
'HostType' => 'entity_type',
|
|
'ClientType' => 'entity_type',
|
|
]);
|
|
|
|
$rows = array_map([$this, 'sanitizeTopLevelPayload'], $rows);
|
|
|
|
return $this->respond([ 'status' => 'success', 'message'=> "Data fetched successfully", 'data' => $rows ], 200);
|
|
}
|
|
|
|
private function sanitizeTopLevelPayload(array $row): array
|
|
{
|
|
unset($row['TestCode'], $row['testcode']);
|
|
return $row;
|
|
}
|
|
|
|
private function resolveDetailOperations(mixed $detailsPayload): ?array
|
|
{
|
|
if ($detailsPayload === null) {
|
|
return null;
|
|
}
|
|
|
|
if (!is_array($detailsPayload)) {
|
|
$this->failValidationErrors('details must be an array or object.');
|
|
return null;
|
|
}
|
|
|
|
if ($this->isDetailOpsPayload($detailsPayload)) {
|
|
$newItems = $this->normalizeDetailList($detailsPayload['new'] ?? []);
|
|
if ($newItems === null) { return null; }
|
|
$editItems = $this->normalizeDetailList($detailsPayload['edit'] ?? []);
|
|
if ($editItems === null) { return null; }
|
|
$deletedIds = $this->normalizeDetailIds($detailsPayload['deleted'] ?? []);
|
|
if ($deletedIds === null) { return null; }
|
|
|
|
return ['new' => $newItems, 'edit' => $editItems, 'deleted' => $deletedIds];
|
|
}
|
|
|
|
if ($this->isListPayload($detailsPayload)) {
|
|
$items = $this->normalizeDetailList($detailsPayload);
|
|
if ($items === null) { return null; }
|
|
return ['new' => $items, 'edit' => [], 'deleted' => []];
|
|
}
|
|
|
|
if ($this->isAssocArray($detailsPayload)) {
|
|
$items = $this->normalizeDetailList([$detailsPayload]);
|
|
if ($items === null) { return null; }
|
|
return ['new' => $items, 'edit' => [], 'deleted' => []];
|
|
}
|
|
|
|
$this->failValidationErrors('details must be an array of objects or contain new/edit/deleted arrays.');
|
|
return null;
|
|
}
|
|
|
|
private function applyDetailOperations(int $testMapID, array $operations): bool
|
|
{
|
|
if (!empty($operations['edit']) && !$this->updateDetails($testMapID, $operations['edit'])) {
|
|
return false;
|
|
}
|
|
|
|
if (!empty($operations['deleted']) && !$this->softDeleteDetails($testMapID, $operations['deleted'])) {
|
|
return false;
|
|
}
|
|
|
|
if (!empty($operations['new']) && !$this->insertDetailRows($testMapID, $operations['new'])) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function insertDetailRows(int $testMapID, array $items): bool
|
|
{
|
|
if (empty($items)) {
|
|
return true;
|
|
}
|
|
|
|
$prepared = [];
|
|
foreach ($items as $index => $item) {
|
|
if (!$this->validateData($item, $this->detailRules)) {
|
|
$this->failValidationErrors(['details.new' => $this->validator->getErrors()]);
|
|
return false;
|
|
}
|
|
$prepared[] = array_merge(['TestMapID' => $testMapID], $item);
|
|
}
|
|
|
|
$this->modelDetail->insertBatch($prepared);
|
|
return true;
|
|
}
|
|
|
|
private function updateDetails(int $testMapID, array $items): bool
|
|
{
|
|
foreach ($items as $index => $detail) {
|
|
$detailID = $detail['TestMapDetailID'] ?? null;
|
|
if (!$detailID || !ctype_digit((string) $detailID)) {
|
|
$this->failValidationErrors("details.edit[{$index}].TestMapDetailID is required and must be an integer.");
|
|
return false;
|
|
}
|
|
|
|
if (array_key_exists('TestMapID', $detail) && (int) $detail['TestMapID'] !== $testMapID) {
|
|
$this->failValidationErrors("details.edit[{$index}] must belong to TestMap {$testMapID}.");
|
|
return false;
|
|
}
|
|
|
|
$existing = $this->modelDetail->where('TestMapDetailID', $detailID)
|
|
->where('TestMapID', $testMapID)
|
|
->where('EndDate', null)
|
|
->first();
|
|
|
|
if (empty($existing)) {
|
|
$this->failValidationErrors("Detail record {$detailID} not found for this test map.");
|
|
return false;
|
|
}
|
|
|
|
$updateData = array_intersect_key($detail, array_flip($this->detailFields));
|
|
if ($updateData === []) {
|
|
continue;
|
|
}
|
|
|
|
if (!$this->validateData($updateData, $this->detailPatchRules)) {
|
|
$this->failValidationErrors($this->validator->getErrors());
|
|
return false;
|
|
}
|
|
|
|
$this->modelDetail->update($detailID, $updateData);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function softDeleteDetails(int $testMapID, array $ids): bool
|
|
{
|
|
if (empty($ids)) {
|
|
return true;
|
|
}
|
|
|
|
$existing = $this->modelDetail->select('TestMapDetailID')
|
|
->whereIn('TestMapDetailID', $ids)
|
|
->where('TestMapID', $testMapID)
|
|
->where('EndDate', null)
|
|
->findAll();
|
|
|
|
$foundIds = array_column($existing, 'TestMapDetailID');
|
|
$missing = array_diff($ids, $foundIds);
|
|
if (!empty($missing)) {
|
|
$this->failValidationErrors('Some detail IDs do not exist or belong to another test map: ' . implode(', ', $missing));
|
|
return false;
|
|
}
|
|
|
|
$this->modelDetail->whereIn('TestMapDetailID', $ids)
|
|
->where('TestMapID', $testMapID)
|
|
->where('EndDate', null)
|
|
->set('EndDate', date('Y-m-d H:i:s'))
|
|
->update();
|
|
|
|
return true;
|
|
}
|
|
|
|
private function isDetailOpsPayload(array $payload): bool
|
|
{
|
|
return (bool) array_intersect(array_keys($payload), ['new', 'edit', 'deleted']);
|
|
}
|
|
|
|
private function isListPayload(array $payload): bool
|
|
{
|
|
if ($payload === []) {
|
|
return true;
|
|
}
|
|
return array_keys($payload) === range(0, count($payload) - 1);
|
|
}
|
|
|
|
private function isAssocArray(array $payload): bool
|
|
{
|
|
if ($payload === []) {
|
|
return false;
|
|
}
|
|
return array_keys($payload) !== range(0, count($payload) - 1);
|
|
}
|
|
|
|
private function normalizeDetailList(mixed $value): ?array
|
|
{
|
|
if ($value === null) {
|
|
return [];
|
|
}
|
|
|
|
if (!is_array($value)) {
|
|
$this->failValidationErrors('Details must be provided as an array of objects.');
|
|
return null;
|
|
}
|
|
|
|
$results = [];
|
|
foreach ($value as $index => $item) {
|
|
if (!is_array($item)) {
|
|
$this->failValidationErrors("details[{$index}] must be an object.");
|
|
return null;
|
|
}
|
|
$results[] = $item;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
private function normalizeDetailIds(mixed $value): ?array
|
|
{
|
|
if ($value === null) {
|
|
return [];
|
|
}
|
|
|
|
if (!is_array($value)) {
|
|
$value = [$value];
|
|
}
|
|
|
|
$results = [];
|
|
foreach ($value as $index => $item) {
|
|
if (!ctype_digit((string) $item)) {
|
|
$this->failValidationErrors("details.deleted[{$index}] must be an integer.");
|
|
return null;
|
|
}
|
|
$results[] = (int) $item;
|
|
}
|
|
|
|
return array_values(array_unique($results));
|
|
}
|
|
|
|
public function batchCreate() {
|
|
$items = $this->request->getJSON(true);
|
|
if (!is_array($items)) { return $this->failValidationErrors('Expected array of items'); }
|
|
|
|
$results = ['success' => [], 'failed' => []];
|
|
$this->db->transStart();
|
|
|
|
foreach ($items as $index => $item) {
|
|
if (!$this->validateData($item, $this->rules)) {
|
|
$results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()];
|
|
continue;
|
|
}
|
|
try {
|
|
$id = $this->model->insert($item);
|
|
$results['success'][] = ['index' => $index, 'TestMapID' => $id];
|
|
} catch (\Exception $e) {
|
|
$results['failed'][] = ['index' => $index, 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
$this->db->transComplete();
|
|
return $this->respond([
|
|
'status' => empty($results['failed']) ? 'success' : 'partial',
|
|
'message' => 'Batch create completed',
|
|
'data' => $results
|
|
], 200);
|
|
}
|
|
|
|
public function batchUpdate() {
|
|
$items = $this->request->getJSON(true);
|
|
if (!is_array($items)) { return $this->failValidationErrors('Expected array of items'); }
|
|
|
|
$results = ['success' => [], 'failed' => []];
|
|
$this->db->transStart();
|
|
|
|
foreach ($items as $index => $item) {
|
|
$id = $item['TestMapID'] ?? null;
|
|
if (!$id) {
|
|
$results['failed'][] = ['index' => $index, 'error' => 'TestMapID required'];
|
|
continue;
|
|
}
|
|
if (!$this->validateData($item, $this->rules)) {
|
|
$results['failed'][] = ['index' => $index, 'errors' => $this->validator->getErrors()];
|
|
continue;
|
|
}
|
|
try {
|
|
$this->model->update($id, $item);
|
|
$results['success'][] = ['index' => $index, 'TestMapID' => $id];
|
|
} catch (\Exception $e) {
|
|
$results['failed'][] = ['index' => $index, 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
$this->db->transComplete();
|
|
return $this->respond([
|
|
'status' => empty($results['failed']) ? 'success' : 'partial',
|
|
'message' => 'Batch update completed',
|
|
'data' => $results
|
|
], 200);
|
|
}
|
|
|
|
public function batchDelete() {
|
|
$ids = $this->request->getJSON(true);
|
|
if (!is_array($ids)) { return $this->failValidationErrors('Expected array of TestMapIDs'); }
|
|
|
|
$results = ['success' => [], 'failed' => []];
|
|
$this->db->transStart();
|
|
|
|
foreach ($ids as $id) {
|
|
try {
|
|
$row = $this->model->where('TestMapID', $id)->where('EndDate', null)->first();
|
|
if (empty($row)) {
|
|
$results['failed'][] = ['TestMapID' => $id, 'error' => 'Not found or already deleted'];
|
|
continue;
|
|
}
|
|
$this->model->update($id, ['EndDate' => date('Y-m-d H:i:s')]);
|
|
$results['success'][] = $id;
|
|
} catch (\Exception $e) {
|
|
$results['failed'][] = ['TestMapID' => $id, 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
$this->db->transComplete();
|
|
return $this->respond([
|
|
'status' => empty($results['failed']) ? 'success' : 'partial',
|
|
'message' => 'Batch delete completed',
|
|
'data' => $results
|
|
], 200);
|
|
}
|
|
|
|
}
|