diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e81430f..b608135 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -7,6 +7,9 @@ use CodeIgniter\Router\RouteCollection; */ $routes->get('/', 'PageController::dashboard'); +$routes->get('/master/dept', 'PageController::masterDept'); +$routes->get('/master/test', 'PageController::masterTest'); +$routes->get('/master/control', 'PageController::masterControl'); $routes->get('/dept', 'PageController::dept'); $routes->get('/test', 'PageController::test'); $routes->get('/control', 'PageController::control'); @@ -42,3 +45,43 @@ $routes->group('api', function ($routes) { $routes->post('entry/monthly', 'Api\EntryApiController::saveMonthly'); $routes->post('entry/comment', 'Api\EntryApiController::saveComment'); }); + + $routes->group('api/master', function ($routes) { + $routes->get('depts', 'Master\MasterDeptsController::index'); + $routes->get('depts/(:num)', 'Master\MasterDeptsController::show/$1'); + $routes->post('depts', 'Master\MasterDeptsController::create'); + $routes->patch('depts/(:num)', 'Master\MasterDeptsController::update/$1'); + $routes->delete('depts/(:num)', 'Master\MasterDeptsController::delete/$1'); + + $routes->get('controls', 'Master\MasterControlsController::index'); + $routes->get('controls/(:num)', 'Master\MasterControlsController::show/$1'); + $routes->post('controls', 'Master\MasterControlsController::create'); + $routes->patch('controls/(:num)', 'Master\MasterControlsController::update/$1'); + $routes->delete('controls/(:num)', 'Master\MasterControlsController::delete/$1'); + + $routes->get('tests', 'Master\MasterTestsController::index'); + $routes->get('tests/(:num)', 'Master\MasterTestsController::show/$1'); + $routes->post('tests', 'Master\MasterTestsController::create'); + $routes->patch('tests/(:num)', 'Master\MasterTestsController::update/$1'); + $routes->delete('tests/(:num)', 'Master\MasterTestsController::delete/$1'); +}); + +$routes->group('api/qc', function ($routes) { + $routes->get('control-tests', 'Qc\ControlTestsController::index'); + $routes->get('control-tests/(:num)', 'Qc\ControlTestsController::show/$1'); + $routes->post('control-tests', 'Qc\ControlTestsController::create'); + $routes->patch('control-tests/(:num)', 'Qc\ControlTestsController::update/$1'); + $routes->delete('control-tests/(:num)', 'Qc\ControlTestsController::delete/$1'); + + $routes->get('results', 'Qc\ResultsController::index'); + $routes->get('results/(:num)', 'Qc\ResultsController::show/$1'); + $routes->post('results', 'Qc\ResultsController::create'); + $routes->patch('results/(:num)', 'Qc\ResultsController::update/$1'); + $routes->delete('results/(:num)', 'Qc\ResultsController::delete/$1'); + + $routes->get('result-comments', 'Qc\ResultCommentsController::index'); + $routes->get('result-comments/(:num)', 'Qc\ResultCommentsController::show/$1'); + $routes->post('result-comments', 'Qc\ResultCommentsController::create'); + $routes->patch('result-comments/(:num)', 'Qc\ResultCommentsController::update/$1'); + $routes->delete('result-comments/(:num)', 'Qc\ResultCommentsController::delete/$1'); +}); diff --git a/app/Controllers/Api/ControlApiController.php b/app/Controllers/Api/ControlApiController.php deleted file mode 100644 index e375076..0000000 --- a/app/Controllers/Api/ControlApiController.php +++ /dev/null @@ -1,148 +0,0 @@ -dictControlModel = new DictControlModel(); - $this->controlTestModel = new ControlTestModel(); - $this->rules = [ - 'name' => 'required|min_length[1]', - 'dept_ref_id' => 'required', - ]; - } - - public function index() - { - $keyword = $this->request->getGet('keyword'); - $deptId = $this->request->getGet('deptId'); - try { - $rows = $this->dictControlModel->getWithDept($keyword, $deptId); - return $this->respond([ - 'status' => 'success', - 'message' => 'fetch success', - 'data' => $rows - ], 200); - } catch (\Exception $e) { - return $this->failServerError('Exception: ' . $e->getMessage()); - } - } - - public function show($id = null) - { - try { - $rows = $this->dictControlModel->where('control_id', $id)->findAll(); - if (empty($rows)) { - return $this->respond([ - 'status' => 'success', - 'message' => 'data not found.' - ], 200); - } - return $this->respond([ - 'status' => 'success', - 'message' => 'fetch success', - 'data' => $rows - ], 200); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function getTests($id = null) - { - try { - $rows = $this->controlTestModel->where('control_ref_id', $id)->findAll(); - return $this->respond([ - 'status' => 'success', - 'message' => 'fetch success', - 'data' => $rows - ], 200); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function create() - { - $input = $this->request->getJSON(true); - if (!$this->validate($this->rules)) { - return $this->failValidationErrors($this->validator->getErrors()); - } - try { - $controlId = $this->dictControlModel->insert($input, true); - - if (!empty($input['test_ids'])) { - foreach ($input['test_ids'] as $testId) { - $this->controlTestModel->insert([ - 'control_ref_id' => $controlId, - 'test_ref_id' => $testId, - 'mean' => 0, - 'sd' => 0, - ]); - } - } - - return $this->respondCreated([ - 'status' => 'success', - 'message' => $controlId - ]); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function update($id = null) - { - $input = $this->request->getJSON(true); - if (!$this->validate($this->rules)) { - return $this->failValidationErrors($this->validator->getErrors()); - } - try { - $this->dictControlModel->update($id, $input); - - if (!empty($input['test_ids'])) { - $this->controlTestModel->where('control_ref_id', $id)->delete(); - foreach ($input['test_ids'] as $testId) { - $this->controlTestModel->insert([ - 'control_ref_id' => $id, - 'test_ref_id' => $testId, - 'mean' => 0, - 'sd' => 0, - ]); - } - } - - return $this->respond([ - 'status' => 'success', - 'message' => 'update success', - 'data' => $id - ]); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function delete($id = null) - { - try { - $this->controlTestModel->where('control_ref_id', $id)->delete(); - $this->dictControlModel->delete($id); - return $this->respond([ - 'status' => 'success', - 'message' => 'delete success' - ]); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } -} diff --git a/app/Controllers/Api/EntryApiController.php b/app/Controllers/Api/EntryApiController.php deleted file mode 100644 index b8971a1..0000000 --- a/app/Controllers/Api/EntryApiController.php +++ /dev/null @@ -1,252 +0,0 @@ -dictControlModel = new \App\Models\DictControlModel(); - $this->controlTestModel = new \App\Models\ControlTestModel(); - $this->dailyResultModel = new \App\Models\DailyResultModel(); - $this->resultModel = new \App\Models\ResultModel(); - $this->commentModel = new \App\Models\ResultCommentModel(); - } - - public function getControls() - { - try { - $date = $this->request->getGet('date'); - $deptId = $this->request->getGet('deptId'); - - $rows = $this->dictControlModel->getActiveByDate($date, $deptId); - return $this->respond([ - 'status' => 'success', - 'message' => 'fetch success', - 'data' => $rows - ], 200); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function getTests() - { - try { - $controlId = $this->request->getGet('controlId'); - - $rows = $this->controlTestModel->getByControl($controlId); - return $this->respond([ - 'status' => 'success', - 'message' => 'fetch success', - 'data' => $rows - ], 200); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function saveDaily() - { - $input = $this->request->getJSON(true); - - try { - $resultData = [ - 'control_ref_id' => $input['controlId'] ?? 0, - 'test_ref_id' => $input['testId'] ?? 0, - 'resdate' => $input['resdate'] ?? date('Y-m-d'), - 'resvalue' => $input['resvalue'] ?? '', - 'rescomment' => $input['rescomment'] ?? '', - ]; - - $this->dailyResultModel->saveResult($resultData); - return $this->respond([ - 'status' => 'success', - 'message' => 'save success' - ]); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function saveMonthly() - { - $input = $this->request->getJSON(true); - - try { - $controlId = $input['controlId'] ?? 0; - $yearMonth = $input['yearMonth'] ?? ''; - $operation = $input['operation'] ?? 'replace'; - $tests = $input['tests'] ?? []; - $results = []; - $statistics = []; - $validations = []; - - foreach ($tests as $testData) { - $testId = $testData['testId'] ?? 0; - $resvalues = $testData['resvalue'] ?? []; - $rescomments = $testData['rescomment'] ?? []; - - $controlTest = $this->controlTestModel->getByControlAndTest($controlId, $testId); - $mean = $controlTest['mean'] ?? 0; - $sd = $controlTest['sd'] ?? 0; - $sdLimit = $sd > 0 ? $sd * 2 : 0; - - $testValues = []; - $validCount = 0; - $validSum = 0; - $validSqSum = 0; - - foreach ($resvalues as $day => $value) { - if (!empty($value)) { - $resdate = $yearMonth . '-' . str_pad($day, 2, '0', STR_PAD_LEFT); - $rescomment = $rescomments[$day] ?? ''; - - $resultData = [ - 'control_ref_id' => $controlId, - 'test_ref_id' => $testId, - 'resdate' => $resdate, - 'resvalue' => $value, - 'rescomment' => $rescomment, - ]; - - if ($operation === 'replace') { - $this->resultModel->saveResult($resultData); - } else { - $existing = $this->resultModel->checkExisting($controlId, $testId, $resultData['resdate']); - if (!$existing) { - $this->resultModel->saveResult($resultData); - } - } - - $numValue = (float) $value; - $testValues[] = $numValue; - $validCount++; - $validSum += $numValue; - $validSqSum += $numValue * $numValue; - - $withinLimit = true; - if ($sdLimit > 0) { - $withinLimit = abs($numValue - $mean) <= $sdLimit; - } - - if (!$withinLimit) { - $validations[] = [ - 'testId' => $testId, - 'day' => $day, - 'value' => $value, - 'mean' => $mean, - 'sd' => $sd, - 'status' => 'out_of_range' - ]; - } - } - } - - if ($validCount > 0) { - $calcMean = $validSum / $validCount; - $calcSd = 0; - if ($validCount > 1) { - $variance = ($validSqSum - ($validSum * $validSum) / $validCount) / ($validCount - 1); - $calcSd = $variance > 0 ? sqrt($variance) : 0; - } - $cv = $calcMean > 0 ? ($calcSd / $calcMean) * 100 : 0; - - $statistics[] = [ - 'testId' => $testId, - 'n' => $validCount, - 'mean' => round($calcMean, 3), - 'sd' => round($calcSd, 3), - 'cv' => round($cv, 2) - ]; - } - - $results[] = [ - 'testId' => $testId, - 'saved' => count($resvalues) - ]; - } - - return $this->respond([ - 'status' => 'success', - 'message' => 'save success', - 'data' => [ - 'results' => $results, - 'statistics' => $statistics, - 'validations' => $validations - ] - ]); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function saveComment() - { - $input = $this->request->getJSON(true); - - try { - $commentData = [ - 'control_ref_id' => $input['controlId'] ?? 0, - 'test_ref_id' => $input['testId'] ?? 0, - 'commonth' => $input['commonth'] ?? '', - 'comtext' => $input['comtext'] ?? '', - ]; - - $this->commentModel->saveComment($commentData); - return $this->respond([ - 'status' => 'success', - 'message' => 'save success' - ]); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } - - public function getMonthlyData() - { - $controlId = $this->request->getGet('controlId'); - $testId = $this->request->getGet('testId'); - $yearMonth = $this->request->getGet('yearMonth'); - - try { - $results = $this->resultModel->getByMonth($controlId, $testId, $yearMonth); - $comment = $this->commentModel->getByControlTestMonth($controlId, $testId, $yearMonth); - - $formValues = []; - $comments = []; - foreach ($results as $row) { - $day = (int)date('j', strtotime($row['resdate'])); - $formValues[$day] = $row['resvalue']; - if (!empty($row['rescomment'])) { - $comments[$day] = $row['rescomment']; - } - } - - return $this->respond([ - 'status' => 'success', - 'message' => 'fetch success', - 'data' => [ - 'formValues' => $formValues, - 'comments' => $comments, - 'comment' => $comment ? $comment['comtext'] : '' - ] - ], 200); - } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); - } - } -} diff --git a/app/Controllers/Control.php b/app/Controllers/Control.php deleted file mode 100644 index 0969dc6..0000000 --- a/app/Controllers/Control.php +++ /dev/null @@ -1,36 +0,0 @@ -dictControlModel = new DictControlModel(); - $this->controlTestModel = new ControlTestModel(); - $this->dictDeptModel = new DictDeptModel(); - $this->dictTestModel = new DictTestModel(); - } - - public function index(): string - { - return view('control/index', [ - 'title' => 'Control Dictionary', - 'controls' => $this->dictControlModel->getWithDept(), - 'depts' => $this->dictDeptModel->findAll(), - 'tests' => $this->dictTestModel->getWithDept(), - 'active_menu' => 'control', - 'page_title' => 'Control Dictionary' - ]); - } -} diff --git a/app/Controllers/Dashboard.php b/app/Controllers/Dashboard.php deleted file mode 100644 index fec4041..0000000 --- a/app/Controllers/Dashboard.php +++ /dev/null @@ -1,28 +0,0 @@ - $dictDeptModel->findAll(), - 'tests' => $dictTestModel->getWithDept(), - 'controls' => $dictControlModel->getWithDept(), - 'recent_results' => $resultModel->findAll(20), - 'page_title' => 'Dashboard', - 'active_menu' => 'dashboard' - ]); - } -} diff --git a/app/Controllers/Dept.php b/app/Controllers/Dept.php deleted file mode 100644 index ffd0055..0000000 --- a/app/Controllers/Dept.php +++ /dev/null @@ -1,25 +0,0 @@ -dictDeptModel = new DictDeptModel(); - } - - public function index(): string - { - return view('dept/index', [ - 'title' => 'Department Dictionary', - 'depts' => $this->dictDeptModel->findAll(), - 'active_menu' => 'dept', - 'page_title' => 'Department Dictionary' - ]); - } -} diff --git a/app/Controllers/Entry.php b/app/Controllers/Entry.php deleted file mode 100644 index 5bbce67..0000000 --- a/app/Controllers/Entry.php +++ /dev/null @@ -1,32 +0,0 @@ - 'Monthly Entry', - 'depts' => $dictDeptModel->findAll(), - 'active_menu' => 'entry', - 'page_title' => 'Monthly Entry' - ]); - } - - public function daily() - { - $dictDeptModel = new \App\Models\DictDeptModel(); - - return view('entry/daily', [ - 'title' => 'Daily Entry', - 'depts' => $dictDeptModel->findAll(), - 'active_menu' => 'entry_daily', - 'page_title' => 'Daily Entry' - ]); - } -} diff --git a/app/Controllers/Api/TestApiController.php b/app/Controllers/Master/MasterControlsController.php similarity index 52% rename from app/Controllers/Api/TestApiController.php rename to app/Controllers/Master/MasterControlsController.php index 37f0cb1..ef09e7f 100644 --- a/app/Controllers/Api/TestApiController.php +++ b/app/Controllers/Master/MasterControlsController.php @@ -1,46 +1,42 @@ dictTestModel = new DictTestModel(); - $this->dictDeptModel = new DictDeptModel(); + public function __construct() { + $this->model = new MasterControlsModel(); $this->rules = [ 'name' => 'required|min_length[1]', - 'dept_ref_id' => 'required', + 'lot' => 'required|min_length[1]', ]; } - public function index() - { + public function index() { + $keyword = $this->request->getGet('keyword'); try { - $rows = $this->dictTestModel->getWithDept(); + $rows = $this->model->search($keyword); return $this->respond([ 'status' => 'success', 'message' => 'fetch success', 'data' => $rows ], 200); } catch (\Exception $e) { - return $this->failServerError('Exception: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } - public function show($id = null) - { + public function show($id = null) { try { - $rows = $this->dictTestModel->where('test_id', $id)->findAll(); - if (empty($rows)) { + $row = $this->model->find($id); + if (!$row) { return $this->respond([ 'status' => 'success', 'message' => 'data not found.' @@ -49,58 +45,58 @@ class TestApiController extends BaseController return $this->respond([ 'status' => 'success', 'message' => 'fetch success', - 'data' => $rows + 'data' => [$row] ], 200); } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } - public function create() - { + public function create() { $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); if (!$this->validate($this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } try { - $id = $this->dictTestModel->insert($input, true); + $id = $this->model->insert($input, true); return $this->respondCreated([ 'status' => 'success', 'message' => $id ]); } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } - public function update($id = null) - { + public function update($id = null) { $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); if (!$this->validate($this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } try { - $this->dictTestModel->update($id, $input); + $this->model->update($id, $input); return $this->respond([ 'status' => 'success', 'message' => 'update success', - 'data' => $id - ]); + 'data' => [$id] + ], 200); } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } - public function delete($id = null) - { + public function delete($id = null) { try { - $this->dictTestModel->delete($id); + $this->model->delete($id); return $this->respond([ 'status' => 'success', - 'message' => 'delete success' - ]); + 'message' => 'delete success', + 'data' => [$id] + ], 200); } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } } diff --git a/app/Controllers/Api/DeptApiController.php b/app/Controllers/Master/MasterDeptsController.php similarity index 54% rename from app/Controllers/Api/DeptApiController.php rename to app/Controllers/Master/MasterDeptsController.php index a7c701d..4fd60ea 100644 --- a/app/Controllers/Api/DeptApiController.php +++ b/app/Controllers/Master/MasterDeptsController.php @@ -1,42 +1,41 @@ dictDeptModel = new DictDeptModel(); + public function __construct() { + $this->model = new MasterDeptsModel(); $this->rules = [ 'name' => 'required|min_length[1]', ]; } - public function index() - { + public function index() { + $keyword = $this->request->getGet('keyword'); try { - $rows = $this->dictDeptModel->findAll(); + $rows = $this->model->search($keyword); return $this->respond([ 'status' => 'success', 'message' => 'fetch success', 'data' => $rows ], 200); } catch (\Exception $e) { - return $this->failServerError('Exception: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } - public function show($id = null) - { + public function show($id = null) { try { - $rows = $this->dictDeptModel->where('dept_id', $id)->findAll(); - if (empty($rows)) { + $row = $this->model->find($id); + if (!$row) { return $this->respond([ 'status' => 'success', 'message' => 'data not found.' @@ -45,58 +44,58 @@ class DeptApiController extends BaseController return $this->respond([ 'status' => 'success', 'message' => 'fetch success', - 'data' => $rows + 'data' => [$row] ], 200); } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } - public function create() - { + public function create() { $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); if (!$this->validate($this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } try { - $id = $this->dictDeptModel->insert($input, true); + $id = $this->model->insert($input, true); return $this->respondCreated([ 'status' => 'success', 'message' => $id ]); } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } - public function update($id = null) - { + public function update($id = null) { $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); if (!$this->validate($this->rules)) { return $this->failValidationErrors($this->validator->getErrors()); } try { - $this->dictDeptModel->update($id, $input); + $this->model->update($id, $input); return $this->respond([ 'status' => 'success', 'message' => 'update success', - 'data' => $id - ]); + 'data' => [$id] + ], 200); } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } - public function delete($id = null) - { + public function delete($id = null) { try { - $this->dictDeptModel->delete($id); + $this->model->delete($id); return $this->respond([ 'status' => 'success', - 'message' => 'delete success' - ]); + 'message' => 'delete success', + 'data' => [$id] + ], 200); } catch (\Exception $e) { - return $this->failServerError('Something went wrong: ' . $e->getMessage()); + return $this->failServerError($e->getMessage()); } } } diff --git a/app/Controllers/Master/MasterTestsController.php b/app/Controllers/Master/MasterTestsController.php new file mode 100644 index 0000000..916c6e8 --- /dev/null +++ b/app/Controllers/Master/MasterTestsController.php @@ -0,0 +1,101 @@ +model = new MasterTestsModel(); + $this->rules = [ + 'name' => 'required|min_length[1]', + ]; + } + + public function index() { + $keyword = $this->request->getGet('keyword'); + try { + $rows = $this->model->search($keyword); + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => $rows + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function show($id = null) { + try { + $row = $this->model->find($id); + if (!$row) { + return $this->respond([ + 'status' => 'success', + 'message' => 'data not found.' + ], 200); + } + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => [$row] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function create() { + $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); + if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + try { + $id = $this->model->insert($input, true); + return $this->respondCreated([ + 'status' => 'success', + 'message' => $id + ]); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function update($id = null) { + $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); + if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + try { + $this->model->update($id, $input); + return $this->respond([ + 'status' => 'success', + 'message' => 'update success', + 'data' => [$id] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function delete($id = null) { + try { + $this->model->delete($id); + return $this->respond([ + 'status' => 'success', + 'message' => 'delete success', + 'data' => [$id] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } +} diff --git a/app/Controllers/PageController.php b/app/Controllers/PageController.php index 1f441ca..7991861 100644 --- a/app/Controllers/PageController.php +++ b/app/Controllers/PageController.php @@ -2,157 +2,40 @@ namespace App\Controllers; -class PageController extends BaseController -{ - protected $dictDeptModel; - protected $dictTestModel; - protected $dictControlModel; - protected $resultModel; - protected $controlTestModel; - protected $commentModel; +use CodeIgniter\API\ResponseTrait; - public function __construct() - { - $this->dictDeptModel = new \App\Models\DictDeptModel(); - $this->dictTestModel = new \App\Models\DictTestModel(); - $this->dictControlModel = new \App\Models\DictControlModel(); - $this->resultModel = new \App\Models\ResultModel(); - $this->controlTestModel = new \App\Models\ControlTestModel(); - $this->commentModel = new \App\Models\ResultCommentModel(); +class PageController extends BaseController { + use ResponseTrait; + + public function dashboard() { + return view('dashboard'); } - public function dashboard(): string - { - return view('dashboard', [ - 'depts' => $this->dictDeptModel->findAll(), - 'tests' => $this->dictTestModel->getWithDept(), - 'controls' => $this->dictControlModel->getWithDept(), - 'recent_results' => $this->resultModel->findAll(20), - 'page_title' => 'Dashboard', - 'active_menu' => 'dashboard' - ]); + public function masterDept() { + return view('master/dept/index'); } - public function dept(): string - { - return view('dept/index', [ - 'title' => 'Department Dictionary', - 'depts' => $this->dictDeptModel->findAll(), - 'active_menu' => 'dept', - 'page_title' => 'Department Dictionary' - ]); + public function masterTest() { + return view('master/test/index'); } - public function test(): string - { - return view('test/index', [ - 'title' => 'Test Dictionary', - 'tests' => $this->dictTestModel->getWithDept(), - 'depts' => $this->dictDeptModel->findAll(), - 'active_menu' => 'test', - 'page_title' => 'Test Dictionary' - ]); + public function masterControl() { + return view('master/control/index'); } - public function control(): string - { - return view('control/index', [ - 'title' => 'Control Dictionary', - 'controls' => $this->dictControlModel->getWithDept(), - 'depts' => $this->dictDeptModel->findAll(), - 'tests' => $this->dictTestModel->getWithDept(), - 'active_menu' => 'control', - 'page_title' => 'Control Dictionary' - ]); + public function entry() { + return view('entry/index'); } - public function entry(): string - { - return view('entry/monthly', [ - 'title' => 'Monthly Entry', - 'depts' => $this->dictDeptModel->findAll(), - 'active_menu' => 'entry', - 'page_title' => 'Monthly Entry' - ]); + public function entryDaily() { + return view('entry/daily'); } - public function entryDaily(): string - { - return view('entry/daily', [ - 'title' => 'Daily Entry', - 'depts' => $this->dictDeptModel->findAll(), - 'active_menu' => 'entry_daily', - 'page_title' => 'Daily Entry' - ]); + public function report() { + return view('report/index'); } - public function report(): string - { - return view('report/index', [ - 'title' => 'Reports', - 'depts' => $this->dictDeptModel->findAll(), - 'tests' => $this->dictTestModel->findAll(), - 'controls' => $this->dictControlModel->findAll(), - 'active_menu' => 'report', - 'page_title' => 'Reports' - ]); - } - - public function reportView(): string - { - $control1 = $this->request->getGet('control1') ?? 0; - $control2 = $this->request->getGet('control2') ?? 0; - $control3 = $this->request->getGet('control3') ?? 0; - $dates = $this->request->getGet('dates') ?? date('Y-m'); - $test = $this->request->getGet('test') ?? 0; - - $controls = []; - if ($control1) { - $c1 = $this->dictControlModel->find($control1); - if ($c1) $controls[] = $c1; - } - if ($control2) { - $c2 = $this->dictControlModel->find($control2); - if ($c2) $controls[] = $c2; - } - if ($control3) { - $c3 = $this->dictControlModel->find($control3); - if ($c3) $controls[] = $c3; - } - - $reportData = []; - foreach ($controls as $control) { - $controlTest = $this->controlTestModel->getByControlAndTest($control['control_id'], $test); - $results = $this->resultModel->getByMonth($control['control_id'], $test, $dates); - $comment = $this->commentModel->getByControlTestMonth($control['control_id'], $test, $dates); - - $outOfRangeCount = 0; - if ($controlTest && $controlTest['sd'] > 0) { - foreach ($results as $res) { - if (abs($res['resvalue'] - $controlTest['mean']) > 2 * $controlTest['sd']) { - $outOfRangeCount++; - } - } - } - - $reportData[] = [ - 'control' => $control, - 'controlTest' => $controlTest, - 'results' => $results, - 'comment' => $comment, - 'test' => $this->dictTestModel->find($test), - 'outOfRange' => $outOfRangeCount - ]; - } - - return view('report/view', [ - 'title' => 'QC Report', - 'reportData' => $reportData, - 'dates' => $dates, - 'test' => $test, - 'depts' => $this->dictDeptModel->findAll(), - 'active_menu' => 'report', - 'page_title' => 'QC Report' - ]); + public function reportView() { + return view('report/view'); } } diff --git a/app/Controllers/Qc/ControlTestsController.php b/app/Controllers/Qc/ControlTestsController.php new file mode 100644 index 0000000..3b3b1af --- /dev/null +++ b/app/Controllers/Qc/ControlTestsController.php @@ -0,0 +1,99 @@ +model = new ControlTestsModel(); + $this->rules = []; + } + + public function index() { + $keyword = $this->request->getGet('keyword'); + try { + $rows = $this->model->search($keyword); + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => $rows + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function show($id = null) { + try { + $row = $this->model->find($id); + if (!$row) { + return $this->respond([ + 'status' => 'success', + 'message' => 'data not found.' + ], 200); + } + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => [$row] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function create() { + $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); + if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + try { + $id = $this->model->insert($input, true); + return $this->respondCreated([ + 'status' => 'success', + 'message' => $id + ]); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function update($id = null) { + $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); + if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + try { + $this->model->update($id, $input); + return $this->respond([ + 'status' => 'success', + 'message' => 'update success', + 'data' => [$id] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function delete($id = null) { + try { + $this->model->delete($id); + return $this->respond([ + 'status' => 'success', + 'message' => 'delete success', + 'data' => [$id] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } +} diff --git a/app/Controllers/Qc/ResultCommentsController.php b/app/Controllers/Qc/ResultCommentsController.php new file mode 100644 index 0000000..1896213 --- /dev/null +++ b/app/Controllers/Qc/ResultCommentsController.php @@ -0,0 +1,99 @@ +model = new ResultCommentsModel(); + $this->rules = []; + } + + public function index() { + $keyword = $this->request->getGet('keyword'); + try { + $rows = $this->model->search($keyword); + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => $rows + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function show($id = null) { + try { + $row = $this->model->find($id); + if (!$row) { + return $this->respond([ + 'status' => 'success', + 'message' => 'data not found.' + ], 200); + } + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => [$row] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function create() { + $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); + if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + try { + $id = $this->model->insert($input, true); + return $this->respondCreated([ + 'status' => 'success', + 'message' => $id + ]); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function update($id = null) { + $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); + if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + try { + $this->model->update($id, $input); + return $this->respond([ + 'status' => 'success', + 'message' => 'update success', + 'data' => [$id] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function delete($id = null) { + try { + $this->model->delete($id); + return $this->respond([ + 'status' => 'success', + 'message' => 'delete success', + 'data' => [$id] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } +} diff --git a/app/Controllers/Qc/ResultsController.php b/app/Controllers/Qc/ResultsController.php new file mode 100644 index 0000000..8b0deb8 --- /dev/null +++ b/app/Controllers/Qc/ResultsController.php @@ -0,0 +1,99 @@ +model = new ResultsModel(); + $this->rules = []; + } + + public function index() { + $keyword = $this->request->getGet('keyword'); + try { + $rows = $this->model->search($keyword); + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => $rows + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function show($id = null) { + try { + $row = $this->model->find($id); + if (!$row) { + return $this->respond([ + 'status' => 'success', + 'message' => 'data not found.' + ], 200); + } + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => [$row] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function create() { + $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); + if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + try { + $id = $this->model->insert($input, true); + return $this->respondCreated([ + 'status' => 'success', + 'message' => $id + ]); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function update($id = null) { + $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); + if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + try { + $this->model->update($id, $input); + return $this->respond([ + 'status' => 'success', + 'message' => 'update success', + 'data' => [$id] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } + + public function delete($id = null) { + try { + $this->model->delete($id); + return $this->respond([ + 'status' => 'success', + 'message' => 'delete success', + 'data' => [$id] + ], 200); + } catch (\Exception $e) { + return $this->failServerError($e->getMessage()); + } + } +} diff --git a/app/Controllers/Report.php b/app/Controllers/Report.php deleted file mode 100644 index 5021bc8..0000000 --- a/app/Controllers/Report.php +++ /dev/null @@ -1,26 +0,0 @@ - 'Reports', - 'depts' => $dictDeptModel->findAll(), - 'tests' => $dictTestModel->findAll(), - 'controls' => $dictControlModel->findAll(), - 'active_menu' => 'report', - 'page_title' => 'Reports' - ]); - } -} diff --git a/app/Controllers/Test.php b/app/Controllers/Test.php deleted file mode 100644 index 0ef1b81..0000000 --- a/app/Controllers/Test.php +++ /dev/null @@ -1,29 +0,0 @@ -dictTestModel = new DictTestModel(); - $this->dictDeptModel = new DictDeptModel(); - } - - public function index() - { - return view('test/index', [ - 'title' => 'Test Dictionary', - 'tests' => $this->dictTestModel->getWithDept(), - 'depts' => $this->dictDeptModel->findAll(), - 'active_menu' => 'test', - 'page_title' => 'Test Dictionary' - ]); - } -} diff --git a/app/Database/Migrations/2026-01-14-000001_CreateCmodQcTables.php b/app/Database/Migrations/2026-01-14-000001_CreateCmodQcTables.php deleted file mode 100644 index ae2c90a..0000000 --- a/app/Database/Migrations/2026-01-14-000001_CreateCmodQcTables.php +++ /dev/null @@ -1,282 +0,0 @@ -forge->addField([ - 'control_test_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'unsigned' => true, - 'auto_increment' => true, - ], - 'control_ref_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'null' => true, - ], - 'test_ref_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'null' => true, - ], - 'mean' => [ - 'type' => 'FLOAT', - 'null' => true, - ], - 'sd' => [ - 'type' => 'FLOAT', - 'null' => true, - ], - 'created_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'updated_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'deleted_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - ]); - $this->forge->addKey('control_test_id', true); - $this->forge->createTable('control_tests'); - - $this->forge->addField([ - 'result_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'unsigned' => true, - 'auto_increment' => true, - ], - 'control_ref_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'null' => true, - ], - 'test_ref_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'null' => true, - ], - 'resdate' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'resvalue' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'rescomment' => [ - 'type' => 'TEXT', - 'null' => true, - ], - 'created_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'updated_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'deleted_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - ]); - $this->forge->addKey('result_id', true); - $this->forge->createTable('results'); - - $this->forge->addField([ - 'control_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'unsigned' => true, - 'auto_increment' => true, - ], - 'dept_ref_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'null' => true, - ], - 'name' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'lot' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'producer' => [ - 'type' => 'TEXT', - 'null' => true, - ], - 'expdate' => [ - 'type' => 'DATE', - 'null' => true, - ], - 'created_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'updated_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'deleted_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - ]); - $this->forge->addKey('control_id', true); - $this->forge->createTable('dict_controls'); - - $this->forge->addField([ - 'dept_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'unsigned' => true, - 'auto_increment' => true, - ], - 'name' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'created_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'updated_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'deleted_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - ]); - $this->forge->addKey('dept_id', true); - $this->forge->createTable('dict_depts'); - - $this->forge->addField([ - 'test_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'unsigned' => true, - 'auto_increment' => true, - ], - 'dept_ref_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'null' => true, - ], - 'name' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'unit' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'method' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'cva' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'ba' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'tea' => [ - 'type' => 'VARCHAR', - 'constraint' => 50, - 'null' => true, - ], - 'created_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'updated_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'deleted_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - ]); - $this->forge->addKey('test_id', true); - $this->forge->createTable('dict_tests'); - - $this->forge->addField([ - 'result_comment_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'unsigned' => true, - 'auto_increment' => true, - ], - 'control_ref_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'null' => true, - ], - 'test_ref_id' => [ - 'type' => 'INT', - 'constraint' => 11, - 'null' => true, - ], - 'commonth' => [ - 'type' => 'VARCHAR', - 'constraint' => 7, - 'null' => true, - ], - 'comtext' => [ - 'type' => 'TEXT', - 'null' => true, - ], - 'created_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'updated_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - 'deleted_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], - ]); - $this->forge->addKey('result_comment_id', true); - $this->forge->createTable('result_comments'); - } - - public function down() - { - $this->forge->dropTable('control_tests'); - $this->forge->dropTable('results'); - $this->forge->dropTable('dict_controls'); - $this->forge->dropTable('dict_depts'); - $this->forge->dropTable('dict_tests'); - $this->forge->dropTable('result_comments'); - } -} diff --git a/app/Database/Migrations/2026-01-17-000001-QualityControlSystem.php b/app/Database/Migrations/2026-01-17-000001-QualityControlSystem.php new file mode 100644 index 0000000..cd5a9bf --- /dev/null +++ b/app/Database/Migrations/2026-01-17-000001-QualityControlSystem.php @@ -0,0 +1,107 @@ +forge->addField([ + 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('dept_id', true); + $this->forge->createTable('dict_depts'); + + $this->forge->addField([ + 'control_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'lot' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'producer' => ['type' => 'TEXT', 'null' => true], + 'exp_date' => ['type' => 'DATE', 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('control_id', true); + $this->forge->addForeignKey('dept_id', 'dict_depts', 'dept_id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('dict_controls'); + + $this->forge->addField([ + 'test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'unit' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'method' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'cva' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'ba' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'tea' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('test_id', true); + $this->forge->addForeignKey('dept_id', 'dict_depts', 'dept_id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('dict_tests'); + + $this->forge->addField([ + 'control_test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'test_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'mean' => ['type' => 'FLOAT', 'null' => true], + 'sd' => ['type' => 'FLOAT', 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('control_test_id', true); + $this->forge->addForeignKey('control_id', 'dict_controls', 'control_id', 'SET NULL', 'CASCADE'); + $this->forge->addForeignKey('test_id', 'dict_tests', 'test_id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('control_tests'); + + $this->forge->addField([ + 'result_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'test_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'res_date' => ['type' => 'DATETIME', 'null' => true], + 'res_value' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'res_comment' => ['type' => 'TEXT', 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('result_id', true); + $this->forge->addForeignKey('control_id', 'dict_controls', 'control_id', 'SET NULL', 'CASCADE'); + $this->forge->addForeignKey('test_id', 'dict_tests', 'test_id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('results'); + + $this->forge->addField([ + 'result_comment_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'control_id' => ['type' => 'INT', 'unsigned' => true], + 'test_id' => ['type' => 'INT', 'unsigned' => true], + 'comment_month' => ['type' => 'VARCHAR', 'constraint' => 7], + 'com_text' => ['type' => 'TEXT', 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('result_comment_id', true); + $this->forge->addUniqueKey(['control_id', 'test_id', 'comment_month']); + $this->forge->addForeignKey('control_id', 'dict_controls', 'control_id', 'CASCADE', 'CASCADE'); + $this->forge->addForeignKey('test_id', 'dict_tests', 'test_id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('result_comments'); + } + + public function down() { + $this->forge->dropTable('result_comments', true); + $this->forge->dropTable('results', true); + $this->forge->dropTable('control_tests', true); + $this->forge->dropTable('dict_tests', true); + $this->forge->dropTable('dict_controls', true); + $this->forge->dropTable('dict_depts', true); + } +} diff --git a/app/Database/Migrations/2026-01-18-000001-QualityControlSystem.php b/app/Database/Migrations/2026-01-18-000001-QualityControlSystem.php new file mode 100644 index 0000000..500f7c6 --- /dev/null +++ b/app/Database/Migrations/2026-01-18-000001-QualityControlSystem.php @@ -0,0 +1,110 @@ +forge->addField([ + 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('dept_id', true); + $this->forge->createTable('master_depts'); + + $this->forge->addField([ + 'control_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'lot' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'producer' => ['type' => 'TEXT', 'null' => true], + 'exp_date' => ['type' => 'DATE', 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('control_id', true); + $this->forge->addForeignKey('dept_id', 'master_depts', 'dept_id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('master_controls'); + + $this->forge->addField([ + 'test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'unit' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'method' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'cva' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'ba' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'tea' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('test_id', true); + $this->forge->addForeignKey('dept_id', 'master_depts', 'dept_id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('master_tests'); + + $this->forge->addField([ + 'control_test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'test_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'mean' => ['type' => 'FLOAT', 'null' => true], + 'sd' => ['type' => 'FLOAT', 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('control_test_id', true); + $this->forge->addForeignKey('control_id', 'master_controls', 'control_id', 'SET NULL', 'CASCADE'); + $this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('control_tests'); + + $this->forge->addField([ + 'result_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'test_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], + 'res_date' => ['type' => 'DATETIME', 'null' => true], + 'res_value' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'res_comment' => ['type' => 'TEXT', 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('result_id', true); + $this->forge->addForeignKey('control_id', 'master_controls', 'control_id', 'SET NULL', 'CASCADE'); + $this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE'); + $this->forge->createTable('results'); + + $this->forge->addField([ + 'result_comment_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], + 'control_id' => ['type' => 'INT', 'unsigned' => true], + 'test_id' => ['type' => 'INT', 'unsigned' => true], + 'comment_month' => ['type' => 'VARCHAR', 'constraint' => 7], + 'com_text' => ['type' => 'TEXT', 'null' => true], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + 'deleted_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('result_comment_id', true); + $this->forge->addUniqueKey(['control_id', 'test_id', 'comment_month']); + $this->forge->addForeignKey('control_id', 'master_controls', 'control_id', 'CASCADE', 'CASCADE'); + $this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'CASCADE', 'CASCADE'); + $this->forge->createTable('result_comments'); + } + + public function down() + { + $this->forge->dropTable('result_comments', true); + $this->forge->dropTable('results', true); + $this->forge->dropTable('control_tests', true); + $this->forge->dropTable('master_tests', true); + $this->forge->dropTable('master_controls', true); + $this->forge->dropTable('master_depts', true); + } +} diff --git a/app/Database/Migrations/2026-01-18-000001-RenameDictToMasterTables.php b/app/Database/Migrations/2026-01-18-000001-RenameDictToMasterTables.php new file mode 100644 index 0000000..3bac25e --- /dev/null +++ b/app/Database/Migrations/2026-01-18-000001-RenameDictToMasterTables.php @@ -0,0 +1,45 @@ +query('SET FOREIGN_KEY_CHECKS=0'); + + $tables = $db->listTables(); + if (in_array('dict_depts', $tables)) { + $db->query('RENAME TABLE dict_depts TO master_depts'); + } + if (in_array('dict_controls', $tables)) { + $db->query('RENAME TABLE dict_controls TO master_controls'); + } + if (in_array('dict_tests', $tables)) { + $db->query('RENAME TABLE dict_tests TO master_tests'); + } + + $db->query('SET FOREIGN_KEY_CHECKS=1'); + } + + public function down() { + $db = \Config\Database::connect(); + + $db->query('SET FOREIGN_KEY_CHECKS=0'); + + $tables = $db->listTables(); + if (in_array('master_depts', $tables)) { + $db->query('RENAME TABLE master_depts TO dict_depts'); + } + if (in_array('master_controls', $tables)) { + $db->query('RENAME TABLE master_controls TO dict_controls'); + } + if (in_array('master_tests', $tables)) { + $db->query('RENAME TABLE master_tests TO dict_tests'); + } + + $db->query('SET FOREIGN_KEY_CHECKS=1'); + } +} diff --git a/app/Database/Migrations/test.php b/app/Database/Migrations/test.php new file mode 100644 index 0000000..1bfd141 --- /dev/null +++ b/app/Database/Migrations/test.php @@ -0,0 +1,10 @@ +query("SELECT * FROM migrations")->getResult(); +print_r($results); + +echo "\nChecking for dict_ tables:\n"; +$tables = $db->listTables(); +print_r($tables); diff --git a/app/Database/Seeds/CmodQcSeeder.php b/app/Database/Seeds/CmodQcSeeder.php deleted file mode 100644 index c0cbdfd..0000000 --- a/app/Database/Seeds/CmodQcSeeder.php +++ /dev/null @@ -1,322 +0,0 @@ - 440, 'control_ref_id' => 125, 'test_ref_id' => 18, 'mean' => 1.28, 'sd' => 0.64], - ['control_test_id' => 441, 'control_ref_id' => 126, 'test_ref_id' => 18, 'mean' => 2.26, 'sd' => 1.13], - ['control_test_id' => 541, 'control_ref_id' => 169, 'test_ref_id' => 18, 'mean' => 1.24, 'sd' => 0.155], - ['control_test_id' => 443, 'control_ref_id' => 128, 'test_ref_id' => 20, 'mean' => 105, 'sd' => 14.6], - ['control_test_id' => 565, 'control_ref_id' => 179, 'test_ref_id' => 15, 'mean' => 44, 'sd' => 2.2], - ['control_test_id' => 566, 'control_ref_id' => 179, 'test_ref_id' => 16, 'mean' => 107, 'sd' => 5.35], - ['control_test_id' => 482, 'control_ref_id' => 144, 'test_ref_id' => 3, 'mean' => 1.81, 'sd' => 0.1575], - ['control_test_id' => 444, 'control_ref_id' => 127, 'test_ref_id' => 20, 'mean' => 26.8, 'sd' => 3.75], - ['control_test_id' => 523, 'control_ref_id' => 163, 'test_ref_id' => 1, 'mean' => 49.8, 'sd' => 3.75], - ['control_test_id' => 524, 'control_ref_id' => 163, 'test_ref_id' => 2, 'mean' => 56.4, 'sd' => 4.2], - ['control_test_id' => 483, 'control_ref_id' => 144, 'test_ref_id' => 4, 'mean' => 4.17, 'sd' => 0.36], - ['control_test_id' => 449, 'control_ref_id' => 133, 'test_ref_id' => 19, 'mean' => 12.55, 'sd' => 1.883], - ['control_test_id' => 567, 'control_ref_id' => 179, 'test_ref_id' => 13, 'mean' => 168, 'sd' => 8.4], - ['control_test_id' => 568, 'control_ref_id' => 179, 'test_ref_id' => 14, 'mean' => 124, 'sd' => 6.2], - ['control_test_id' => 439, 'control_ref_id' => 124, 'test_ref_id' => 19, 'mean' => 3.03, 'sd' => 0.455], - ['control_test_id' => 569, 'control_ref_id' => 180, 'test_ref_id' => 1, 'mean' => 49.5, 'sd' => 3.75], - ['control_test_id' => 450, 'control_ref_id' => 134, 'test_ref_id' => 13, 'mean' => 166, 'sd' => 8.3], - ['control_test_id' => 570, 'control_ref_id' => 180, 'test_ref_id' => 2, 'mean' => 56.4, 'sd' => 4.2], - ['control_test_id' => 451, 'control_ref_id' => 134, 'test_ref_id' => 14, 'mean' => 127, 'sd' => 6.35], - ['control_test_id' => 452, 'control_ref_id' => 134, 'test_ref_id' => 15, 'mean' => 43, 'sd' => 2.15], - ['control_test_id' => 545, 'control_ref_id' => 173, 'test_ref_id' => 1, 'mean' => 150, 'sd' => 12], - ['control_test_id' => 546, 'control_ref_id' => 173, 'test_ref_id' => 2, 'mean' => 137, 'sd' => 10.5], - ['control_test_id' => 542, 'control_ref_id' => 170, 'test_ref_id' => 18, 'mean' => 2.02, 'sd' => 0.275], - ['control_test_id' => 547, 'control_ref_id' => 173, 'test_ref_id' => 6, 'mean' => 126, 'sd' => 9], - ['control_test_id' => 453, 'control_ref_id' => 134, 'test_ref_id' => 16, 'mean' => 105, 'sd' => 5.25], - ['control_test_id' => 454, 'control_ref_id' => 135, 'test_ref_id' => 13, 'mean' => 232, 'sd' => 11.6], - ['control_test_id' => 455, 'control_ref_id' => 135, 'test_ref_id' => 14, 'mean' => 179, 'sd' => 8.95], - ['control_test_id' => 456, 'control_ref_id' => 135, 'test_ref_id' => 15, 'mean' => 61, 'sd' => 3.05], - ['control_test_id' => 457, 'control_ref_id' => 135, 'test_ref_id' => 16, 'mean' => 146, 'sd' => 7.3], - ['control_test_id' => 473, 'control_ref_id' => 143, 'test_ref_id' => 2, 'mean' => 137, 'sd' => 10.5], - ['control_test_id' => 474, 'control_ref_id' => 143, 'test_ref_id' => 1, 'mean' => 146, 'sd' => 10.5], - ['control_test_id' => 475, 'control_ref_id' => 143, 'test_ref_id' => 7, 'mean' => 4.3, 'sd' => 0.3225], - ['control_test_id' => 476, 'control_ref_id' => 143, 'test_ref_id' => 9, 'mean' => 243, 'sd' => 19], - ['control_test_id' => 489, 'control_ref_id' => 145, 'test_ref_id' => 37, 'mean' => 0.851, 'sd' => 0.113], - ['control_test_id' => 480, 'control_ref_id' => 143, 'test_ref_id' => 5, 'mean' => 4.73, 'sd' => 2.4], - ['control_test_id' => 481, 'control_ref_id' => 143, 'test_ref_id' => 36, 'mean' => 3.31, 'sd' => 0.12], - ['control_test_id' => 484, 'control_ref_id' => 144, 'test_ref_id' => 10, 'mean' => 162, 'sd' => 20.25], - ['control_test_id' => 485, 'control_ref_id' => 144, 'test_ref_id' => 11, 'mean' => 91.7, 'sd' => 6.7], - ['control_test_id' => 580, 'control_ref_id' => 182, 'test_ref_id' => 17, 'mean' => 10.71, 'sd' => 0.25], - ['control_test_id' => 429, 'control_ref_id' => 121, 'test_ref_id' => 3, 'mean' => 0.83, 'sd' => 0.073], - ['control_test_id' => 430, 'control_ref_id' => 121, 'test_ref_id' => 4, 'mean' => 1.53, 'sd' => 0.13], - ['control_test_id' => 431, 'control_ref_id' => 121, 'test_ref_id' => 10, 'mean' => 66.6, 'sd' => 5.575], - ['control_test_id' => 432, 'control_ref_id' => 121, 'test_ref_id' => 11, 'mean' => 27.5, 'sd' => 2.025], - ['control_test_id' => 582, 'control_ref_id' => 184, 'test_ref_id' => 10, 'mean' => 61.65, 'sd' => 5.125], - ['control_test_id' => 434, 'control_ref_id' => 121, 'test_ref_id' => 30, 'mean' => 144, 'sd' => 14.5], - ['control_test_id' => 583, 'control_ref_id' => 184, 'test_ref_id' => 3, 'mean' => 0.765, 'sd' => 0.0675], - ['control_test_id' => 487, 'control_ref_id' => 144, 'test_ref_id' => 30, 'mean' => 220, 'sd' => 22], - ['control_test_id' => 488, 'control_ref_id' => 143, 'test_ref_id' => 32, 'mean' => 8.07, 'sd' => 0.4], - ['control_test_id' => 490, 'control_ref_id' => 145, 'test_ref_id' => 38, 'mean' => 1.15, 'sd' => 0.15], - ['control_test_id' => 494, 'control_ref_id' => 145, 'test_ref_id' => 39, 'mean' => 6.69, 'sd' => 0.89], - ['control_test_id' => 495, 'control_ref_id' => 146, 'test_ref_id' => 37, 'mean' => 23.219, 'sd' => 3.096], - ['control_test_id' => 496, 'control_ref_id' => 146, 'test_ref_id' => 39, 'mean' => 15.24, 'sd' => 2.03], - ['control_test_id' => 497, 'control_ref_id' => 146, 'test_ref_id' => 38, 'mean' => 3.81, 'sd' => 0.51], - ['control_test_id' => 514, 'control_ref_id' => 153, 'test_ref_id' => 47, 'mean' => 0.08, 'sd' => 0.13], - ['control_test_id' => 515, 'control_ref_id' => 154, 'test_ref_id' => 47, 'mean' => 4.91, 'sd' => 0.9], - ['control_test_id' => 516, 'control_ref_id' => 158, 'test_ref_id' => 49, 'mean' => 22.34, 'sd' => 2.98], - ['control_test_id' => 517, 'control_ref_id' => 159, 'test_ref_id' => 49, 'mean' => 63.25, 'sd' => 8.43], - ['control_test_id' => 518, 'control_ref_id' => 155, 'test_ref_id' => 47, 'mean' => 7.6, 'sd' => 1.39], - ['control_test_id' => 519, 'control_ref_id' => 156, 'test_ref_id' => 48, 'mean' => 0.01, 'sd' => 0.13], - ['control_test_id' => 520, 'control_ref_id' => 157, 'test_ref_id' => 48, 'mean' => 4.62, 'sd' => 0.77], - ['control_test_id' => 525, 'control_ref_id' => 163, 'test_ref_id' => 8, 'mean' => 5.16, 'sd' => 0.355], - ['control_test_id' => 526, 'control_ref_id' => 163, 'test_ref_id' => 6, 'mean' => 41.1, 'sd' => 3.075], - ['control_test_id' => 527, 'control_ref_id' => 163, 'test_ref_id' => 7, 'mean' => 1.07, 'sd' => 0.08], - ['control_test_id' => 528, 'control_ref_id' => 163, 'test_ref_id' => 9, 'mean' => 101, 'sd' => 7.75], - ['control_test_id' => 529, 'control_ref_id' => 163, 'test_ref_id' => 5, 'mean' => 3.16, 'sd' => 0.1575], - ['control_test_id' => 530, 'control_ref_id' => 163, 'test_ref_id' => 32, 'mean' => 5.07, 'sd' => 0.2539], - ['control_test_id' => 535, 'control_ref_id' => 163, 'test_ref_id' => 36, 'mean' => 2.2, 'sd' => 0.0838], - ['control_test_id' => 536, 'control_ref_id' => 165, 'test_ref_id' => 17, 'mean' => 5.355, 'sd' => 0.15], - ['control_test_id' => 537, 'control_ref_id' => 166, 'test_ref_id' => 17, 'mean' => 10.29, 'sd' => 0.25], - ['control_test_id' => 548, 'control_ref_id' => 173, 'test_ref_id' => 7, 'mean' => 4.11, 'sd' => 0.305], - ['control_test_id' => 549, 'control_ref_id' => 173, 'test_ref_id' => 8, 'mean' => 10.7, 'sd' => 0.75], - ['control_test_id' => 550, 'control_ref_id' => 173, 'test_ref_id' => 9, 'mean' => 241, 'sd' => 18.75], - ['control_test_id' => 551, 'control_ref_id' => 173, 'test_ref_id' => 5, 'mean' => 4.74, 'sd' => 0.36], - ['control_test_id' => 553, 'control_ref_id' => 175, 'test_ref_id' => 19, 'mean' => 3.05, 'sd' => 0.457], - ['control_test_id' => 554, 'control_ref_id' => 174, 'test_ref_id' => 19, 'mean' => 12.53, 'sd' => 1.88], - ['control_test_id' => 555, 'control_ref_id' => 176, 'test_ref_id' => 43, 'mean' => 34.29, 'sd' => 2.8], - ['control_test_id' => 559, 'control_ref_id' => 163, 'test_ref_id' => 53, 'mean' => 4.46, 'sd' => 0.22], - ['control_test_id' => 560, 'control_ref_id' => 173, 'test_ref_id' => 53, 'mean' => 9.08, 'sd' => 0.47], - ['control_test_id' => 577, 'control_ref_id' => 180, 'test_ref_id' => 32, 'mean' => 5.12, 'sd' => 0.256], - ['control_test_id' => 584, 'control_ref_id' => 184, 'test_ref_id' => 4, 'mean' => 1.33, 'sd' => 0.115], - ['control_test_id' => 585, 'control_ref_id' => 184, 'test_ref_id' => 11, 'mean' => 31.98, 'sd' => 2.325], - ['control_test_id' => 586, 'control_ref_id' => 184, 'test_ref_id' => 30, 'mean' => 145, 'sd' => 14.5], - ['control_test_id' => 587, 'control_ref_id' => 185, 'test_ref_id' => 17, 'mean' => 5.67, 'sd' => 0.15], - ['control_test_id' => 588, 'control_ref_id' => 186, 'test_ref_id' => 17, 'mean' => 10.29, 'sd' => 0.25], - ['control_test_id' => 477, 'control_ref_id' => 143, 'test_ref_id' => 8, 'mean' => 10.4, 'sd' => 0.7], - ['control_test_id' => 478, 'control_ref_id' => 143, 'test_ref_id' => 6, 'mean' => 125, 'sd' => 9], - ['control_test_id' => 499, 'control_ref_id' => 147, 'test_ref_id' => 40, 'mean' => 2.7, 'sd' => 0.36], - ['control_test_id' => 500, 'control_ref_id' => 147, 'test_ref_id' => 41, 'mean' => 1.125, 'sd' => 0.15], - ['control_test_id' => 501, 'control_ref_id' => 147, 'test_ref_id' => 42, 'mean' => 4.38, 'sd' => 0.58], - ['control_test_id' => 502, 'control_ref_id' => 147, 'test_ref_id' => 43, 'mean' => 10.09, 'sd' => 1.35], - ['control_test_id' => 503, 'control_ref_id' => 147, 'test_ref_id' => 44, 'mean' => 29.43, 'sd' => 3.92], - ['control_test_id' => 592, 'control_ref_id' => 187, 'test_ref_id' => 3, 'mean' => 2.19, 'sd' => 0.19], - ['control_test_id' => 362, 'control_ref_id' => 98, 'test_ref_id' => 3, 'mean' => 1.8, 'sd' => 0.232], - ['control_test_id' => 363, 'control_ref_id' => 98, 'test_ref_id' => 4, 'mean' => 5.02, 'sd' => 0.652], - ['control_test_id' => 469, 'control_ref_id' => 139, 'test_ref_id' => 20, 'mean' => 26.7, 'sd' => 3.75], - ['control_test_id' => 593, 'control_ref_id' => 187, 'test_ref_id' => 4, 'mean' => 4.63, 'sd' => 0.403], - ['control_test_id' => 594, 'control_ref_id' => 187, 'test_ref_id' => 10, 'mean' => 151, 'sd' => 12.5], - ['control_test_id' => 595, 'control_ref_id' => 187, 'test_ref_id' => 11, 'mean' => 88.7, 'sd' => 6.58], - ['control_test_id' => 596, 'control_ref_id' => 187, 'test_ref_id' => 30, 'mean' => 216, 'sd' => 21.5], - ['control_test_id' => 470, 'control_ref_id' => 140, 'test_ref_id' => 20, 'mean' => 104.8, 'sd' => 14.6], - ['control_test_id' => 531, 'control_ref_id' => 164, 'test_ref_id' => 13, 'mean' => 233, 'sd' => 11.65], - ['control_test_id' => 532, 'control_ref_id' => 164, 'test_ref_id' => 14, 'mean' => 174, 'sd' => 8.7], - ['control_test_id' => 557, 'control_ref_id' => 173, 'test_ref_id' => 36, 'mean' => 3.26, 'sd' => 0.18], - ['control_test_id' => 533, 'control_ref_id' => 164, 'test_ref_id' => 15, 'mean' => 61, 'sd' => 3.05], - ['control_test_id' => 534, 'control_ref_id' => 164, 'test_ref_id' => 16, 'mean' => 147, 'sd' => 7.35], - ['control_test_id' => 384, 'control_ref_id' => 98, 'test_ref_id' => 11, 'mean' => 95.6, 'sd' => 10.625], - ['control_test_id' => 581, 'control_ref_id' => 183, 'test_ref_id' => 18, 'mean' => 1.94, 'sd' => 0.26], - ['control_test_id' => 579, 'control_ref_id' => 181, 'test_ref_id' => 17, 'mean' => 5.775, 'sd' => 0.15], - ['control_test_id' => 365, 'control_ref_id' => 98, 'test_ref_id' => 10, 'mean' => 188, 'sd' => 23.5], - ['control_test_id' => 367, 'control_ref_id' => 98, 'test_ref_id' => 21, 'mean' => 12.6, 'sd' => 0.675], - ['control_test_id' => 368, 'control_ref_id' => 98, 'test_ref_id' => 29, 'mean' => 7.66, 'sd' => 0.69], - ['control_test_id' => 369, 'control_ref_id' => 98, 'test_ref_id' => 31, 'mean' => 111, 'sd' => 11.025], - ['control_test_id' => 370, 'control_ref_id' => 98, 'test_ref_id' => 30, 'mean' => 252, 'sd' => 25.25], - ['control_test_id' => 505, 'control_ref_id' => 148, 'test_ref_id' => 40, 'mean' => 27, 'sd' => 3.6], - ['control_test_id' => 506, 'control_ref_id' => 148, 'test_ref_id' => 41, 'mean' => 15.277, 'sd' => 2.037], - ['control_test_id' => 507, 'control_ref_id' => 148, 'test_ref_id' => 42, 'mean' => 47.6, 'sd' => 6.35], - ['control_test_id' => 508, 'control_ref_id' => 148, 'test_ref_id' => 43, 'mean' => 58.51, 'sd' => 7.8], - ['control_test_id' => 509, 'control_ref_id' => 148, 'test_ref_id' => 44, 'mean' => 193.03, 'sd' => 25.74], - ['control_test_id' => 510, 'control_ref_id' => 149, 'test_ref_id' => 45, 'mean' => 0.001, 'sd' => 0.017], - ['control_test_id' => 511, 'control_ref_id' => 150, 'test_ref_id' => 45, 'mean' => 0.581, 'sd' => 0.077], - ['control_test_id' => 512, 'control_ref_id' => 151, 'test_ref_id' => 46, 'mean' => 0, 'sd' => 1.67], - ['control_test_id' => 513, 'control_ref_id' => 152, 'test_ref_id' => 46, 'mean' => 19.98, 'sd' => 3], - ['control_test_id' => 538, 'control_ref_id' => 167, 'test_ref_id' => 52, 'mean' => 1.08, 'sd' => 0.1], - ['control_test_id' => 539, 'control_ref_id' => 168, 'test_ref_id' => 39, 'mean' => 6.6485, 'sd' => 0.7125], - ['control_test_id' => 540, 'control_ref_id' => 160, 'test_ref_id' => 50, 'mean' => 34.4, 'sd' => 3.51], - ['control_test_id' => 552, 'control_ref_id' => 173, 'test_ref_id' => 32, 'mean' => 8.01, 'sd' => 0.6], - ['control_test_id' => 572, 'control_ref_id' => 180, 'test_ref_id' => 9, 'mean' => 101.5, 'sd' => 7.75], - ['control_test_id' => 578, 'control_ref_id' => 180, 'test_ref_id' => 5, 'mean' => 3.16, 'sd' => 0.158], - ['control_test_id' => 571, 'control_ref_id' => 180, 'test_ref_id' => 7, 'mean' => 1.08, 'sd' => 0.0825], - ['control_test_id' => 573, 'control_ref_id' => 180, 'test_ref_id' => 8, 'mean' => 5.225, 'sd' => 0.3525], - ['control_test_id' => 574, 'control_ref_id' => 180, 'test_ref_id' => 6, 'mean' => 40.7, 'sd' => 3.05], - ['control_test_id' => 590, 'control_ref_id' => 180, 'test_ref_id' => 53, 'mean' => 4.52, 'sd' => 0.22], - ['control_test_id' => 591, 'control_ref_id' => 180, 'test_ref_id' => 36, 'mean' => 2.2, 'sd' => 0.083], - ]; - $this->db->table('control_tests')->insertBatch($data); - - $data = [ - ['result_id' => 28, 'control_ref_id' => 1, 'test_ref_id' => 1, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '152.868220727426', 'rescomment' => null], - ['result_id' => 29, 'control_ref_id' => 1, 'test_ref_id' => 2, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '118.684360070769', 'rescomment' => null], - ['result_id' => 30, 'control_ref_id' => 1, 'test_ref_id' => 3, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.78676744546881', 'rescomment' => null], - ['result_id' => 31, 'control_ref_id' => 1, 'test_ref_id' => 4, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.88788440216215', 'rescomment' => null], - ['result_id' => 32, 'control_ref_id' => 1, 'test_ref_id' => 5, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.54649638396286', 'rescomment' => null], - ['result_id' => 33, 'control_ref_id' => 1, 'test_ref_id' => 6, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '68.9277965359498', 'rescomment' => null], - ['result_id' => 34, 'control_ref_id' => 1, 'test_ref_id' => 7, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.7616948568887', 'rescomment' => null], - ['result_id' => 35, 'control_ref_id' => 1, 'test_ref_id' => 8, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '8.2805131289407', 'rescomment' => null], - ['result_id' => 36, 'control_ref_id' => 1, 'test_ref_id' => 9, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '255.718750547819', 'rescomment' => null], - ['result_id' => 37, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '185.86621742091', 'rescomment' => null], - ['result_id' => 38, 'control_ref_id' => 1, 'test_ref_id' => 11, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '96.0370738667909', 'rescomment' => null], - ['result_id' => 39, 'control_ref_id' => 1, 'test_ref_id' => 12, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '7.26130670682815', 'rescomment' => null], - ['result_id' => 40, 'control_ref_id' => 2, 'test_ref_id' => 13, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '158.320085275538', 'rescomment' => null], - ['result_id' => 41, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '123.642030022172', 'rescomment' => null], - ['result_id' => 42, 'control_ref_id' => 2, 'test_ref_id' => 15, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '36.9505985250087', 'rescomment' => null], - ['result_id' => 43, 'control_ref_id' => 2, 'test_ref_id' => 16, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '96.5175241884342', 'rescomment' => null], - ['result_id' => 44, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '110.353601999312', 'rescomment' => null], - ['result_id' => 45, 'control_ref_id' => 2, 'test_ref_id' => 15, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '40.9599586941077', 'rescomment' => null], - ['result_id' => 46, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1528049849.3', 'rescomment' => null], - ['result_id' => 47, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '5.1', 'rescomment' => null], - ['result_id' => 48, 'control_ref_id' => 4, 'test_ref_id' => 13, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '215.383147793402', 'rescomment' => null], - ['result_id' => 49, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '183.063991935605', 'rescomment' => null], - ['result_id' => 50, 'control_ref_id' => 4, 'test_ref_id' => 15, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '52.9496711121656', 'rescomment' => null], - ['result_id' => 51, 'control_ref_id' => 4, 'test_ref_id' => 16, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '133.934963501757', 'rescomment' => null], - ['result_id' => 52, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '158.484164984758', 'rescomment' => null], - ['result_id' => 53, 'control_ref_id' => 4, 'test_ref_id' => 15, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '56.8818601408997', 'rescomment' => null], - ['result_id' => 54, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1518899850.3', 'rescomment' => null], - ['result_id' => 55, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '10.5', 'rescomment' => null], - ['result_id' => 56, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '10.5', 'rescomment' => null], - ['result_id' => 57, 'control_ref_id' => 6, 'test_ref_id' => 1, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '48.9562924866303', 'rescomment' => null], - ['result_id' => 58, 'control_ref_id' => 6, 'test_ref_id' => 2, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '52.1440402715625', 'rescomment' => null], - ['result_id' => 59, 'control_ref_id' => 6, 'test_ref_id' => 3, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '0.827525226237078', 'rescomment' => null], - ['result_id' => 60, 'control_ref_id' => 6, 'test_ref_id' => 4, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.43268122033837', 'rescomment' => null], - ['result_id' => 61, 'control_ref_id' => 6, 'test_ref_id' => 5, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '3.32791020761993', 'rescomment' => null], - ['result_id' => 62, 'control_ref_id' => 6, 'test_ref_id' => 6, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '20.1899916884103', 'rescomment' => null], - ['result_id' => 63, 'control_ref_id' => 6, 'test_ref_id' => 7, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '0.942164469720469', 'rescomment' => null], - ['result_id' => 64, 'control_ref_id' => 6, 'test_ref_id' => 8, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.92672863627875', 'rescomment' => null], - ['result_id' => 65, 'control_ref_id' => 6, 'test_ref_id' => 9, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '97.9752041498986', 'rescomment' => null], - ['result_id' => 66, 'control_ref_id' => 6, 'test_ref_id' => 10, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '82', 'rescomment' => null], - ['result_id' => 67, 'control_ref_id' => 6, 'test_ref_id' => 11, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '55.6067623790927', 'rescomment' => null], - ['result_id' => 68, 'control_ref_id' => 7, 'test_ref_id' => 18, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.3534129858017', 'rescomment' => null], - ['result_id' => 69, 'control_ref_id' => 8, 'test_ref_id' => 18, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '2.92454624176025', 'rescomment' => null], - ['result_id' => 70, 'control_ref_id' => 9, 'test_ref_id' => 1, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '41.9241811453099', 'rescomment' => null], - ['result_id' => 71, 'control_ref_id' => 9, 'test_ref_id' => 2, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '50.2606047539432', 'rescomment' => null], - ['result_id' => 72, 'control_ref_id' => 9, 'test_ref_id' => 3, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '0.618850628140718', 'rescomment' => null], - ['result_id' => 73, 'control_ref_id' => 9, 'test_ref_id' => 4, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.64659256568823', 'rescomment' => null], - ['result_id' => 74, 'control_ref_id' => 9, 'test_ref_id' => 5, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '3.37936528279952', 'rescomment' => null], - ['result_id' => 75, 'control_ref_id' => 9, 'test_ref_id' => 6, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '19.9687141929191', 'rescomment' => null], - ['result_id' => 76, 'control_ref_id' => 9, 'test_ref_id' => 7, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '1.25401465739322', 'rescomment' => null], - ['result_id' => 77, 'control_ref_id' => 9, 'test_ref_id' => 8, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '5.20068765776226', 'rescomment' => null], - ['result_id' => 78, 'control_ref_id' => 9, 'test_ref_id' => 9, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '85.8656412403935', 'rescomment' => null], - ['result_id' => 79, 'control_ref_id' => 9, 'test_ref_id' => 10, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '73.7679469620088', 'rescomment' => null], - ['result_id' => 80, 'control_ref_id' => 9, 'test_ref_id' => 11, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '32.6032920741649', 'rescomment' => null], - ['result_id' => 81, 'control_ref_id' => 9, 'test_ref_id' => 12, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '4.60016347657495', 'rescomment' => null], - ['result_id' => 82, 'control_ref_id' => 10, 'test_ref_id' => 19, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '3.07556349669703', 'rescomment' => null], - ['result_id' => 83, 'control_ref_id' => 11, 'test_ref_id' => 19, 'resdate' => '2022-01-03 00:00:00', 'resvalue' => '14.2416023954004', 'rescomment' => null], - ['result_id' => 84, 'control_ref_id' => 1, 'test_ref_id' => 1, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '151.987144896542', 'rescomment' => null], - ['result_id' => 85, 'control_ref_id' => 1, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '118.037436722317', 'rescomment' => null], - ['result_id' => 86, 'control_ref_id' => 1, 'test_ref_id' => 3, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '1.7947151556153', 'rescomment' => null], - ['result_id' => 87, 'control_ref_id' => 1, 'test_ref_id' => 4, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.73906612533412', 'rescomment' => null], - ['result_id' => 88, 'control_ref_id' => 1, 'test_ref_id' => 5, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.60361234521986', 'rescomment' => null], - ['result_id' => 89, 'control_ref_id' => 1, 'test_ref_id' => 6, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '72.6643107473908', 'rescomment' => null], - ['result_id' => 90, 'control_ref_id' => 1, 'test_ref_id' => 7, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.63980962604914', 'rescomment' => null], - ['result_id' => 91, 'control_ref_id' => 1, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '8.38141623646488', 'rescomment' => null], - ['result_id' => 92, 'control_ref_id' => 1, 'test_ref_id' => 9, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '264.978406417832', 'rescomment' => null], - ['result_id' => 93, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '181.555224534248', 'rescomment' => null], - ['result_id' => 94, 'control_ref_id' => 1, 'test_ref_id' => 11, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '96.2859292539928', 'rescomment' => null], - ['result_id' => 95, 'control_ref_id' => 1, 'test_ref_id' => 12, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '6.9194402361869', 'rescomment' => null], - ['result_id' => 96, 'control_ref_id' => 1, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '110.231586595837', 'rescomment' => null], - ['result_id' => 97, 'control_ref_id' => 1, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '7.83563261140536', 'rescomment' => null], - ['result_id' => 98, 'control_ref_id' => 2, 'test_ref_id' => 13, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '162.230018707428', 'rescomment' => null], - ['result_id' => 99, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '130.52137139783', 'rescomment' => null], - ['result_id' => 100, 'control_ref_id' => 2, 'test_ref_id' => 15, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '40.1447263115978', 'rescomment' => null], - ['result_id' => 101, 'control_ref_id' => 2, 'test_ref_id' => 16, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '101.152360213402', 'rescomment' => null], - ['result_id' => 102, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '102.256365254374', 'rescomment' => null], - ['result_id' => 103, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '104.629167708808', 'rescomment' => null], - ['result_id' => 104, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '82.6663002141555', 'rescomment' => null], - ['result_id' => 105, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '116.848292060971', 'rescomment' => null], - ['result_id' => 106, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '6', 'rescomment' => null], - ['result_id' => 107, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.9', 'rescomment' => null], - ['result_id' => 108, 'control_ref_id' => 4, 'test_ref_id' => 13, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '225.968857118997', 'rescomment' => null], - ['result_id' => 109, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '174.601513698878', 'rescomment' => null], - ['result_id' => 110, 'control_ref_id' => 4, 'test_ref_id' => 15, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '60.5069551717738', 'rescomment' => null], - ['result_id' => 111, 'control_ref_id' => 4, 'test_ref_id' => 16, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '139.447942846202', 'rescomment' => null], - ['result_id' => 112, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '143.31010007369', 'rescomment' => null], - ['result_id' => 113, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '144.761372351536', 'rescomment' => null], - ['result_id' => 114, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '115.071814150515', 'rescomment' => null], - ['result_id' => 115, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '161.317934555282', 'rescomment' => null], - ['result_id' => 116, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '10.2', 'rescomment' => null], - ['result_id' => 117, 'control_ref_id' => 6, 'test_ref_id' => 1, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '49.5516111762261', 'rescomment' => null], - ['result_id' => 118, 'control_ref_id' => 6, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '52.3007836764823', 'rescomment' => null], - ['result_id' => 119, 'control_ref_id' => 6, 'test_ref_id' => 3, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '0.826492230714472', 'rescomment' => null], - ['result_id' => 120, 'control_ref_id' => 6, 'test_ref_id' => 4, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '1.4488869704763', 'rescomment' => null], - ['result_id' => 121, 'control_ref_id' => 6, 'test_ref_id' => 5, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '3.44673661899837', 'rescomment' => null], - ['result_id' => 122, 'control_ref_id' => 6, 'test_ref_id' => 6, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '20.730181610828', 'rescomment' => null], - ['result_id' => 123, 'control_ref_id' => 6, 'test_ref_id' => 7, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '0.931063374396039', 'rescomment' => null], - ['result_id' => 124, 'control_ref_id' => 6, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.90811693843901', 'rescomment' => null], - ['result_id' => 125, 'control_ref_id' => 6, 'test_ref_id' => 9, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '101.930759918121', 'rescomment' => null], - ['result_id' => 126, 'control_ref_id' => 6, 'test_ref_id' => 10, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '75.5729386133723', 'rescomment' => null], - ['result_id' => 127, 'control_ref_id' => 6, 'test_ref_id' => 11, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '56.5900268982851', 'rescomment' => null], - ['result_id' => 128, 'control_ref_id' => 6, 'test_ref_id' => 10, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '102.528037735786', 'rescomment' => null], - ['result_id' => 129, 'control_ref_id' => 12, 'test_ref_id' => 20, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '51.0905355215073', 'rescomment' => null], - ['result_id' => 130, 'control_ref_id' => 12, 'test_ref_id' => 20, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '37.0758876204491', 'rescomment' => null], - ['result_id' => 131, 'control_ref_id' => 13, 'test_ref_id' => 20, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '104.115432500839', 'rescomment' => null], - ['result_id' => 132, 'control_ref_id' => 13, 'test_ref_id' => 20, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '126.745772361755', 'rescomment' => null], - ['result_id' => 133, 'control_ref_id' => 9, 'test_ref_id' => 1, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '44.2766951686621', 'rescomment' => null], - ['result_id' => 134, 'control_ref_id' => 9, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '52.3172542490699', 'rescomment' => null], - ['result_id' => 135, 'control_ref_id' => 9, 'test_ref_id' => 3, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '0.611564331171467', 'rescomment' => null], - ['result_id' => 136, 'control_ref_id' => 9, 'test_ref_id' => 4, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '1.61071374749995', 'rescomment' => null], - ['result_id' => 137, 'control_ref_id' => 9, 'test_ref_id' => 5, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '3.40677107339127', 'rescomment' => null], - ['result_id' => 138, 'control_ref_id' => 9, 'test_ref_id' => 6, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '20.3040445233696', 'rescomment' => null], - ['result_id' => 139, 'control_ref_id' => 9, 'test_ref_id' => 7, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '1.18843758036108', 'rescomment' => null], - ['result_id' => 140, 'control_ref_id' => 9, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '5.30604556512929', 'rescomment' => null], - ['result_id' => 141, 'control_ref_id' => 9, 'test_ref_id' => 9, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '89.4499417480113', 'rescomment' => null], - ['result_id' => 142, 'control_ref_id' => 9, 'test_ref_id' => 10, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '74.0960687372425', 'rescomment' => null], - ['result_id' => 143, 'control_ref_id' => 9, 'test_ref_id' => 11, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '32.4566384751244', 'rescomment' => null], - ['result_id' => 144, 'control_ref_id' => 9, 'test_ref_id' => 12, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.48437909776323', 'rescomment' => null], - ['result_id' => 145, 'control_ref_id' => 9, 'test_ref_id' => 2, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '50.9667682640676', 'rescomment' => null], - ['result_id' => 146, 'control_ref_id' => 9, 'test_ref_id' => 8, 'resdate' => '2022-01-04 00:00:00', 'resvalue' => '4.93310398630639', 'rescomment' => null], - ['result_id' => 147, 'control_ref_id' => 1, 'test_ref_id' => 1, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '145.49911151053', 'rescomment' => null], - ['result_id' => 148, 'control_ref_id' => 1, 'test_ref_id' => 2, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '113.383579655159', 'rescomment' => null], - ['result_id' => 149, 'control_ref_id' => 1, 'test_ref_id' => 3, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1.78951109759924', 'rescomment' => null], - ['result_id' => 150, 'control_ref_id' => 1, 'test_ref_id' => 4, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.93788942107251', 'rescomment' => null], - ['result_id' => 151, 'control_ref_id' => 1, 'test_ref_id' => 5, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.6838893149483', 'rescomment' => null], - ['result_id' => 152, 'control_ref_id' => 1, 'test_ref_id' => 6, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '71.2844487736166', 'rescomment' => null], - ['result_id' => 153, 'control_ref_id' => 1, 'test_ref_id' => 7, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.66921401448631', 'rescomment' => null], - ['result_id' => 154, 'control_ref_id' => 1, 'test_ref_id' => 8, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '7.75508570982025', 'rescomment' => null], - ['result_id' => 155, 'control_ref_id' => 1, 'test_ref_id' => 9, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '259.337398768362', 'rescomment' => null], - ['result_id' => 156, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '237.342516597661', 'rescomment' => null], - ['result_id' => 157, 'control_ref_id' => 1, 'test_ref_id' => 11, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '95.377689131424', 'rescomment' => null], - ['result_id' => 158, 'control_ref_id' => 1, 'test_ref_id' => 12, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '6.33840481787399', 'rescomment' => null], - ['result_id' => 159, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '254.058423430058', 'rescomment' => null], - ['result_id' => 160, 'control_ref_id' => 1, 'test_ref_id' => 12, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '7.61797516866643', 'rescomment' => null], - ['result_id' => 161, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '243.755676108857', 'rescomment' => null], - ['result_id' => 162, 'control_ref_id' => 1, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '193.063562956371', 'rescomment' => null], - ['result_id' => 163, 'control_ref_id' => 2, 'test_ref_id' => 13, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '156.243798489035', 'rescomment' => null], - ['result_id' => 164, 'control_ref_id' => 2, 'test_ref_id' => 14, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '119.907027927493', 'rescomment' => null], - ['result_id' => 165, 'control_ref_id' => 2, 'test_ref_id' => 15, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '40.447059699393', 'rescomment' => null], - ['result_id' => 166, 'control_ref_id' => 2, 'test_ref_id' => 16, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '102.231291242292', 'rescomment' => null], - ['result_id' => 167, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '5.3', 'rescomment' => null], - ['result_id' => 168, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1921499810', 'rescomment' => null], - ['result_id' => 169, 'control_ref_id' => 3, 'test_ref_id' => 17, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '5.1', 'rescomment' => null], - ['result_id' => 170, 'control_ref_id' => 4, 'test_ref_id' => 13, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '219.038828305261', 'rescomment' => null], - ['result_id' => 171, 'control_ref_id' => 4, 'test_ref_id' => 14, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '166.155678322747', 'rescomment' => null], - ['result_id' => 172, 'control_ref_id' => 4, 'test_ref_id' => 15, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '56.8925909121002', 'rescomment' => null], - ['result_id' => 173, 'control_ref_id' => 4, 'test_ref_id' => 16, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '140.823342734069', 'rescomment' => null], - ['result_id' => 174, 'control_ref_id' => 5, 'test_ref_id' => 17, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '10.2', 'rescomment' => null], - ['result_id' => 175, 'control_ref_id' => 6, 'test_ref_id' => 1, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '48.9587059015567', 'rescomment' => null], - ['result_id' => 176, 'control_ref_id' => 6, 'test_ref_id' => 2, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '49.1468267441428', 'rescomment' => null], - ['result_id' => 177, 'control_ref_id' => 6, 'test_ref_id' => 3, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '0.799690212662777', 'rescomment' => null], - ['result_id' => 178, 'control_ref_id' => 6, 'test_ref_id' => 4, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1.45552498470141', 'rescomment' => null], - ['result_id' => 179, 'control_ref_id' => 6, 'test_ref_id' => 5, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '3.41160793396446', 'rescomment' => null], - ['result_id' => 180, 'control_ref_id' => 6, 'test_ref_id' => 6, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '20.2910405834124', 'rescomment' => null], - ['result_id' => 181, 'control_ref_id' => 6, 'test_ref_id' => 7, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '0.94094934133914', 'rescomment' => null], - ['result_id' => 182, 'control_ref_id' => 6, 'test_ref_id' => 8, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.49152415745592', 'rescomment' => null], - ['result_id' => 183, 'control_ref_id' => 6, 'test_ref_id' => 9, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '100.024475839182', 'rescomment' => null], - ['result_id' => 184, 'control_ref_id' => 6, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '85.9679935713317', 'rescomment' => null], - ['result_id' => 185, 'control_ref_id' => 6, 'test_ref_id' => 11, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '53.8509856427683', 'rescomment' => null], - ['result_id' => 186, 'control_ref_id' => 9, 'test_ref_id' => 1, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '42.7806264740846', 'rescomment' => null], - ['result_id' => 187, 'control_ref_id' => 9, 'test_ref_id' => 2, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '51.9985050454933', 'rescomment' => null], - ['result_id' => 188, 'control_ref_id' => 9, 'test_ref_id' => 3, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '0.649795363795779', 'rescomment' => null], - ['result_id' => 189, 'control_ref_id' => 9, 'test_ref_id' => 4, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1.80260045112525', 'rescomment' => null], - ['result_id' => 190, 'control_ref_id' => 9, 'test_ref_id' => 5, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '3.35288773545917', 'rescomment' => null], - ['result_id' => 191, 'control_ref_id' => 9, 'test_ref_id' => 6, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '20.6904621653494', 'rescomment' => null], - ['result_id' => 192, 'control_ref_id' => 9, 'test_ref_id' => 7, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '1.19703451489628', 'rescomment' => null], - ['result_id' => 193, 'control_ref_id' => 9, 'test_ref_id' => 8, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.89184138396658', 'rescomment' => null], - ['result_id' => 194, 'control_ref_id' => 9, 'test_ref_id' => 9, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '91.9123811518993', 'rescomment' => null], - ['result_id' => 195, 'control_ref_id' => 9, 'test_ref_id' => 10, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '100.544617296457', 'rescomment' => null], - ['result_id' => 196, 'control_ref_id' => 9, 'test_ref_id' => 11, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '31.390065068111', 'rescomment' => null], - ['result_id' => 197, 'control_ref_id' => 9, 'test_ref_id' => 12, 'resdate' => '2022-01-05 00:00:00', 'resvalue' => '4.17808071506795', 'rescomment' => null], - ]; - $this->db->table('results')->insertBatch($data); - } -} diff --git a/app/Models/ControlModel.php b/app/Models/ControlModel.php deleted file mode 100644 index 3aec9bd..0000000 --- a/app/Models/ControlModel.php +++ /dev/null @@ -1,41 +0,0 @@ -where('dept_ref_id', $deptId)->findAll(); - } - - public function getWithDept() - { - $builder = $this->db->table('dict_controls c'); - $builder->select('c.*, d.name as dept_name'); - $builder->join('dict_depts d', 'd.dept_id = c.dept_ref_id', 'left'); - return $builder->get()->getResultArray(); - } - - public function getActiveByDate($date, $deptId = null) - { - $builder = $this->db->table('dict_controls c'); - $builder->select('c.*'); - $builder->where('c.expdate >=', $date); - if ($deptId) { - $builder->where('c.dept_ref_id', $deptId); - } - $builder->orderBy('c.name', 'ASC'); - return $builder->get()->getResultArray(); - } -} diff --git a/app/Models/ControlTestModel.php b/app/Models/ControlTestModel.php deleted file mode 100644 index 7ba7530..0000000 --- a/app/Models/ControlTestModel.php +++ /dev/null @@ -1,32 +0,0 @@ -db->table('control_tests ct'); - $builder->select('ct.*, t.name as test_name, t.unit'); - $builder->join('dict_tests t', 't.test_id = ct.test_ref_id'); - $builder->where('ct.control_ref_id', $controlId); - return $builder->get()->getResultArray(); - } - - public function getByControlAndTest($controlId, $testId) - { - return $this->where('control_ref_id', $controlId) - ->where('test_ref_id', $testId) - ->first(); - } -} diff --git a/app/Models/DailyResultModel.php b/app/Models/DailyResultModel.php deleted file mode 100644 index 323634e..0000000 --- a/app/Models/DailyResultModel.php +++ /dev/null @@ -1,61 +0,0 @@ -db->table('daily_result'); - $builder->select('*'); - $builder->where('control_ref_id', $controlId); - $builder->where('test_ref_id', $testId); - $builder->where('resdate >=', $startDate); - $builder->where('resdate <=', $endDate); - $builder->orderBy('resdate', 'ASC'); - return $builder->get()->getResultArray(); - } - - public function getByControlMonth($controlId, $yearMonth) - { - $startDate = $yearMonth . '-01'; - $endDate = $yearMonth . '-31'; - - $builder = $this->db->table('daily_result'); - $builder->select('*'); - $builder->where('control_ref_id', $controlId); - $builder->where('resdate >=', $startDate); - $builder->where('resdate <=', $endDate); - return $builder->get()->getResultArray(); - } - - public function saveResult($data) - { - $builder = $this->db->table('daily_result'); - $existing = $builder->select('*') - ->where('control_ref_id', $data['control_ref_id']) - ->where('test_ref_id', $data['test_ref_id']) - ->where('resdate', $data['resdate']) - ->get() - ->getRowArray(); - - if ($existing) { - return $builder->where('daily_result_id', $existing['daily_result_id'])->update($data); - } else { - return $builder->insert($data); - } - } -} diff --git a/app/Models/DeptModel.php b/app/Models/DeptModel.php deleted file mode 100644 index 02351b0..0000000 --- a/app/Models/DeptModel.php +++ /dev/null @@ -1,16 +0,0 @@ -where('dept_ref_id', $deptId)->findAll(); - } - - public function getWithDept($keyword = null, $deptId = null) - { - $builder = $this->db->table('dict_controls c'); - $builder->select('c.*, d.name as dept_name'); - $builder->join('dict_depts d', 'd.dept_id = c.dept_ref_id', 'left'); - - if ($keyword) { - $builder->groupStart(); - $builder->like('c.name', $keyword); - $builder->orLike('c.lot', $keyword); - $builder->groupEnd(); - } - - if ($deptId) { - $builder->where('c.dept_ref_id', $deptId); - } - - return $builder->get()->getResultArray(); - } - - public function getActiveByDate($date, $deptId = null) - { - $builder = $this->db->table('dict_controls c'); - $builder->select('c.*'); - $builder->where('c.expdate >=', $date); - if ($deptId) { - $builder->where('c.dept_ref_id', $deptId); - } - $builder->orderBy('c.name', 'ASC'); - return $builder->get()->getResultArray(); - } -} diff --git a/app/Models/DictDeptModel.php b/app/Models/DictDeptModel.php deleted file mode 100644 index d5bd067..0000000 --- a/app/Models/DictDeptModel.php +++ /dev/null @@ -1,16 +0,0 @@ -where('dept_ref_id', $deptId)->findAll(); - } - - public function getWithDept() - { - $builder = $this->db->table('dict_tests t'); - $builder->select('t.*, d.name as dept_name'); - $builder->join('dict_depts d', 'd.dept_id = t.dept_ref_id', 'left'); - return $builder->get()->getResultArray(); - } -} diff --git a/app/Models/Master/MasterControlsModel.php b/app/Models/Master/MasterControlsModel.php new file mode 100644 index 0000000..ba73971 --- /dev/null +++ b/app/Models/Master/MasterControlsModel.php @@ -0,0 +1,32 @@ +groupStart() + ->like('name', $keyword) + ->orLike('lot', $keyword) + ->groupEnd() + ->findAll(); + } + return $this->findAll(); + } +} diff --git a/app/Models/Master/MasterDeptsModel.php b/app/Models/Master/MasterDeptsModel.php new file mode 100644 index 0000000..5e2a300 --- /dev/null +++ b/app/Models/Master/MasterDeptsModel.php @@ -0,0 +1,27 @@ +groupStart() + ->like('name', $keyword) + ->groupEnd() + ->findAll(); + } + return $this->findAll(); + } +} diff --git a/app/Models/Master/MasterTestsModel.php b/app/Models/Master/MasterTestsModel.php new file mode 100644 index 0000000..e692759 --- /dev/null +++ b/app/Models/Master/MasterTestsModel.php @@ -0,0 +1,33 @@ +groupStart() + ->like('name', $keyword) + ->groupEnd() + ->findAll(); + } + return $this->findAll(); + } +} diff --git a/app/Models/MonthlyCommentModel.php b/app/Models/MonthlyCommentModel.php deleted file mode 100644 index cf8c2b1..0000000 --- a/app/Models/MonthlyCommentModel.php +++ /dev/null @@ -1,38 +0,0 @@ -where('control_ref_id', $controlId) - ->where('test_ref_id', $testId) - ->where('commonth', $yearMonth) - ->first(); - } - - public function saveComment($data) - { - $existing = $this->where('control_ref_id', $data['control_ref_id']) - ->where('test_ref_id', $data['test_ref_id']) - ->where('commonth', $data['commonth']) - ->first(); - - if ($existing) { - return $this->update($existing['monthly_comment_id'], $data); - } else { - return $this->insert($data); - } - } -} diff --git a/app/Models/Qc/ControlTestsModel.php b/app/Models/Qc/ControlTestsModel.php new file mode 100644 index 0000000..e15abdc --- /dev/null +++ b/app/Models/Qc/ControlTestsModel.php @@ -0,0 +1,30 @@ +groupStart() + ->like('mean', $keyword) + ->groupEnd() + ->findAll(); + } + return $this->findAll(); + } +} diff --git a/app/Models/Qc/ResultCommentsModel.php b/app/Models/Qc/ResultCommentsModel.php new file mode 100644 index 0000000..d602de0 --- /dev/null +++ b/app/Models/Qc/ResultCommentsModel.php @@ -0,0 +1,31 @@ +groupStart() + ->like('comment_month', $keyword) + ->orLike('com_text', $keyword) + ->groupEnd() + ->findAll(); + } + return $this->findAll(); + } +} diff --git a/app/Models/Qc/ResultsModel.php b/app/Models/Qc/ResultsModel.php new file mode 100644 index 0000000..50943aa --- /dev/null +++ b/app/Models/Qc/ResultsModel.php @@ -0,0 +1,31 @@ +groupStart() + ->like('res_value', $keyword) + ->groupEnd() + ->findAll(); + } + return $this->findAll(); + } +} diff --git a/app/Models/ResultCommentModel.php b/app/Models/ResultCommentModel.php deleted file mode 100644 index d4086be..0000000 --- a/app/Models/ResultCommentModel.php +++ /dev/null @@ -1,38 +0,0 @@ -where('control_ref_id', $controlId) - ->where('test_ref_id', $testId) - ->where('commonth', $yearMonth) - ->first(); - } - - public function saveComment($data) - { - $existing = $this->where('control_ref_id', $data['control_ref_id']) - ->where('test_ref_id', $data['test_ref_id']) - ->where('commonth', $data['commonth']) - ->first(); - - if ($existing) { - return $this->update($existing['result_comment_id'], $data); - } else { - return $this->insert($data); - } - } -} diff --git a/app/Models/ResultModel.php b/app/Models/ResultModel.php deleted file mode 100644 index 9bb70a9..0000000 --- a/app/Models/ResultModel.php +++ /dev/null @@ -1,72 +0,0 @@ -db->table('results'); - $builder->select('*'); - $builder->where('control_ref_id', $controlId); - $builder->where('test_ref_id', $testId); - $builder->where('resdate >=', $startDate); - $builder->where('resdate <=', $endDate); - $builder->orderBy('resdate', 'ASC'); - return $builder->get()->getResultArray(); - } - - public function getByControlMonth($controlId, $yearMonth) - { - $startDate = $yearMonth . '-01'; - $endDate = $yearMonth . '-31'; - - $builder = $this->db->table('results'); - $builder->select('*'); - $builder->where('control_ref_id', $controlId); - $builder->where('resdate >=', $startDate); - $builder->where('resdate <=', $endDate); - return $builder->get()->getResultArray(); - } - - public function saveResult($data) - { - $builder = $this->db->table('results'); - $existing = $builder->select('*') - ->where('control_ref_id', $data['control_ref_id']) - ->where('test_ref_id', $data['test_ref_id']) - ->where('resdate', $data['resdate']) - ->get() - ->getRowArray(); - - if ($existing) { - return $builder->where('result_id', $existing['result_id'])->update($data); - } else { - return $builder->insert($data); - } - } - - public function checkExisting($controlId, $testId, $resdate) - { - $builder = $this->db->table('results'); - return $builder->select('result_id') - ->where('control_ref_id', $controlId) - ->where('test_ref_id', $testId) - ->where('resdate', $resdate) - ->get() - ->getRowArray(); - } -} diff --git a/app/Models/TestModel.php b/app/Models/TestModel.php deleted file mode 100644 index 2897ab7..0000000 --- a/app/Models/TestModel.php +++ /dev/null @@ -1,29 +0,0 @@ -where('dept_ref_id', $deptId)->findAll(); - } - - public function getWithDept() - { - $builder = $this->db->table('dict_tests t'); - $builder->select('t.*, d.name as dept_name'); - $builder->join('dict_depts d', 'd.dept_id = t.dept_ref_id', 'left'); - return $builder->get()->getResultArray(); - } -} diff --git a/app/Views/control/dialog_form.php b/app/Views/control/dialog_form.php deleted file mode 100644 index 2ee4847..0000000 --- a/app/Views/control/dialog_form.php +++ /dev/null @@ -1,95 +0,0 @@ - -
-
- - -
-
- -
-

- -
- - -
-
-
-
- - -

-
-
- - -

-
-
- - -
-
- - -
-
- - -
-
- -
-
- -
-
-

Select tests to associate with this control

-
-
-
- - -
- - -
-
-
-
- diff --git a/app/Views/control/form.php b/app/Views/control/form.php deleted file mode 100644 index f9cd142..0000000 --- a/app/Views/control/form.php +++ /dev/null @@ -1,46 +0,0 @@ -
-
-

- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- Cancel - -
-
-
-
-
diff --git a/app/Views/control/index.php b/app/Views/control/index.php deleted file mode 100644 index 85cb763..0000000 --- a/app/Views/control/index.php +++ /dev/null @@ -1,261 +0,0 @@ -extend("layout/main_layout"); ?> -section("content") ?> -
- -
-
-

Control Dictionary

-

Manage control materials and lot numbers

-
- -
- - -
-
- - -
-
- - -
-
-
- - -
- - -
-
- - -
- - - - - - - - -
- - - include('control/dialog_form'); ?> -
-endSection(); ?> - -section("script") ?> - -endSection(); ?> - diff --git a/app/Views/dashboard.php b/app/Views/dashboard.php index 631534c..c30c5a9 100644 --- a/app/Views/dashboard.php +++ b/app/Views/dashboard.php @@ -1,142 +1,67 @@ extend("layout/main_layout"); ?> -section("content") ?> -
-
-
-
-
-
-

Total Tests

-

-
-
- -
-
-
- - - Active - - in library -
-
-
-
-
-

Total Controls

-

-
-
- -
-
-
- Reference - definitions -
-
+section("content"); ?> +
+
+
+

Dashboard

+

Quality Control Overview

+
+
-
-
-
-

Departments

-

-
-
- -
+
+
+
+
+

Total Controls

+

24

-
- Active - units +
+
- -
endSection(); ?> - -section("script") ?> - -endSection(); ?> - diff --git a/app/Views/dept/dialog_form.php b/app/Views/dept/dialog_form.php deleted file mode 100644 index fa96914..0000000 --- a/app/Views/dept/dialog_form.php +++ /dev/null @@ -1,60 +0,0 @@ - -
-
- - -
-
- -
-

- -
- - -
-
-
- - -

-
-
- - -
- - -
-
-
-
- diff --git a/app/Views/dept/index.php b/app/Views/dept/index.php deleted file mode 100644 index 54c9f27..0000000 --- a/app/Views/dept/index.php +++ /dev/null @@ -1,187 +0,0 @@ -extend("layout/main_layout"); ?> -section("content") ?> -
- -
-
-

Department Dictionary

-

Manage department entries

-
- -
- - -
-
- - -
-
- - -
- - - - - - - - -
- - - include('dept/dialog_form'); ?> -
-endSection(); ?> - -section("script") ?> - -endSection(); ?> - diff --git a/app/Views/entry/daily.php b/app/Views/entry/daily.php index c281a5e..7298629 100644 --- a/app/Views/entry/daily.php +++ b/app/Views/entry/daily.php @@ -1,256 +1,16 @@ extend("layout/main_layout"); ?> -section("content") ?> -
-
-
-
-

Daily Entry

-

Enter daily QC results

-
-
- - Press Ctrl+S to save -
-
-
-
- - -
+section("content"); ?> +
+
+
+

Daily Entry

+

Record daily QC test results

+
-
-
- - -

-
-
- - -

-
-
- - -

-
-
- - -

-
-
- -
-
-

Result

-
-
- - -

-
-
- - -
-
-
- -
-
-
+
+

Daily entry form coming soon...

endSection(); ?> - -section("script") ?> - -endSection(); ?> - diff --git a/app/Views/entry/index.php b/app/Views/entry/index.php new file mode 100644 index 0000000..3e71483 --- /dev/null +++ b/app/Views/entry/index.php @@ -0,0 +1,40 @@ +extend("layout/main_layout"); ?> + +section("content"); ?> +
+
+
+

QC Entry

+

Record quality control results

+
+
+ +
+
+ +
+
+ +
+
+

Daily Entry

+

Record daily QC test results

+
+
+
+ +
+
+
+ +
+
+

Monthly Review

+

Monthly QC analysis & comments

+
+
+
+
+
+
+endSection(); ?> diff --git a/app/Views/entry/monthly.php b/app/Views/entry/monthly.php deleted file mode 100644 index e2d2b56..0000000 --- a/app/Views/entry/monthly.php +++ /dev/null @@ -1,498 +0,0 @@ -extend("layout/main_layout"); ?> -section("content") ?> -
-
-
-
-

Monthly Entry

-

Enter monthly QC results

-
-
- - Press Ctrl+S to save -
-
- -
-
- - -
-
- -
-
- - -

-
-
- - -

-
-
- - -

-
-
- -
-
- -
- -
-
-
- -
-
-

- -

-
-
- - Weekday -
-
- - Weekend -
-
-
- - - - - -
-
-
- - -
-
-
- - -
- -
-
-
-
- -
- -

Select one or more tests above to view and enter monthly data

-
- -
- -

Select a control to view available tests

-
-
-
-endSection(); ?> - -section("script") ?> - -endSection(); ?> diff --git a/app/Views/layout/form_layout.php b/app/Views/layout/form_layout.php deleted file mode 100644 index fb6eed5..0000000 --- a/app/Views/layout/form_layout.php +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - <?= $page_title ?? 'QC Application' ?> - - - - - - - - - - - - - - - -
-
-
-

-
- renderSection("content"); ?> -
-
- - - renderSection("script"); ?> - - - diff --git a/app/Views/layout/main_layout.php b/app/Views/layout/main_layout.php index 4f5a0eb..f8961c9 100644 --- a/app/Views/layout/main_layout.php +++ b/app/Views/layout/main_layout.php @@ -1,252 +1,171 @@ - - + - <?= $page_title ?? 'QC Application' ?> - - - - - - + <?= $pageData['title'] ?? 'TinyQC - QC Management System' ?> + + + + - - - - - - - - - -
-
-
- Loading application... -
-
- -
- - - - -
-
-
-
- -

-
- -
- - - -
AD
+ + renderSection('content') ?> +
+ +
+ +
- -
-
- renderSection("content"); ?> -
-
+
- - - - + - renderSection("script"); ?> + + renderSection('script') ?> - diff --git a/app/Views/master/control/dialog_control_form.php b/app/Views/master/control/dialog_control_form.php new file mode 100644 index 0000000..80a58ba --- /dev/null +++ b/app/Views/master/control/dialog_control_form.php @@ -0,0 +1,83 @@ + + + + diff --git a/app/Views/master/control/index.php b/app/Views/master/control/index.php new file mode 100644 index 0000000..20dc3aa --- /dev/null +++ b/app/Views/master/control/index.php @@ -0,0 +1,242 @@ +extend("layout/main_layout"); ?> + +section("content"); ?> +
+
+
+

Controls

+

Manage QC control standards

+
+ +
+ +
+
+
+ + +
+ +
+
+ +
+ + + + + +
+ + include('master/control/dialog_control_form'); ?> +
+endSection(); ?> + +section("script"); ?> + +endSection(); ?> diff --git a/app/Views/master/dept/dialog_dept_form.php b/app/Views/master/dept/dialog_dept_form.php new file mode 100644 index 0000000..a619dc7 --- /dev/null +++ b/app/Views/master/dept/dialog_dept_form.php @@ -0,0 +1,44 @@ + + + + diff --git a/app/Views/master/dept/index.php b/app/Views/master/dept/index.php new file mode 100644 index 0000000..d5e8fa1 --- /dev/null +++ b/app/Views/master/dept/index.php @@ -0,0 +1,231 @@ +extend("layout/main_layout"); ?> + +section("content"); ?> +
+
+
+

Departments

+

Manage laboratory departments

+
+ +
+ +
+
+
+ + +
+ +
+
+ +
+ + + + + +
+ + include('master/dept/dialog_dept_form'); ?> +
+endSection(); ?> + +section("script"); ?> + +endSection(); ?> diff --git a/app/Views/master/test/dialog_test_form.php b/app/Views/master/test/dialog_test_form.php new file mode 100644 index 0000000..5739f05 --- /dev/null +++ b/app/Views/master/test/dialog_test_form.php @@ -0,0 +1,113 @@ + + + + diff --git a/app/Views/master/test/index.php b/app/Views/master/test/index.php new file mode 100644 index 0000000..da4d6fa --- /dev/null +++ b/app/Views/master/test/index.php @@ -0,0 +1,240 @@ +extend("layout/main_layout"); ?> + +section("content"); ?> +
+
+
+

Tests

+

Manage laboratory tests and parameters

+
+ +
+ +
+
+
+ + +
+ +
+
+ +
+ + + + + +
+ + include('master/test/dialog_test_form'); ?> +
+endSection(); ?> + +section("script"); ?> + +endSection(); ?> diff --git a/app/Views/report/index.php b/app/Views/report/index.php index a375afa..0dccdd8 100644 --- a/app/Views/report/index.php +++ b/app/Views/report/index.php @@ -1,141 +1,16 @@ extend("layout/main_layout"); ?> -section("content") ?> -
-
-
-
-

Generate Report

-

Select parameters to generate QC report

-
-
-
- - -
-
- -
-
-
- - -

-
- -
- - -

-
- -
-
- - -

-
-
- - -
-
- - -
-
- -
- -
-
-
+section("content"); ?> +
+
+
+

Reports

+

Generate and view QC reports

+ +
+

Reports module coming soon...

+
endSection(); ?> - -section("script") ?> - -endSection(); ?> - diff --git a/app/Views/report/view.php b/app/Views/report/view.php index 13ee2ae..2d72b39 100644 --- a/app/Views/report/view.php +++ b/app/Views/report/view.php @@ -1,286 +1,16 @@ extend("layout/main_layout"); ?> -section("content") ?> -
-
-

QC Report -

-
- + +section("content"); ?> +
+
+
+

Report View

+

View detailed QC report

- 1): ?> -
-

QC Trend Overview

-
-
- -
-
- -
-
+
+

Report viewer coming soon...

- - - $data): ?> -
-
-
-

(Lot: )

-

-
-
- - Out of Range - -
-
- - - -
-
-
-

Mean

-

-
-
-

SD

-

-
-
-

+2SD

-

-
-
-

-2SD

-

-
-
- -
-

Trend Chart

-
- -
-
- -
- - - - - - - - - - - 0) ? ($value - $mean) / $sd : null; - $status = $zScore !== null ? (abs($zScore) > 2 ? 'Out' : (abs($zScore) > 1 ? 'Warn' : 'OK')) : '-'; - $statusClass = $status == 'Out' ? 'text-red-600 font-bold bg-red-50' : ($status == 'Warn' ? 'text-yellow-600 bg-yellow-50' : 'text-green-600'); - ?> - - - - - - - - -
DayValueZ-ScoreStatus
-
- - -
-

Monthly Comment:

-

-
- -
- -
- No test data found for this control. -
- -
- -
+
endSection(); ?> - -section("script") ?> - - - -endSection(); ?> - diff --git a/app/Views/test/dialog_form.php b/app/Views/test/dialog_form.php deleted file mode 100644 index 57922cb..0000000 --- a/app/Views/test/dialog_form.php +++ /dev/null @@ -1,89 +0,0 @@ - -
-
- - -
-
- -
-

- -
- - -
-
-
-
- - -

-
-
- - -

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
- - -
-
-
-
- diff --git a/app/Views/test/index.php b/app/Views/test/index.php deleted file mode 100644 index 4e84ed8..0000000 --- a/app/Views/test/index.php +++ /dev/null @@ -1,231 +0,0 @@ -extend("layout/main_layout"); ?> -section("content") ?> -
- -
-
-

Test Dictionary

-

Manage test types, methods, and units

-
- -
- - -
-
- - -
-
- - -
-
-
- - -
- -
-
- - -
- - - - - - - - -
- - - include('test/dialog_form'); ?> -
-endSection(); ?> - -section("script") ?> - -endSection(); ?> - diff --git a/backup/qc20260114.bak b/backup/qc20260114.bak new file mode 100644 index 0000000..04c5f37 Binary files /dev/null and b/backup/qc20260114.bak differ diff --git a/backup/script.sql b/backup/script.sql new file mode 100644 index 0000000..99aeb89 Binary files /dev/null and b/backup/script.sql differ diff --git a/backup/script_utf8.sql b/backup/script_utf8.sql new file mode 100644 index 0000000..903d0c1 --- /dev/null +++ b/backup/script_utf8.sql @@ -0,0 +1,170 @@ +USE [master] +GO +/****** Object: Database [cmod_qc] Script Date: 17/01/2026 18:26:32 ******/ +CREATE DATABASE [cmod_qc] + CONTAINMENT = NONE + ON PRIMARY +( NAME = N'cmod_qc', FILENAME = N'C:\db\cmod_qc.mdf' , SIZE = 8192KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB ) + LOG ON +( NAME = N'cmod_qc_log', FILENAME = N'C:\db\cmod_qc_log.ldf' , SIZE = 32448KB , MAXSIZE = 2048GB , FILEGROWTH = 10%) + WITH CATALOG_COLLATION = DATABASE_DEFAULT, LEDGER = OFF +GO +ALTER DATABASE [cmod_qc] SET COMPATIBILITY_LEVEL = 110 +GO +IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled')) +begin +EXEC [cmod_qc].[dbo].[sp_fulltext_database] @action = 'enable' +end +GO +ALTER DATABASE [cmod_qc] SET ANSI_NULL_DEFAULT OFF +GO +ALTER DATABASE [cmod_qc] SET ANSI_NULLS OFF +GO +ALTER DATABASE [cmod_qc] SET ANSI_PADDING OFF +GO +ALTER DATABASE [cmod_qc] SET ANSI_WARNINGS OFF +GO +ALTER DATABASE [cmod_qc] SET ARITHABORT OFF +GO +ALTER DATABASE [cmod_qc] SET AUTO_CLOSE OFF +GO +ALTER DATABASE [cmod_qc] SET AUTO_SHRINK OFF +GO +ALTER DATABASE [cmod_qc] SET AUTO_UPDATE_STATISTICS ON +GO +ALTER DATABASE [cmod_qc] SET CURSOR_CLOSE_ON_COMMIT OFF +GO +ALTER DATABASE [cmod_qc] SET CURSOR_DEFAULT GLOBAL +GO +ALTER DATABASE [cmod_qc] SET CONCAT_NULL_YIELDS_NULL OFF +GO +ALTER DATABASE [cmod_qc] SET NUMERIC_ROUNDABORT OFF +GO +ALTER DATABASE [cmod_qc] SET QUOTED_IDENTIFIER OFF +GO +ALTER DATABASE [cmod_qc] SET RECURSIVE_TRIGGERS OFF +GO +ALTER DATABASE [cmod_qc] SET DISABLE_BROKER +GO +ALTER DATABASE [cmod_qc] SET AUTO_UPDATE_STATISTICS_ASYNC OFF +GO +ALTER DATABASE [cmod_qc] SET DATE_CORRELATION_OPTIMIZATION OFF +GO +ALTER DATABASE [cmod_qc] SET TRUSTWORTHY OFF +GO +ALTER DATABASE [cmod_qc] SET ALLOW_SNAPSHOT_ISOLATION OFF +GO +ALTER DATABASE [cmod_qc] SET PARAMETERIZATION SIMPLE +GO +ALTER DATABASE [cmod_qc] SET READ_COMMITTED_SNAPSHOT OFF +GO +ALTER DATABASE [cmod_qc] SET HONOR_BROKER_PRIORITY OFF +GO +ALTER DATABASE [cmod_qc] SET RECOVERY SIMPLE +GO +ALTER DATABASE [cmod_qc] SET MULTI_USER +GO +ALTER DATABASE [cmod_qc] SET PAGE_VERIFY CHECKSUM +GO +ALTER DATABASE [cmod_qc] SET DB_CHAINING OFF +GO +ALTER DATABASE [cmod_qc] SET FILESTREAM( NON_TRANSACTED_ACCESS = OFF ) +GO +ALTER DATABASE [cmod_qc] SET TARGET_RECOVERY_TIME = 0 SECONDS +GO +ALTER DATABASE [cmod_qc] SET DELAYED_DURABILITY = DISABLED +GO +ALTER DATABASE [cmod_qc] SET ACCELERATED_DATABASE_RECOVERY = OFF +GO +ALTER DATABASE [cmod_qc] SET QUERY_STORE = OFF +GO +USE [cmod_qc] +GO +/****** Object: Table [dbo].[CONTROL_TEST] Script Date: 17/01/2026 18:26:33 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[CONTROL_TEST]( + [id] [int] IDENTITY(1,1) NOT NULL, + [controlid] [int] NULL, + [testid] [int] NULL, + [mean] [float] NULL, + [sd] [float] NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[DAILY_RESULT] Script Date: 17/01/2026 18:26:33 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[DAILY_RESULT]( + [id] [int] IDENTITY(1,1) NOT NULL, + [controlid] [int] NULL, + [testid] [int] NULL, + [resdate] [datetime] NULL, + [resvalue] [varchar](50) NULL, + [rescomment] [text] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[DICT_CONTROL] Script Date: 17/01/2026 18:26:33 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[DICT_CONTROL]( + [id] [int] IDENTITY(1,1) NOT NULL, + [deptid] [int] NULL, + [name] [varchar](50) NULL, + [lot] [varchar](50) NULL, + [producer] [text] NULL, + [expdate] [date] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +/****** Object: Table [dbo].[DICT_DEPT] Script Date: 17/01/2026 18:26:33 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[DICT_DEPT]( + [id] [int] IDENTITY(1,1) NOT NULL, + [name] [varchar](50) NULL, + CONSTRAINT [PK_DICT_DEPT] PRIMARY KEY CLUSTERED +( + [id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[DICT_TEST] Script Date: 17/01/2026 18:26:33 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[DICT_TEST]( + [id] [int] IDENTITY(1,1) NOT NULL, + [deptid] [int] NULL, + [name] [varchar](50) NULL, + [unit] [varchar](50) NULL, + [method] [varchar](50) NULL, + [cva] [varchar](50) NULL, + [ba] [varchar](50) NULL, + [tea] [varchar](50) NULL +) ON [PRIMARY] +GO +/****** Object: Table [dbo].[MONTHLY_COMMENT] Script Date: 17/01/2026 18:26:33 ******/ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +CREATE TABLE [dbo].[MONTHLY_COMMENT]( + [id] [int] IDENTITY(1,1) NOT NULL, + [controlid] [int] NOT NULL, + [testid] [int] NOT NULL, + [commonth] [varchar](7) NOT NULL, + [comtext] [text] NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +USE [master] +GO +ALTER DATABASE [cmod_qc] SET READ_WRITE +GO diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 0000000..769583b --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,1009 @@ +# TinyQC Product Requirements Document (PRD) + +## 1. Executive Summary + +### 1.1 Product Overview +**TinyQC** is a lightweight, web-based Laboratory Quality Control (QC) Management System designed to help laboratories record, track, and analyze quality control test results with statistical analysis capabilities. + +### 1.2 Product Vision +Provide a simple yet powerful QC management solution that enables laboratory staff to efficiently manage dictionary data (departments, tests, control materials), enter QC results, and generate statistical reports—all through an intuitive web interface. + +### 1.3 Target Users +- Laboratory technicians responsible for daily data entry +- QC managers overseeing quality control processes +- Laboratory directors managing department and test configurations + +### 1.4 Problem Statement +Laboratories need a centralized system to: +- Maintain organized dictionaries for departments, tests, and control materials +- Record QC test results efficiently (daily and monthly entry modes) +- Calculate statistical parameters (mean, standard deviation) +- Identify out-of-range results that require attention +- Generate reports for quality assurance purposes + +### 1.5 Scope (MVP) +The MVP includes core dictionary management, data entry functionality, and basic reporting capabilities. Future phases will add authentication, user management, advanced analytics, and export features. + +--- + +## 2. User Personas + +### 2.1 Laboratory Technician +**Role:** Day-to-day data entry and result recording + +**Goals:** +- Quickly enter QC test results for the current day +- Perform bulk monthly data entry using an intuitive calendar interface +- Add comments and notes to explain unusual results +- Search and filter existing results when needed + +**Pain Points:** +- Manual paper-based recording is error-prone +- No automatic statistical calculation +- Difficulty tracking which controls expire soon + +**How TinyQC Helps:** +- Digital data entry with validation +- Automatic mean and SD calculations +- Expiry date tracking for control materials + +--- + +### 2.2 QC Manager +**Role:** Oversight of quality control processes and reporting + +**Goals:** +- Review QC data across multiple departments and tests +- Generate monthly reports for quality assurance documentation +- Identify out-of-range results (beyond 2 standard deviations) +- Maintain accurate dictionaries of tests and control materials + +**Pain Points:** +- Manual compilation of reports from multiple sources +- Difficulty identifying trends in QC data +- No centralized view of control materials across departments + +**How TinyQC Helps:** +- Consolidated reporting by test and control +- Automatic statistical analysis with out-of-range detection +- Centralized dictionary management with relationships + +--- + +### 2.3 Laboratory Director +**Role:** Administrative oversight and system configuration + +**Goals:** +- Manage department structure (add, edit, delete departments) +- Define test catalog with QC parameters +- Configure control materials with lot numbers and expiry dates +- Link tests to appropriate controls with statistical parameters + +**Pain Points:** +- No systematic way to maintain test definitions +- Manual tracking of control material lots and producers +- Difficulty ensuring consistency in QC parameters + +**How TinyQC Helps:** +- Complete CRUD operations for departments, tests, and controls +- Control-test linkage with configurable mean and SD values +- Soft delete functionality to preserve historical data + +--- + +## 3. Functional Requirements + +### 3.1 Department Management + +#### 3.1.1 View Departments + +**User Story:** As a user, I want to view all departments so I can understand the organizational structure of the laboratory. + +### US-001: View Departments + +**As a** user +**I want** to view all departments +**So that** I can understand the organizational structure of the laboratory + +#### Acceptance Criteria +- [ ] Display department ID and name +- [ ] Show total count of tests and controls per department +- [ ] Allow sorting by name or ID +- [ ] Provide search functionality by department name + +--- + +#### 3.1.2 Create Department + +**User Story:** As an administrator, I want to create new departments so I can organize tests and controls by laboratory section. + +### US-002: Create Department + +**As an** administrator +**I want** to create new departments +**So that** I can organize tests and controls by laboratory section + +#### Acceptance Criteria +- [ ] Provide input field for department name +- [ ] Validate that name is required and unique +- [ ] Show success notification upon creation +- [ ] Redirect to department list or stay on form after creation + +--- + +#### 3.1.3 Edit Department + +**User Story:** As an administrator, I want to edit department names so I can correct typos or reflect organizational changes. + +### US-003: Edit Department + +**As an** administrator +**I want** to edit department names +**So that** I can correct typos or reflect organizational changes + +#### Acceptance Criteria +- [ ] Pre-populate form with current department name +- [ ] Validate name is required and unique (excluding current record) +- [ ] Track creation and modification timestamps +- [ ] Show success notification upon update + +--- + +#### 3.1.4 Delete Department + +**User Story:** As an administrator, I want to remove obsolete departments so the system remains accurate and current. + +### US-004: Delete Department + +**As an** administrator +**I want** to remove obsolete departments +**So that** the system remains accurate and current + +#### Acceptance Criteria +- [ ] Display confirmation dialog before deletion +- [ ] Check for related records (tests, controls) and handle cascade or restrict +- [ ] Set deleted_at timestamp instead of permanent deletion +- [ ] Provide option to restore deleted departments + +--- + +### 3.2 Test Management + +#### 3.2.1 View Tests + +**User Story:** As a user, I want to browse all tests so I can understand what tests are available for QC entry. + +### US-005: View Tests + +**As a** user +**I want** to browse all tests +**So that** I can understand what tests are available for QC entry + +#### Acceptance Criteria +- [ ] Display test ID, name, unit, method, and associated department +- [ ] Show QC parameters: CVA (Control Value Average), BA (Base Average), TEA (Total Error Allowance) +- [ ] Display links to related controls +- [ ] Provide search functionality by test name or code +- [ ] Allow filtering by department + +--- + +#### 3.2.2 Create Test + +**User Story:** As an administrator, I want to add new tests with their QC parameters so they become available for QC data entry. + +### US-006: Create Test + +**As an** administrator +**I want** to add new tests with their QC parameters +**So that** they become available for QC data entry + +#### Acceptance Criteria +- [ ] Provide fields: test name, unit, method, CVA, BA, TEA, department +- [ ] Validate that name and unit are required +- [ ] Enable department selection via dropdown +- [ ] Set automatic timestamps on creation +- [ ] Show success notification upon creation + +--- + +#### 3.2.3 Edit Test + +**User Story:** As an administrator, I want to update test parameters so QC targets remain accurate. + +### US-007: Edit Test + +**As an** administrator +**I want** to update test parameters +**So that** QC targets remain accurate + +#### Acceptance Criteria +- [ ] Allow editing all fields except ID +- [ ] Preserve historical data associations +- [ ] Update modification timestamp +- [ ] Apply validation rules to edited fields + +--- + +#### 3.2.4 Delete Test + +**User Story:** As an administrator, I want to remove obsolete tests so they don't appear in data entry options. + +### US-008: Delete Test + +**As an** administrator +**I want** to remove obsolete tests +**So that** they don't appear in data entry options + +#### Acceptance Criteria +- [ ] Display confirmation dialog before deletion +- [ ] Check for associated control links before deletion +- [ ] Preserve historical QC results +- [ ] Set deleted_at timestamp + +--- + +### 3.3 Control Management + +#### 3.3.1 View Controls + +**User Story:** As a user, I want to view all available controls so I can select the correct one for data entry. + +### US-009: View Controls + +**As a** user +**I want** to view all available controls +**So that** I can select the correct one for data entry + +#### Acceptance Criteria +- [ ] Display control ID, name, lot number, producer, expiry date +- [ ] Show associated department +- [ ] Indicate if control is active (based on expiry date) +- [ ] Provide search functionality +- [ ] Allow filtering by department + +--- + +#### 3.3.2 Create Control + +**User Story:** As an administrator, I want to register new control materials with their test associations so technicians can select them during data entry. + +### US-010: Create Control + +**As an** administrator +**I want** to register new control materials with their test associations +**So that** technicians can select them during data entry + +#### Acceptance Criteria +- [ ] Provide fields: name, lot number, producer, expiry date, department +- [ ] Enable linking to one or more tests +- [ ] Allow setting mean and SD values for each test association +- [ ] Validate that name, lot, and expiry date are required +- [ ] Set automatic timestamps on creation + +--- + +#### 3.3.3 Edit Control + +**User Story:** As an administrator, I want to update control information when lot numbers change or new test associations are needed. + +### US-011: Edit Control + +**As an** administrator +**I want** to update control information +**So that** lot numbers remain current and test associations are accurate + +#### Acceptance Criteria +- [ ] Allow editing all fields except ID +- [ ] Enable updating test associations and their parameters +- [ ] Update modification timestamp +- [ ] Apply validation rules to edited fields + +--- + +#### 3.3.4 Delete Control + +**User Story:** As an administrator, I want to remove discontinued controls so they don't appear in selection lists. + +### US-012: Delete Control + +**As an** administrator +**I want** to remove discontinued controls +**So that** they don't appear in selection lists + +#### Acceptance Criteria +- [ ] Display confirmation dialog before deletion +- [ ] Preserve associated QC results +- [ ] Set deleted_at timestamp + +--- + +### 3.4 Data Entry - Daily + +#### 3.4.1 Daily Entry Interface + +**User Story:** As a technician, I want to enter daily QC results quickly so I can maintain accurate quality records. + +### US-013: Daily Entry Interface + +**As a** technician +**I want** to enter daily QC results quickly +**So that** I can maintain accurate quality records + +#### Acceptance Criteria +- [ ] Display date picker (defaults to current date) +- [ ] Provide department filter +- [ ] Show controls active for selected date +- [ ] Display tests associated with each control +- [ ] Provide input field for numeric result value +- [ ] Include optional comment field for notes +- [ ] Display save button with confirmation + +--- + +#### 3.4.2 Save Daily Result + +**User Story:** As a technician, I want my entered results saved so they contribute to statistical analysis. + +### US-014: Save Daily Result + +**As a** technician +**I want** my entered results saved +**So that** they contribute to statistical analysis + +#### Acceptance Criteria +- [ ] Validate result is numeric +- [ ] Associate result with control, test, and date +- [ ] Store optional comment +- [ ] Return appropriate success/error response +- [ ] Clear form after successful save + +--- + +### 3.5 Data Entry - Monthly + +#### 3.5.1 Monthly Entry Interface + +**User Story:** As a technician, I want to enter a full month of results efficiently using a calendar grid. + +### US-015: Monthly Entry Interface + +**As a** technician +**I want** to enter a full month of results efficiently using a calendar grid +**So that** I can complete monthly data entry quickly + +#### Acceptance Criteria +- [ ] Provide month and year selector +- [ ] Display department filter +- [ ] Show calendar grid view of the month +- [ ] Display controls with their associated tests +- [ ] Provide input fields for each day of the month +- [ ] Highlight weekends and holidays +- [ ] Enable horizontal scrolling for multiple controls/tests + +--- + +#### 3.5.2 Save Monthly Results + +**User Story:** As a technician, I want to save multiple results at once so I don't have to save each one individually. + +### US-016: Save Monthly Results + +**As a** technician +**I want** to save multiple results at once +**So that** I don't have to save each one individually + +#### Acceptance Criteria +- [ ] Accept array of results with dates and values +- [ ] Validate all entries before saving +- [ ] Support partial save with error reporting if any entry fails +- [ ] Show success notification + +--- + +#### 3.5.3 Monthly Comments + +**User Story:** As a technician, I want to add monthly notes so I can explain batch-wide observations or issues. + +### US-017: Monthly Comments + +**As a** technician +**I want** to add monthly notes +**So that** I can explain batch-wide observations or issues + +#### Acceptance Criteria +- [ ] Enable selecting month, control, and test +- [ ] Provide text area for comment +- [ ] Associate comment with month rather than specific date +- [ ] Display comments in reports + +--- + +### 3.6 Reporting + +#### 3.6.1 Report Generation Form + +**User Story:** As a QC manager, I want to configure report parameters so I can generate targeted quality reports. + +### US-018: Report Generation Form + +**As a** QC manager +**I want** to configure report parameters +**So that** I can generate targeted quality reports + +#### Acceptance Criteria +- [ ] Provide month and year selector +- [ ] Display department filter +- [ ] Enable test selection (single or all) +- [ ] Allow control selection (up to 3) +- [ ] Display generate report button +- [ ] Provide preview mode before printing/export + +--- + +#### 3.6.2 Report Display + +**User Story:** As a QC manager, I want to view statistical analysis of QC results so I can assess quality performance. + +### US-019: Report Display + +**As a** QC manager +**I want** to view statistical analysis of QC results +**So that** I can assess quality performance + +#### Acceptance Criteria +- [ ] Display control name, lot, and expiry +- [ ] Show test name and unit +- [ ] Present table of daily result values +- [ ] Calculate and display mean (average of all values) +- [ ] Calculate and display standard deviation (SD) +- [ ] Display number of results +- [ ] Show out-of-range count (beyond 2 SD from mean) +- [ ] Highlight out-of-range values +- [ ] Include monthly comments if present +- [ ] Provide responsive layout for screen viewing + +--- + +#### 3.6.3 Out-of-Range Detection + +**User Story:** As a QC manager, I want to quickly identify results requiring investigation so I can take corrective action. + +### US-020: Out-of-Range Detection + +**As a** QC manager +**I want** to quickly identify results requiring investigation +**So that** I can take corrective action + +#### Acceptance Criteria +- [ ] Calculate mean and SD from entered values +- [ ] Define limits as mean ± 2 SD +- [ ] Flag values outside these limits +- [ ] Count and display number of out-of-range results +- [ ] Highlight individual out-of-range cells in report + +--- + +## 4. Technical Requirements + +### 4.1 Technology Stack + +| Layer | Technology | Version | +|-------|------------|---------| +| Backend Framework | CodeIgniter 4 | 4.x | +| PHP Version | PHP | 8.1+ | +| Database | MySQL/MariaDB | 5.7+ | +| Frontend Framework | Alpine.js | 3.x | +| Styling | TailwindCSS | 3.x | +| UI Components | DaisyUI | Latest | +| Icons | FontAwesome | 6.5.1 | +| Charts | Chart.js | Latest | +| Notifications | Toastify-js | Latest | +| Testing | PHPUnit | Latest | + +### 4.2 Architecture + +#### 4.2.1 Backend Architecture +- **Pattern:** MVC (Model-View-Controller) +- **API Style:** RESTful JSON endpoints +- **Input Validation:** CodeIgniter validation library +- **Error Handling:** Exception catching with standardized error responses +- **Case Conversion:** Automatic camelCase/snake_case conversion for API + +#### 4.2.2 Frontend Architecture +- **SPA-like experience:** Alpine.js for reactivity +- **Module-based:** ES6 modules with import statements +- **State Management:** Component-level Alpine data objects +- **AJAX:** Fetch API for all server communication + +### 4.3 Database Requirements + +#### 4.3.1 Supported Databases +- **Primary:** MySQL 5.7+ / MariaDB 10+ +- **Secondary:** Microsoft SQL Server (via CodeIgniter drivers) + +#### 4.3.2 Naming Conventions +- **Tables:** snake_case (e.g., `master_depts`, `master_tests`) +- **Columns:** snake_case (e.g., `dept_id`, `created_at`) +- **Primary Keys:** `{table_singular}_id` (e.g., `dept_id`, `test_id`) +- **Foreign Keys:** Match referenced primary key name + +#### 4.3.3 Required Fields (All Tables) +- `created_at` DATETIME +- `updated_at` DATETIME +- `deleted_at` DATETIME (for soft deletes) + +### 4.4 API Requirements + +#### 4.4.1 Response Format +All API responses must follow this structure: + +```json +{ + "status": "success" | "error", + "message": "Human-readable message", + "data": { } | [ ] +} +``` + +#### 4.4.2 HTTP Status Codes +- `200` - Success +- `201` - Created +- `400` - Bad Request / Validation Error +- `401` - Unauthorized +- `404` - Not Found +- `500` - Server Error + +#### 4.4.3 Case Conversion +- **Input (Frontend → Backend):** Convert camelCase to snake_case +- **Output (Backend → Frontend):** Convert snake_case to camelCase +- Helper functions: `camel_to_snake()`, `snake_to_camel()`, `camel_to_snake_array()` + +### 4.5 Security Requirements + +#### 4.5.1 Input Validation +- All user inputs must be validated before processing +- Numeric fields must reject non-numeric values +- Date fields must use proper date format +- Required fields must be enforced + +#### 4.5.2 SQL Injection Prevention +- Use CodeIgniter's Query Builder for all database operations +- Never concatenate user input into SQL queries +- Use parameterized queries + +#### 4.5.3 XSS Prevention +- Escape output in views using CodeIgniter's esc() function +- Sanitize user-generated content before display + +#### 4.5.4 CSRF Protection +- Enable CodeIgniter's CSRF protection +- Validate CSRF token on all POST/PATCH/DELETE requests + +### 4.6 Performance Requirements + +#### 4.6.1 Response Times +- Page load: < 2 seconds +- API responses: < 500 milliseconds +- Database queries: < 200 milliseconds + +#### 4.6.2 Scalability +- Support up to 100 concurrent users +- Handle up to 10,000 result records +- Pagination for large result sets + +--- + +## 5. Data Model + +### 5.1 Entity Relationship Overview + +``` +┌─────────────────┐ ┌─────────────────┐ +│ master_depts │ │ master_tests │ +│ (1)──────────(N) │ │ +│ │ │ test_id │ +│ dept_id │ │ name │ +│ name │ │ unit │ +│ created_at │ │ method │ +│ updated_at │◄──────│ cva │ +│ deleted_at │ dept_ │ ba │ +└─────────────────┘ ref_id │ tea │ + │ dept_ref_id │ + │ created_at │ + │ updated_at │ + │ deleted_at │ + └─────────────────┘ + ┌─────────────────┐ ┌─────────────────┐ + │ master_controls │ │ control_tests │ + │ │ │ │ + │ control_id │ │ control_test_id │ + │ name │(1)───(N)│ control_ref_id │ + │ lot │ │ test_ref_id │ + │ producer │ │ mean │ + │ expiry_date │ │ sd │ + │ dept_ref_id │ │ created_at │ + │ created_at │ │ updated_at │ + │ updated_at │ │ deleted_at │ + │ deleted_at │ └─────────────────┘ + └─────────────────┘ + │ + ▼ (1) + ┌─────────────────┐ ┌─────────────────┐ + │ results │ │result_comments │ + │ │ │ │ + │ result_id │ │ comment_id │ + │ control_ref_id │ │ control_ref_id │ + │ test_ref_id │ │ test_ref_id │ + │ resdate │ │ commonth │ + │ resvalue │ │ comtext │ + │ rescomment │ │ created_at │ + │ created_at │ │ updated_at │ + │ updated_at │ │ deleted_at │ + │ deleted_at │ └─────────────────┘ + └─────────────────┘ +``` + +### 5.2 Table Specifications + +#### 5.2.1 master_depts (Departments) +| Column | Type | Description | +|--------|------|-------------| +| dept_id | INT UNSIGNED | Primary key, auto-increment | +| name | VARCHAR(100) | Department name, unique | +| created_at | DATETIME | Record creation timestamp | +| updated_at | DATETIME | Last modification timestamp | +| deleted_at | DATETIME | Soft delete timestamp (NULL if active) | + +--- + +#### 5.2.2 master_tests (Tests) +| Column | Type | Description | +|--------|------|-------------| +| test_id | INT UNSIGNED | Primary key, auto-increment | +| name | VARCHAR(150) | Test name | +| unit | VARCHAR(50) | Measurement unit | +| method | VARCHAR(50) | Test method | +| cva | DECIMAL(10,2) | Control Value Average | +| ba | DECIMAL(10,2) | Base Average | +| tea | DECIMAL(10,2) | Total Error Allowance | +| dept_ref_id | INT UNSIGNED | Foreign key to master_depts | +| created_at | DATETIME | Record creation timestamp | +| updated_at | DATETIME | Last modification timestamp | +| deleted_at | DATETIME | Soft delete timestamp | + +--- + +#### 5.2.3 master_controls (Controls) +| Column | Type | Description | +|--------|------|-------------| +| control_id | INT UNSIGNED | Primary key, auto-increment | +| name | VARCHAR(100) | Control name | +| lot | VARCHAR(50) | Lot number | +| producer | VARCHAR(100) | Control producer/manufacturer | +| expiry_date | DATE | Expiration date | +| dept_ref_id | INT UNSIGNED | Foreign key to master_depts | +| created_at | DATETIME | Record creation timestamp | +| updated_at | DATETIME | Last modification timestamp | +| deleted_at | DATETIME | Soft delete timestamp | + +--- + +#### 5.2.4 control_tests (Control-Test Link) +| Column | Type | Description | +|--------|------|-------------| +| control_test_id | INT UNSIGNED | Primary key, auto-increment | +| control_ref_id | INT UNSIGNED | Foreign key to master_controls | +| test_ref_id | INT UNSIGNED | Foreign key to master_tests | +| mean | DECIMAL(10,4) | Expected mean value | +| sd | DECIMAL(10,4) | Standard deviation | +| created_at | DATETIME | Record creation timestamp | +| updated_at | DATETIME | Last modification timestamp | +| deleted_at | DATETIME | Soft delete timestamp | + +--- + +#### 5.2.5 results (QC Results) +| Column | Type | Description | +|--------|------|-------------| +| result_id | INT UNSIGNED | Primary key, auto-increment | +| control_ref_id | INT UNSIGNED | Foreign key to master_controls | +| test_ref_id | INT UNSIGNED | Foreign key to master_tests | +| resdate | DATE | Result date | +| resvalue | DECIMAL(10,4) | Measured value | +| rescomment | TEXT | Optional comment for this result | +| created_at | DATETIME | Record creation timestamp | +| updated_at | DATETIME | Last modification timestamp | +| deleted_at | DATETIME | Soft delete timestamp | + +--- + +#### 5.2.6 result_comments (Monthly Comments) +| Column | Type | Description | +|--------|------|-------------| +| comment_id | INT UNSIGNED | Primary key, auto-increment | +| control_ref_id | INT UNSIGNED | Foreign key to master_controls | +| test_ref_id | INT UNSIGNED | Foreign key to master_tests | +| commonth | VARCHAR(7) | Month in YYYY-MM format | +| comtext | TEXT | Comment text | +| created_at | DATETIME | Record creation timestamp | +| updated_at | DATETIME | Last modification timestamp | +| deleted_at | DATETIME | Soft delete timestamp | + +--- + +### 5.3 Constraints + +#### 5.3.1 Unique Constraints +- `master_depts.name` - Department names must be unique +- `master_tests.name` - Test names should be unique within a department +- `master_controls.lot` - Lot numbers should be unique per control name + +#### 5.3.2 Foreign Key Relationships +- `master_tests.dept_ref_id` → `master_depts.dept_id` +- `master_controls.dept_ref_id` → `master_depts.dept_id` +- `control_tests.control_ref_id` → `master_controls.control_id` +- `control_tests.test_ref_id` → `master_tests.test_id` +- `results.control_ref_id` → `master_controls.control_id` +- `results.test_ref_id` → `master_tests.test_id` +- `result_comments.control_ref_id` → `master_controls.control_id` +- `result_comments.test_ref_id` → `master_tests.test_id` + +--- + +## 6. Non-Functional Requirements + +### 6.1 Usability Requirements +- Responsive design supporting desktop, tablet, and mobile devices +- Intuitive navigation with clear menu structure +- Modal-based forms for quick data entry without page navigation +- Toast notifications for user feedback (success, error, warning) +- Keyboard shortcuts for common operations (where applicable) +- Clear error messages with guidance for resolution + +### 6.2 Reliability Requirements +- 99.5% uptime during business hours +- Data integrity through soft delete mechanism +- Automatic timestamp tracking for all records +- No data loss on form validation errors +- Graceful error handling with user-friendly messages + +### 6.3 Maintainability Requirements +- Clean, documented code following CodeIgniter conventions +- Modular architecture separating concerns (controllers, models, views) +- Consistent naming conventions throughout the codebase +- Reusable components and layouts +- Centralized configuration (routes, database, environment) + +### 6.4 Security Requirements +- Input validation on all user inputs +- SQL injection prevention via parameterized queries +- XSS protection through output escaping +- CSRF protection on state-changing operations +- No sensitive data exposure in error messages +- Secure password storage (when authentication is implemented) + +### 6.5 Compatibility Requirements +- Modern browsers: Chrome, Firefox, Edge, Safari (latest 2 versions) +- Operating systems: Windows, macOS, Linux +- Screen resolutions: 1280x720 minimum, responsive to 4K +- Touch devices supported for mobile data entry + +--- + +## 7. UI/UX Requirements + +### 7.1 Design System + +#### 7.1.1 Color Palette +- **Primary:** Blue tones (brand color) +- **Background:** Light slate/gray for content areas +- **Cards/Panels:** White with subtle shadows and borders +- **Text:** Dark slate for readability +- **Accents:** Status colors (green=success, red=error, amber=warning) + +#### 7.1.2 Typography +- **Font:** System fonts or Inter/Sans-serif +- **Sizes:** 14px base, 12px for metadata, 16-24px for headings +- **Weights:** Regular for body, Semibold/Bold for headings + +#### 7.1.3 Component Style +- Rounded corners (6-12px border-radius) +- Subtle shadows for depth +- Glass-morphism effects for overlays +- DaisyUI-style buttons and form elements + +### 7.2 Layout Requirements + +#### 7.2.1 Main Layout +- **Sidebar:** Collapsible navigation on the left + - Logo/branding area + - Menu items with icons + - Collapsible menu groups + - User info section +- **Header:** Top bar with page title and actions +- **Content Area:** Main content in central panel +- **Footer:** Version info and copyright + +#### 7.2.2 Mobile Layout +- Hamburger menu to toggle sidebar +- Touch-friendly tap targets (44px minimum) +- Stacked layouts for forms and tables + +### 7.3 Page Requirements + +#### 7.3.1 Dashboard (`/`) +- Overview statistics cards +- Quick access to common actions +- Recent activity summary (future) + +#### 7.3.2 Dictionary Pages (`/dept`, `/test`, `/control`) +- List view with search and filters +- "Add New" button prominent +- Action buttons (Edit, Delete) per row +- Modal form for Create/Edit +- Confirmation dialog for Delete + +#### 7.3.3 Data Entry Pages (`/entry/daily`, `/entry/monthly`) +- Date/month selector +- Department filter +- Dynamic control/test selection +- Input fields with validation +- Save button with loading state +- Success/error feedback + +#### 7.3.4 Report Pages (`/report`, `/report/view`) +- Parameter selection form +- Statistical summary section +- Data table with highlighting +- Responsive table with horizontal scroll +- Print-friendly view (future) + +### 7.4 Interaction Requirements + +#### 7.4.1 Forms +- Clear labels and placeholders +- Inline validation feedback +- Required field indicators +- Loading states on submit +- Auto-focus on first field + +#### 7.4.2 Tables +- Sortable columns +- Search/filter functionality +- Pagination for large datasets +- Hover states for rows +- Action buttons clearly visible + +#### 7.4.3 Modals +- Close on backdrop click +- ESC key to close +- Smooth transition animations +- Focus management for accessibility + +#### 7.4.4 Notifications +- Toast notifications for actions +- Auto-dismiss after 3-5 seconds +- Persistent for errors (until dismissed) +- Multiple notifications stacked + +--- + +## 8. API Endpoints Reference + +### 8.1 Departments API (`/api/dept`) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/dept` | List all departments | +| GET | `/api/dept/(:num)` | Get department by ID | +| POST | `/api/dept` | Create new department | +| PATCH | `/api/dept/(:num)` | Update department | +| DELETE | `/api/dept/(:num)` | Soft delete department | + +### 8.2 Tests API (`/api/test`) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/test` | List all tests | +| GET | `/api/test/(:num)` | Get test by ID | +| POST | `/api/test` | Create new test | +| PATCH | `/api/test/(:num)` | Update test | +| DELETE | `/api/test/(:num)` | Soft delete test | + +### 8.3 Controls API (`/api/control`) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/control` | List all controls | +| GET | `/api/control/(:num)` | Get control by ID | +| GET | `/api/control/(:num)/tests` | Get tests linked to control | +| POST | `/api/control` | Create new control | +| PATCH | `/api/control/(:num)` | Update control | +| DELETE | `/api/control/(:num)` | Soft delete control | + +### 8.4 Entries API (`/api/entry`) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/entry/controls` | Get active controls by date/dept | +| GET | `/api/entry/tests` | Get tests for a control | +| POST | `/api/entry/daily` | Save daily result | +| POST | `/api/entry/monthly` | Save monthly results | +| POST | `/api/entry/comment` | Save monthly comment | + +--- + +## 9. Future Considerations + +The following features are identified for future development but are out of scope for the MVP: + +### 9.1 Authentication & Authorization +- User registration and login +- JWT-based session management +- Role-based access control (RBAC) +- User permissions by feature + +### 9.2 Enhanced Reporting +- PDF report export +- Excel/CSV data export +- Levey-Jennings charts +- Multi-month trend analysis + +### 9.3 QC Validation +- Westgard QC rules implementation +- Configurable rule sets +- Automatic rule violation detection +- Email alerts for out-of-range results + +### 9.4 Data Management +- Audit logging of all changes +- Data backup and restore +- Import from lab instruments +- Data migration tools + +### 9.5 User Experience +- Dashboard with visualizations +- Keyboard shortcuts power user features +- Dark mode theme +- Multi-language support + +--- + +## 10. Appendix + +### 10.1 Glossary + +| Term | Definition | +|------|------------| +| QC | Quality Control - processes to ensure test accuracy | +| CVA | Control Value Average - expected average control value | +| BA | Base Average - baseline average for comparison | +| TEA | Total Error Allowance - acceptable error range | +| SD | Standard Deviation - measure of data dispersion | +| Mean | Average of a set of values | +| Out-of-range | Value exceeding acceptable limits (typically mean ± 2 SD) | +| Soft Delete | Marking a record as deleted without physical removal | +| Control Material | Reference material with known values for QC testing | +| Lot Number | Manufacturer's batch identifier for control materials | + +### 10.2 References +- CodeIgniter 4 Documentation: https://codeigniter4.github.io/userguide/ +- Alpine.js Documentation: https://alpinejs.dev/ +- TailwindCSS Documentation: https://tailwindcss.com/docs +- DaisyUI Documentation: https://daisyui.com/ + +--- + +**Document Information** + +| Field | Value | +|-------|-------| +| Version | 1.0 | +| Status | Draft | +| Created | January 2026 | +| Last Updated | January 2026 | + +--- + +*This PRD defines the requirements for the TinyQC MVP. All stakeholders should review and approve before development begins.* diff --git a/docs/llms.txt b/docs/llms.txt new file mode 100644 index 0000000..86dbc15 --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,1852 @@ +--- +description: daisyUI 5 +alwaysApply: true +applyTo: "**" +downloadedFrom: https://daisyui.com/llms.txt +version: 5.5.x +--- + +# daisyUI 5 +daisyUI 5 is a CSS library for Tailwind CSS 4 +daisyUI 5 provides class names for common UI components + +- [daisyUI 5 docs](http://daisyui.com) +- [Guide: How to use this file in LLMs and code editors](https://daisyui.com/docs/editor/) +- [daisyUI 5 release notes](https://daisyui.com/docs/v5/) +- [daisyUI 4 to 5 upgrade guide](https://daisyui.com/docs/upgrade/) + +## daisyUI 5 install notes +[install guide](https://daisyui.com/docs/install/) +1. daisyUI 5 requires Tailwind CSS 4 +2. `tailwind.config.js` file is deprecated in Tailwind CSS v4. do not use `tailwind.config.js`. Tailwind CSS v4 only needs `@import "tailwindcss";` in the CSS file if it's a node dependency. +3. daisyUI 5 can be installed using `npm i -D daisyui@latest` and then adding `@plugin "daisyui";` to the CSS file +4. daisyUI is suggested to be installed as a dependency but if you really want to use it from CDN, you can use Tailwind CSS and daisyUI CDN files: +```html + + +``` +5. A CSS file with Tailwind CSS and daisyUI looks like this (if it's a node dependency) +```css +@import "tailwindcss"; +@plugin "daisyui"; +``` + +## daisyUI 5 usage rules +1. We can give styles to a HTML element by adding daisyUI class names to it. By adding a component class name, part class names (if there's any available for that component), and modifier class names (if there's any available for that component) +2. Components can be customized using Tailwind CSS utility classes if the customization is not possible using the existing daisyUI classes. For example `btn px-10` sets a custom horizontal padding to a `btn` +3. If customization of daisyUI styles using Tailwind CSS utility classes didn't work because of CSS specificity issues, you can use the `!` at the end of the Tailwind CSS utility class to override the existing styles. For example `btn bg-red-500!` sets a custom background color to a `btn` forcefully. This is a last resort solution and should be used sparingly +4. If a specific component or something similar to it doesn't exist in daisyUI, you can create your own component using Tailwind CSS utility +5. when using Tailwind CSS `flex` and `grid` for layout, it should be responsive using Tailwind CSS responsive utility prefixes. +6. Only allowed class names are existing daisyUI class names or Tailwind CSS utility classes. +7. Ideally, you won't need to write any custom CSS. Using daisyUI class names or Tailwind CSS utility classes is preferred. +8. suggested - if you need placeholder images, use https://picsum.photos/200/300 with the size you want +9. suggested - when designing , don't add a custom font unless it's necessary +10. don't add `bg-base-100 text-base-content` to body unless it's necessary +11. For design decisions, use Refactoring UI book best practices + +daisyUI 5 class names are one of the following categories. These type names are only for reference and are not used in the actual code +- `component`: the required component class +- `part`: a child part of a component +- `style`: sets a specific style to component or part +- `behavior`: changes the behavior of component or part +- `color`: sets a specific color to component or part +- `size`: sets a specific size to component or part +- `placement`: sets a specific placement to component or part +- `direction`: sets a specific direction to component or part +- `modifier`: modifies the component or part in a specific way +- `variant`: prefixes for utility classes that conditionally apply styles. syntax is `variant:utility-class` + +## Config +daisyUI 5 config docs: https://daisyui.com/docs/config/ +daisyUI without config: +```css +@plugin "daisyui"; +``` +daisyUI config with `light` theme only: +```css +@plugin "daisyui" { + themes: light --default; +} +``` +daisyUI with all the default configs: +```css +@plugin "daisyui" { + themes: light --default, dark --prefersdark; + root: ":root"; + include: ; + exclude: ; + prefix: ; + logs: true; +} +``` +An example config: +In below config, all the built-in themes are enabled while bumblebee is the default theme and synthwave is the prefersdark theme (default dark mode) +All the other themes are enabled and can be used by adding `data-theme="THEME_NAME"` to the `` element +root scrollbar gutter is excluded. `daisy-` prefix is used for all daisyUI classes and console.log is disabled +```css +@plugin "daisyui" { + themes: light, dark, cupcake, bumblebee --default, emerald, corporate, synthwave --prefersdark, retro, cyberpunk, valentine, halloween, garden, forest, aqua, lofi, pastel, fantasy, wireframe, black, luxury, dracula, cmyk, autumn, business, acid, lemonade, night, coffee, winter, dim, nord, sunset, caramellatte, abyss, silk; + root: ":root"; + include: ; + exclude: rootscrollgutter, checkbox; + prefix: daisy-; + logs: false; +} +``` +## daisyUI 5 colors + +### daisyUI color names +- `primary`: Primary brand color, The main color of your brand +- `primary-content`: Foreground content color to use on primary color +- `secondary`: Secondary brand color, The optional, secondary color of your brand +- `secondary-content`: Foreground content color to use on secondary color +- `accent`: Accent brand color, The optional, accent color of your brand +- `accent-content`: Foreground content color to use on accent color +- `neutral`: Neutral dark color, For not-saturated parts of UI +- `neutral-content`: Foreground content color to use on neutral color +- `base-100`:-100 Base surface color of page, used for blank backgrounds +- `base-200`:-200 Base color, darker shade, to create elevations +- `base-300`:-300 Base color, even more darker shade, to create elevations +- `base-content`: Foreground content color to use on base color +- `info`: Info color, For informative/helpful messages +- `info-content`: Foreground content color to use on info color +- `success`: Success color, For success/safe messages +- `success-content`: Foreground content color to use on success color +- `warning`: Warning color, For warning/caution messages +- `warning-content`: Foreground content color to use on warning color +- `error`: Error color, For error/danger/destructive messages +- `error-content`: Foreground content color to use on error color + +### daisyUI color rules +1. daisyUI adds semantic color names to Tailwind CSS colors +2. daisyUI color names can be used in utility classes, like other Tailwind CSS color names. for example, `bg-primary` will use the primary color for the background +3. daisyUI color names include variables as value so they can change based the theme +4. There's no need to use `dark:` for daisyUI color names +5. Ideally only daisyUI color names should be used for colors so the colors can change automatically based on the theme +6. If a Tailwind CSS color name (like `red-500`) is used, it will be same red color on all themes +7. If a daisyUI color name (like `primary`) is used, it will change color based on the theme +8. Using Tailwind CSS color names for text colors should be avoided because Tailwind CSS color `text-gray-800` on `bg-base-100` would be unreadable on a dark theme - because on dark theme, `bg-base-100` is a dark color +9. `*-content` colors should have a good contrast compared to their associated colors +10. suggestion - when designing a page use `base-*` colors for majority of the page. use `primary` color for important elements + +### daisyUI custom theme with custom colors +A CSS file with Tailwind CSS, daisyUI and a custom daisyUI theme looks like this: +```css +@import "tailwindcss"; +@plugin "daisyui"; +@plugin "daisyui/theme" { + name: "mytheme"; + default: true; /* set as default */ + prefersdark: false; /* set as default dark mode (prefers-color-scheme:dark) */ + color-scheme: light; /* color of browser-provided UI */ + + --color-base-100: oklch(98% 0.02 240); + --color-base-200: oklch(95% 0.03 240); + --color-base-300: oklch(92% 0.04 240); + --color-base-content: oklch(20% 0.05 240); + --color-primary: oklch(55% 0.3 240); + --color-primary-content: oklch(98% 0.01 240); + --color-secondary: oklch(70% 0.25 200); + --color-secondary-content: oklch(98% 0.01 200); + --color-accent: oklch(65% 0.25 160); + --color-accent-content: oklch(98% 0.01 160); + --color-neutral: oklch(50% 0.05 240); + --color-neutral-content: oklch(98% 0.01 240); + --color-info: oklch(70% 0.2 220); + --color-info-content: oklch(98% 0.01 220); + --color-success: oklch(65% 0.25 140); + --color-success-content: oklch(98% 0.01 140); + --color-warning: oklch(80% 0.25 80); + --color-warning-content: oklch(20% 0.05 80); + --color-error: oklch(65% 0.3 30); + --color-error-content: oklch(98% 0.01 30); + + --radius-selector: 1rem; /* border radius of selectors (checkbox, toggle, badge) */ + --radius-field: 0.25rem; /* border radius of fields (button, input, select, tab) */ + --radius-box: 0.5rem; /* border radius of boxes (card, modal, alert) */ + /* preferred values for --radius-* : 0rem, 0.25rem, 0.5rem, 1rem, 2rem */ + + --size-selector: 0.25rem; /* base size of selectors (checkbox, toggle, badge). Value must be 0.25rem unless we intentionally want bigger selectors. In so it can be 0.28125 or 0.3125. If we intentionally want smaller selectors, it can be 0.21875 or 0.1875 */ + --size-field: 0.25rem; /* base size of fields (button, input, select, tab). Value must be 0.25rem unless we intentionally want bigger fields. In so it can be 0.28125 or 0.3125. If we intentionally want smaller fields, it can be 0.21875 or 0.1875 */ + + --border: 1px; /* border size. Value must be 1px unless we intentionally want thicker borders. In so it can be 1.5px or 2px. If we intentionally want thinner borders, it can be 0.5px */ + + --depth: 1; /* only 0 or 1 – Adds a shadow and subtle 3D depth effect to components */ + --noise: 0; /* only 0 or 1 - Adds a subtle noise (grain) effect to components */ +} +``` +#### Rules +- All CSS variables above are required +- Colors can be OKLCH or hex or other formats +- If you're generating a custom theme, do not include the comments from the example above. Just provide the code. + +People can use https://daisyui.com/theme-generator/ visual tool to create their own theme. + +## daisyUI 5 components + +### accordion +Accordion is used for showing and hiding content but only one item can stay open at a time + +[accordion docs](https://daisyui.com/components/accordion/) + +#### Class names +- component: `collapse` +- part: `collapse-title`, `collapse-content` +- modifier: `collapse-arrow`, `collapse-plus`, `collapse-open`, `collapse-close` + +#### Syntax +```html +
{CONTENT}
+``` +where content is: +```html + +
{title}
+
{CONTENT}
+``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier class names +- Accordion uses radio inputs. All radio inputs with the same name work together and only one of them can be open at a time +- If you have more than one set of accordion items on a page, use different names for the radio inputs on each set +- Replace {name} with a unique name for the accordion group +- replace `{checked}` with `checked="checked"` if you want the accordion to be open by default + +### alert +Alert informs users about important events + +[alert docs](https://daisyui.com/components/alert/) + +#### Class names +- component: `alert` +- style: `alert-outline`, `alert-dash`, `alert-soft` +- color: `alert-info`, `alert-success`, `alert-warning`, `alert-error` +- direction: `alert-vertical`, `alert-horizontal` + +#### Syntax +```html + +``` + +#### Rules +- {MODIFIER} is optional and can have one of each style/color/direction class names +- Add `sm:alert-horizontal` for responsive layouts + +### avatar +Avatars are used to show a thumbnail + +[avatar docs](https://daisyui.com/components/avatar/) + +#### Class names +- component: `avatar`, `avatar-group` +- modifier: `avatar-online`, `avatar-offline`, `avatar-placeholder` + +#### Syntax +```html +
+
+ +
+
+``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier class names +- Use `avatar-group` for containing multiple avatars +- You can set custom sizes using `w-*` and `h-*` +- You can use mask classes such as `mask-squircle`, `mask-hexagon`, `mask-triangle` + +### badge +Badges are used to inform the user of the status of specific data + +[badge docs](https://daisyui.com/components/badge/) + +#### Class names +- component: `badge` +- style: `badge-outline`, `badge-dash`, `badge-soft`, `badge-ghost` +- color: `badge-neutral`, `badge-primary`, `badge-secondary`, `badge-accent`, `badge-info`, `badge-success`, `badge-warning`, `badge-error` +- size: `badge-xs`, `badge-sm`, `badge-md`, `badge-lg`, `badge-xl` + +#### Syntax +```html +Badge +``` + +#### Rules +- {MODIFIER} is optional and can have one of each style/color/size class names +- Can be used inside text or buttons +- To create an empty badge, just remove the text between the span tags + +### breadcrumbs +Breadcrumbs helps users to navigate + +[breadcrumbs docs](https://daisyui.com/components/breadcrumbs/) + +#### Class names +- component: `breadcrumbs` + +#### Syntax +```html + +``` + +#### Rules +- breadcrumbs only has one main class name +- Can contain icons inside the links +- If you set `max-width` or the list gets larger than the container it will scroll + +### button +Buttons allow the user to take actions + +[button docs](https://daisyui.com/components/button/) + +#### Class names +- component: `btn` +- color: `btn-neutral`, `btn-primary`, `btn-secondary`, `btn-accent`, `btn-info`, `btn-success`, `btn-warning`, `btn-error` +- style: `btn-outline`, `btn-dash`, `btn-soft`, `btn-ghost`, `btn-link` +- behavior: `btn-active`, `btn-disabled` +- size: `btn-xs`, `btn-sm`, `btn-md`, `btn-lg`, `btn-xl` +- modifier: `btn-wide`, `btn-block`, `btn-square`, `btn-circle` + +#### Syntax +```html + +``` +#### Rules +- {MODIFIER} is optional and can have one of each color/style/behavior/size/modifier class names +- btn can be used on any html tags such as ` +``` + +#### Rules +- {MODIFIER} is optional and can have one of the size class names +- To make a button active, add `dock-active` class to the button +- add `` is required for responsivness of the dock in iOS + +### drawer +Drawer is a grid layout that can show/hide a sidebar on the left or right side of the page + +[drawer docs](https://daisyui.com/components/drawer/) + +#### Class names +- component: `drawer` +- part: `drawer-toggle`, `drawer-content`, `drawer-side`, `drawer-overlay` +- placement: `drawer-end` +- modifier: `drawer-open` +- variant: `is-drawer-open:`, `is-drawer-close:` + +#### Syntax +```html +
+ +
{CONTENT}
+
{SIDEBAR}
+
+``` +where {CONTENT} can be navbar, site content, footer, etc +and {SIDEBAR} can be a menu like: +```html +
+``` +To open/close the drawer, use a label that points to the `drawer-toggle` input: +```html + +``` +Example: This sidebar is always visible on large screen, can be toggled on small screen: +```html +
+ +
+ + +
+
+ + +
+
+``` + +Example: This sidebar is always visible. When it's close we only see iocns, when it's open we see icons and text +```html +
+ +
+ +
+
+ +
+ + + +
+ +
+
+
+
+``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier/placement class names +- `id` is required for the `drawer-toggle` input. change `my-drawer` to a unique id according to your needs +- `lg:drawer-open` can be used to make sidebar visible on larger screens +- `drawer-toggle` is a hidden checkbox. Use label with "for" attribute to toggle state +- if you want to open the drawer when a button is clicked, use `` where `my-drawer` is the id of the `drawer-toggle` input +- when using drawer, every page content must be inside `drawer-content` element. for example navbar, footer, etc should not be outside of `drawer` + +### dropdown +Dropdown can open a menu or any other element when the button is clicked + +[dropdown docs](https://daisyui.com/components/dropdown/) + +#### Class names +- component: `dropdown` +- part: `dropdown-content` +- placement: `dropdown-start`, `dropdown-center`, `dropdown-end`, `dropdown-top`, `dropdown-bottom`, `dropdown-left`, `dropdown-right` +- modifier: `dropdown-hover`, `dropdown-open`, `dropdown-close` + +#### Syntax +Using details and summary +```html + +``` + +Using popover API +```html + + +``` + +Using CSS focus +```html + +``` + +#### Rules +- {MODIFIER} is optional and can have one of the modifier/placement class names +- replace `{id}` and `{anchor}` with a unique name +- For CSS focus dropdowns, use `tabindex="0"` and `role="button"` on the button +- The content can be any HTML element (not just `
    `) + +### fab +FAB (Floating Action Button) stays in the bottom corner of screen. It includes a focusable and accessible element with button role. Clicking or focusing it shows additional buttons (known as Speed Dial buttons) in a vertical arrangement or a flower shape (quarter circle) + +[fab docs](https://daisyui.com/components/fab/) + +#### Class names +- component: `fab` +- part: `fab-close`, `fab-main-action` +- modifier: `fab-flower` + +#### Syntax +A single FAB in the corder of screen +```html +
    + +
    +``` +A FAB that opens a 3 other buttons in the corner of page vertically +```html +
    +
    {IconOriginal}
    + + + +
    +``` +A FAB that opens a 3 other buttons in the corner of page vertically and they have label text +```html +
    +
    {IconOriginal}
    +
    {Label1}
    +
    {Label2}
    +
    {Label3}
    +
    +``` +FAB with rectangle buttons. These are not circular buttons so they can have more content. +```html +
    +
    {IconOriginal}
    + + + +
    +``` +FAB with close button. When FAB is open, the original button is replaced with a close button +```html +
    +
    {IconOriginal}
    +
    Close
    +
    {Label1}
    +
    {Label2}
    +
    {Label3}
    +
    +``` +FAB with Main Action button. When FAB is open, the original button is replaced with a main action button +```html +
    +
    {IconOriginal}
    +
    + {LabelMainAction} +
    +
    {Label1}
    +
    {Label2}
    +
    {Label3}
    +
    +``` +FAB Flower. It opens the buttons in a flower shape (quarter circle) arrangement instead of vertical +```html +
    +
    {IconOriginal}
    + + + + +
    +``` +FAB Flower with tooltips. There's no space for a text label in a quarter circle, so tooltips are used to indicate the button's function +```html +
    +
    {IconOriginal}
    + +
    + +
    +
    + +
    +
    + +
    +
    +``` +#### Rules +- {Icon*} should be replaced with the appropriate icon for each button. SVG icons are recommended +- {IconOriginal} is the icon that we see before opening the FAB +- {IconMainAction} is the icon we see after opening the FAB +- {Icon1}, {Icon2}, {Icon3} are the icons for the additional buttons +- {Label*} is the label text for each button + +### fieldset +Fieldset is a container for grouping related form elements. It includes fieldset-legend as a title and label as a description + +[fieldset docs](https://daisyui.com/components/fieldset/) + +#### Class names +- Component: `fieldset`, `label` +- Parts: `fieldset-legend` + +#### Syntax +```html +
    + {title} + {CONTENT} +

    {description}

    +
    +``` + +#### Rules +- You can use any element as a direct child of fieldset to add form elements + +### file-input +File Input is a an input field for uploading files + +[file-input docs](https://daisyui.com/components/file-input/) + +#### Class Names: +- Component: `file-input` +- Style: `file-input-ghost` +- Color: `file-input-neutral`, `file-input-primary`, `file-input-secondary`, `file-input-accent`, `file-input-info`, `file-input-success`, `file-input-warning`, `file-input-error` +- Size: `file-input-xs`, `file-input-sm`, `file-input-md`, `file-input-lg`, `file-input-xl` + +#### Syntax +```html + +``` + +#### Rules +- {MODIFIER} is optional and can have one of each style/color/size class names + +### filter +Filter is a group of radio buttons. Choosing one of the options will hide the others and shows a reset button next to the chosen option + +[filter docs](https://daisyui.com/components/filter/) + +#### Class names +- component: `filter` +- part: `filter-reset` + +#### Syntax +Using HTML form +```html +
    + + + +
    +``` +Without HTML form +```html +
    + + + +
    +``` + +#### Rules +- replace `{NAME}` with proper value, according to the context of the filter +- Each set of radio inputs must have unique `name` attributes to avoid conflicts +- Use `
    ` tag when possible and only use `
    ` if you can't use a HTML form for some reason +- Use `filter-reset` class for the reset button + +### footer +Footer can contain logo, copyright notice, and links to other pages + +[footer docs](https://daisyui.com/components/footer/) + +#### Class names +- component: `footer` +- part: `footer-title` +- placement: `footer-center` +- direction: `footer-horizontal`, `footer-vertical` + +#### Syntax +```html +
    {CONTENT}
    +``` +where content can contain several `