diff --git a/CLAUDE.md b/CLAUDE.md index 89d70ed..2fb2260 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ This is a CodeIgniter 4 Quality Control management system with: - **Database**: SQL Server (uses `SQLSRV` driver) - **Frontend**: TailwindCSS + Alpine.js + DaisyUI (CDN-based, no build step) - **Testing**: PHPUnit 10 -- **Icons**: FontAwesome 6 +- **Icons**: FontAwesome 7 ### Key Components @@ -39,8 +39,8 @@ This is a CodeIgniter 4 Quality Control management system with: **Controllers** (`app/Controllers/`): - `PageController` - Renders page views with `main_layout` -- `Api\*` - Generic entry API controllers (Dashboard, Report, Entry) -- `Master\*` - CRUD for master data (Depts, Tests, Controls) +- `Api\*` - Consolidated entry API controllers (DashboardApi, EntryApi, ReportApi) +- `Master\*` - CRUD for master data (MasterDepts, MasterTests, MasterControls) - `Qc\*` - QC domain controllers (ControlTests, Results, ResultComments) **Views** (`app/Views/`): @@ -50,6 +50,7 @@ This is a CodeIgniter 4 Quality Control management system with: **Helpers** (`app/Helpers/`): - `stringcase_helper.php` - `camel_to_snake_array()`, `snake_to_camel_array()` +- The `stringcase` helper is auto-loaded in `BaseController` ### Database Schema @@ -170,3 +171,8 @@ class MasterDeptsModel extends BaseModel { 1. Don't skip soft deletes (`deleted_at`) 2. Don't mix concerns - controllers handle HTTP, models handle data 3. Don't forget case conversion - use helpers or BaseModel + +## Response Style + +- Use emojis in responses where appropriate to add visual appeal 😊 +- Keep responses concise and helpful diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php index 9a92824..319835c 100644 --- a/app/Config/Autoload.php +++ b/app/Config/Autoload.php @@ -88,5 +88,7 @@ class Autoload extends AutoloadConfig * * @var list */ - public $helpers = []; + public $helpers = [ + 'stringcase', + ]; } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 8fac304..3ced2aa 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -10,6 +10,7 @@ $routes->get('/', 'PageController::dashboard'); $routes->get('/master/dept', 'PageController::masterDept'); $routes->get('/master/test', 'PageController::masterTest'); $routes->get('/master/control', 'PageController::masterControl'); +$routes->get('/master/control-tests', 'PageController::controlTests'); $routes->get('/dept', 'PageController::dept'); $routes->get('/test', 'PageController::test'); $routes->get('/control', 'PageController::control'); diff --git a/app/Controllers/Api/DashboardApiController.php b/app/Controllers/Api/DashboardApiController.php index 3fc3045..3d4e5cf 100644 --- a/app/Controllers/Api/DashboardApiController.php +++ b/app/Controllers/Api/DashboardApiController.php @@ -32,6 +32,7 @@ class DashboardApiController extends BaseController r.res_value, r.created_at, c.control_name as controlName, + c.lot, t.test_name as testName, ct.mean, ct.sd @@ -65,6 +66,7 @@ class DashboardApiController extends BaseController 'resValue' => $row['res_value'], 'createdAt' => $row['created_at'], 'controlName' => $row['controlName'], + 'lot' => $row['lot'], 'testName' => $row['testName'], 'mean' => $row['mean'], 'sd' => $row['sd'], diff --git a/app/Controllers/Api/EntryApiController.php b/app/Controllers/Api/EntryApiController.php index 6648dce..188516d 100644 --- a/app/Controllers/Api/EntryApiController.php +++ b/app/Controllers/Api/EntryApiController.php @@ -185,15 +185,18 @@ class EntryApiController extends BaseController 'control_id' => $r['controlId'], 'test_id' => $r['testId'], 'res_date' => $date, - 'res_value' => $r['value'] !== '' ? (float) $r['value'] : null, - 'res_comment' => $r['comment'] ?? null + 'res_value' => $r['value'] !== '' ? (float) $r['value'] : null ]; if ($data['res_value'] === null) { continue; // Skip empty values } - $savedIds[] = $this->resultModel->upsertResult($data); + $resultId = $this->resultModel->upsertResult($data); + $savedIds[] = [ + 'testId' => $r['testId'], + 'resultId' => $resultId + ]; } // Commit transaction @@ -231,14 +234,14 @@ class EntryApiController extends BaseController return $this->failNotFound('Test not found'); } - // Get controls for this test with QC parameters - $controls = $this->controlTestModel->getByTest((int) $testId); + // Get controls for this test with QC parameters (filter out expired) + $controls = $this->controlTestModel->getByTest((int) $testId, $month); // Get existing results for this month $results = $this->resultModel->getByMonth((int) $testId, $month); - // Get comments for this month - $comments = $this->commentModel->getByTestMonth((int) $testId, $month); + // Get comments for this test (via results) + $comments = $this->commentModel->getByTest((int) $testId); // Map results by control_id and day $resultsByControl = []; @@ -246,17 +249,16 @@ class EntryApiController extends BaseController $day = (int) date('j', strtotime($r['resDate'])); $resultsByControl[$r['controlId']][$day] = [ 'resultId' => $r['id'], - 'resValue' => $r['resValue'], - 'resComment' => $r['resComment'] + 'resValue' => $r['resValue'] ]; } - // Map comments by control_id (BaseModel returns camelCase) - $commentsByControl = []; + // Map comments by result_id + $commentsByResultId = []; foreach ($comments as $c) { - $commentsByControl[$c['controlId']] = [ + $commentsByResultId[$c['resultId']] = [ 'commentId' => $c['resultCommentId'], - 'comText' => $c['comText'] + 'commentText' => $c['commentText'] ]; } @@ -267,11 +269,16 @@ class EntryApiController extends BaseController $resultsArray = array_fill(1, 31, null); foreach ($resultsByDay as $day => $val) { - $resultsArray[$day] = $val; + $resultWithComment = $val; + // Add comment if exists for this result + if (isset($commentsByResultId[$val['resultId']])) { + $resultWithComment['resComment'] = $commentsByResultId[$val['resultId']]['commentText']; + } else { + $resultWithComment['resComment'] = null; + } + $resultsArray[$day] = $resultWithComment; } - $comment = $commentsByControl[$c['controlId']] ?? null; - $controlsWithData[] = [ 'controlTestId' => $c['id'], 'controlId' => $c['controlId'], @@ -280,8 +287,7 @@ class EntryApiController extends BaseController 'producer' => $c['producer'], 'mean' => $c['mean'], 'sd' => $c['sd'], - 'results' => $resultsArray, - 'comment' => $comment + 'results' => $resultsArray ]; } @@ -326,7 +332,7 @@ class EntryApiController extends BaseController $daysInMonth = (int) date('t', strtotime($month . '-01')); $savedCount = 0; - $commentCount = 0; + $resultIdMap = []; // Map controlId + day -> resultId // Start transaction $this->resultModel->db->transBegin(); @@ -334,10 +340,18 @@ class EntryApiController extends BaseController foreach ($controls as $c) { $controlId = $c['controlId']; $results = $c['results'] ?? []; - $commentText = $c['comment'] ?? null; - // Save results - foreach ($results as $day => $value) { + // Save results with optional comments + foreach ($results as $day => $data) { + // Handle both old format (value only) and new format (value + comment) + if (is_array($data)) { + $value = $data['value']; + $commentText = $data['comment'] ?? null; + } else { + $value = $data; + $commentText = null; + } + if ($value === null || $value === '') { continue; } @@ -348,29 +362,17 @@ class EntryApiController extends BaseController } $date = $month . '-' . str_pad($day, 2, '0', STR_PAD_LEFT); - $data = [ + $resultData = [ 'control_id' => $controlId, 'test_id' => $testId, 'res_date' => $date, 'res_value' => (float) $value ]; - $this->resultModel->upsertResult($data); + $resultId = $this->resultModel->upsertResult($resultData); + $resultIdMap["{$controlId}_{$day}"] = $resultId; $savedCount++; } - - // Save comment - if ($commentText !== null) { - $commentData = [ - 'control_id' => $controlId, - 'test_id' => $testId, - 'comment_month' => $month, - 'com_text' => trim($commentText) - ]; - - $this->commentModel->upsertComment($commentData); - $commentCount++; - } } // Commit transaction @@ -378,8 +380,11 @@ class EntryApiController extends BaseController return $this->respond([ 'status' => 'success', - 'message' => "Saved {$savedCount} results and {$commentCount} comments", - 'data' => ['savedCount' => $savedCount, 'commentCount' => $commentCount] + 'message' => "Saved {$savedCount} results", + 'data' => [ + 'savedCount' => $savedCount, + 'resultIdMap' => $resultIdMap + ] ], 200); } catch (\Exception $e) { // Rollback transaction on error @@ -390,14 +395,14 @@ class EntryApiController extends BaseController /** * POST /api/entry/comment - * Save monthly comment (single) + * Save daily comment (single) */ public function saveComment() { try { $input = $this->request->getJSON(true); - $required = ['controlId', 'testId', 'month', 'comment']; + $required = ['resultId', 'comment']; foreach ($required as $field) { if (!isset($input[$field])) { return $this->failValidationErrors([$field => 'Required']); @@ -405,10 +410,8 @@ class EntryApiController extends BaseController } $commentData = [ - 'control_id' => $input['controlId'], - 'test_id' => $input['testId'], - 'comment_month' => $input['month'], - 'com_text' => trim($input['comment']) + 'result_id' => $input['resultId'], + 'comment_text' => trim($input['comment']) ]; $id = $this->commentModel->upsertComment($commentData); diff --git a/app/Controllers/Api/ReportApiController.php b/app/Controllers/Api/ReportApiController.php index 6984401..19c3ae1 100644 --- a/app/Controllers/Api/ReportApiController.php +++ b/app/Controllers/Api/ReportApiController.php @@ -45,22 +45,22 @@ class ReportApiController extends BaseController $control = $this->dictControlModel->find($controlId); if (!$control) continue; - $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); + $controlTest = $this->controlTestModel->getByControlAndTest($control['controlId'], $test); + $results = $this->resultModel->getByControlAndMonth($control['controlId'], $test, $dates); + $comment = $this->commentModel->getByControlTestMonth($control['controlId'], $test, $dates); $testInfo = $this->dictTestModel->find($test); $outOfRangeCount = 0; $processedResults = []; if ($controlTest && $controlTest['sd'] > 0) { foreach ($results as $res) { - $zScore = ($res['resvalue'] - $controlTest['mean']) / $controlTest['sd']; + $zScore = ($res['resValue'] - $controlTest['mean']) / $controlTest['sd']; $outOfRange = abs($zScore) > 2; if ($outOfRange) $outOfRangeCount++; $processedResults[] = [ - 'resdate' => $res['resdate'], - 'resvalue' => $res['resvalue'], + 'resdate' => $res['resDate'], + 'resvalue' => $res['resValue'], 'zScore' => round($zScore, 2), 'outOfRange' => $outOfRange, 'status' => $zScore === null ? '-' : (abs($zScore) > 2 ? 'Out' : (abs($zScore) > 1 ? 'Warn' : 'OK')) @@ -69,8 +69,8 @@ class ReportApiController extends BaseController } else { foreach ($results as $res) { $processedResults[] = [ - 'resdate' => $res['resdate'], - 'resvalue' => $res['resvalue'], + 'resdate' => $res['resDate'], + 'resvalue' => $res['resValue'], 'zScore' => null, 'outOfRange' => false, 'status' => '-' diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 343b084..3194091 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -18,6 +18,6 @@ abstract class BaseController extends Controller { parent::initController($request, $response, $logger); $this->session = \Config\Services::session(); - $this->helpers = ['form', 'url', 'json']; + $this->helpers = ['form', 'url', 'json', 'stringcase']; } } diff --git a/app/Controllers/Master/MasterControlsController.php b/app/Controllers/Master/MasterControlsController.php index ef09e7f..8989ea6 100644 --- a/app/Controllers/Master/MasterControlsController.php +++ b/app/Controllers/Master/MasterControlsController.php @@ -14,7 +14,7 @@ class MasterControlsController extends BaseController { public function __construct() { $this->model = new MasterControlsModel(); $this->rules = [ - 'name' => 'required|min_length[1]', + 'controlName' => 'required|min_length[1]', 'lot' => 'required|min_length[1]', ]; } @@ -71,10 +71,10 @@ class MasterControlsController extends BaseController { 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()); } + $input = camel_to_snake_array($input); try { $this->model->update($id, $input); return $this->respond([ diff --git a/app/Controllers/Master/MasterTestsController.php b/app/Controllers/Master/MasterTestsController.php index 916c6e8..fec313a 100644 --- a/app/Controllers/Master/MasterTestsController.php +++ b/app/Controllers/Master/MasterTestsController.php @@ -14,7 +14,7 @@ class MasterTestsController extends BaseController { public function __construct() { $this->model = new MasterTestsModel(); $this->rules = [ - 'name' => 'required|min_length[1]', + 'testName' => 'required|min_length[1]', ]; } diff --git a/app/Controllers/PageController.php b/app/Controllers/PageController.php index 5891b3a..934cbb4 100644 --- a/app/Controllers/PageController.php +++ b/app/Controllers/PageController.php @@ -23,6 +23,10 @@ class PageController extends BaseController { return view('master/control/index'); } + public function controlTests() { + return view('master/control_test/index'); + } + public function entry() { return view('entry/index'); } diff --git a/app/Controllers/Qc/ControlTestsController.php b/app/Controllers/Qc/ControlTestsController.php index 3b3b1af..0f984e8 100644 --- a/app/Controllers/Qc/ControlTestsController.php +++ b/app/Controllers/Qc/ControlTestsController.php @@ -5,93 +5,67 @@ use CodeIgniter\API\ResponseTrait; use App\Controllers\BaseController; use App\Models\Qc\ControlTestsModel; -class ControlTestsController extends BaseController { +class ControlTestsController extends BaseController +{ use ResponseTrait; protected $model; - protected $rules; + protected $rules = [ + 'controlId' => 'required|numeric', + 'testId' => 'required|numeric', + 'mean' => 'required|decimal', + 'sd' => 'required|decimal', + ]; - public function __construct() { + public function __construct() + { $this->model = new ControlTestsModel(); - $this->rules = []; } - public function index() { + 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()); - } + return $this->respond([ + 'status' => 'success', + 'data' => $this->model->search($keyword) + ]); } - 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() { + 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 - ]); + $id = $this->model->insert($input); + return $this->respondCreated(['status' => 'success', 'data' => $id]); } catch (\Exception $e) { 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->model->update($id, $input); - return $this->respond([ - 'status' => 'success', - 'message' => 'update success', - 'data' => [$id] - ], 200); + return $this->respond(['status' => 'success']); } catch (\Exception $e) { return $this->failServerError($e->getMessage()); } } - public function delete($id = null) { + public function delete($id = null) + { try { $this->model->delete($id); - return $this->respond([ - 'status' => 'success', - 'message' => 'delete success', - 'data' => [$id] - ], 200); + return $this->respond(['status' => 'success']); } catch (\Exception $e) { return $this->failServerError($e->getMessage()); } diff --git a/app/Database/Migrations/2026-01-17-000001_QualityControlSystem.php b/app/Database/Migrations/2026-01-17-000001_QualityControlSystem.php index a399241..70a8c01 100644 --- a/app/Database/Migrations/2026-01-17-000001_QualityControlSystem.php +++ b/app/Database/Migrations/2026-01-17-000001_QualityControlSystem.php @@ -8,10 +8,10 @@ class QualityControlSystem extends Migration { public function up() { - // master_depts - No dependencies + // master_depts $this->forge->addField([ 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], - 'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'dept_name' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], 'created_at' => ['type' => 'DATETIME', 'null' => true], 'updated_at' => ['type' => 'DATETIME', 'null' => true], 'deleted_at' => ['type' => 'DATETIME', 'null' => true], @@ -19,11 +19,11 @@ class QualityControlSystem extends Migration $this->forge->addKey('dept_id', true); $this->forge->createTable('master_depts'); - // master_controls - FK to master_depts + // master_controls $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], + 'control_name' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], 'lot' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], 'producer' => ['type' => 'TEXT', 'null' => true], 'exp_date' => ['type' => 'DATE', 'null' => true], @@ -35,13 +35,14 @@ class QualityControlSystem extends Migration $this->forge->addForeignKey('dept_id', 'master_depts', 'dept_id', 'SET NULL', 'CASCADE'); $this->forge->createTable('master_controls'); - // master_tests - FK to master_depts + // master_tests $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], + 'test_code' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], + 'test_name' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], + 'test_unit' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true], + 'test_method' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true], 'cva' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], 'ba' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], 'tea' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], @@ -50,10 +51,11 @@ class QualityControlSystem extends Migration 'deleted_at' => ['type' => 'DATETIME', 'null' => true], ]); $this->forge->addKey('test_id', true); + $this->forge->addUniqueKey('test_code'); $this->forge->addForeignKey('dept_id', 'master_depts', 'dept_id', 'SET NULL', 'CASCADE'); $this->forge->createTable('master_tests'); - // control_tests - FK to master_controls, master_tests + // control_tests $this->forge->addField([ 'control_test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], 'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], @@ -65,18 +67,18 @@ class QualityControlSystem extends Migration 'deleted_at' => ['type' => 'DATETIME', 'null' => true], ]); $this->forge->addKey('control_test_id', true); + $this->forge->addUniqueKey(['control_id', 'test_id']); $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'); - // results - FK to master_controls, master_tests + // results $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], + 'res_date' => ['type' => 'DATE', 'null' => true], + 'res_value' => ['type' => 'DECIMAL', 'constraint' => '18,4', 'null' => true], 'created_at' => ['type' => 'DATETIME', 'null' => true], 'updated_at' => ['type' => 'DATETIME', 'null' => true], 'deleted_at' => ['type' => 'DATETIME', 'null' => true], @@ -86,21 +88,17 @@ class QualityControlSystem extends Migration $this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE'); $this->forge->createTable('results'); - // result_comments - FK to master_controls, master_tests (composite unique key) + // result_comments $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], + 'result_id' => ['type' => 'INT', 'unsigned' => true], + 'comment_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->addForeignKey('result_id', 'results', 'result_id', 'CASCADE', 'CASCADE'); $this->forge->createTable('result_comments'); } diff --git a/app/Database/Migrations/2026-01-20-000001_RenameMasterColumns.php b/app/Database/Migrations/2026-01-20-000001_RenameMasterColumns.php deleted file mode 100644 index 0104d64..0000000 --- a/app/Database/Migrations/2026-01-20-000001_RenameMasterColumns.php +++ /dev/null @@ -1,36 +0,0 @@ - dept_name - $this->db->query("ALTER TABLE master_depts CHANGE COLUMN name dept_name VARCHAR(255) NOT NULL"); - - // master_controls: name -> control_name - $this->db->query("ALTER TABLE master_controls CHANGE COLUMN name control_name VARCHAR(255) NOT NULL"); - - // master_tests: name -> test_name, unit -> test_unit, method -> test_method - $this->db->query("ALTER TABLE master_tests CHANGE COLUMN name test_name VARCHAR(255) NOT NULL"); - $this->db->query("ALTER TABLE master_tests CHANGE COLUMN unit test_unit VARCHAR(100) NULL"); - $this->db->query("ALTER TABLE master_tests CHANGE COLUMN method test_method VARCHAR(255) NULL"); - } - - public function down() - { - // master_tests: revert - $this->db->query("ALTER TABLE master_tests CHANGE COLUMN test_method method VARCHAR(255) NULL"); - $this->db->query("ALTER TABLE master_tests CHANGE COLUMN test_unit unit VARCHAR(100) NULL"); - $this->db->query("ALTER TABLE master_tests CHANGE COLUMN test_name name VARCHAR(255) NOT NULL"); - - // master_controls: revert - $this->db->query("ALTER TABLE master_controls CHANGE COLUMN control_name name VARCHAR(255) NOT NULL"); - - // master_depts: revert - $this->db->query("ALTER TABLE master_depts CHANGE COLUMN dept_name name VARCHAR(255) NOT NULL"); - } -} diff --git a/app/Database/Seeds/CmodQcSeeder.php b/app/Database/Seeds/CmodQcSeeder.php index 4425577..bdc4668 100644 --- a/app/Database/Seeds/CmodQcSeeder.php +++ b/app/Database/Seeds/CmodQcSeeder.php @@ -8,108 +8,110 @@ class CmodQcSeeder extends Seeder { public function run() { - // 1. Insert Departments (4 entries) + $this->seedDepts(); + $this->seedControls(); + $this->seedTests(); + $this->seedControlTests(); + $this->seedResults(); + $this->seedResultComments(); + } + + protected function seedDepts() + { $depts = [ - ['name' => 'Chemistry'], - ['name' => 'Hematology'], - ['name' => 'Immunology'], - ['name' => 'Urinalysis'], + ['dept_name' => 'Chemistry'], + ['dept_name' => 'Hematology'], + ['dept_name' => 'Immunology'], + ['dept_name' => 'Urinalysis'], ]; $this->db->table('master_depts')->insertBatch($depts); - $deptIds = $this->db->table('master_depts')->select('dept_id')->get()->getResultArray(); - $deptIdMap = array_column($deptIds, 'dept_id'); + } - // 2. Insert Controls (6 entries - 2 per dept for first 3 depts) + protected function seedControls() + { $controls = [ - ['dept_id' => $deptIdMap[0], 'name' => 'QC Normal Chemistry', 'lot' => 'QC2024001', 'producer' => 'BioRad', 'exp_date' => '2025-12-31'], - ['dept_id' => $deptIdMap[0], 'name' => 'QC High Chemistry', 'lot' => 'QC2024002', 'producer' => 'BioRad', 'exp_date' => '2025-12-31'], - ['dept_id' => $deptIdMap[1], 'name' => 'QC Normal Hema', 'lot' => 'QC2024003', 'producer' => 'Streck', 'exp_date' => '2025-11-30'], - ['dept_id' => $deptIdMap[1], 'name' => 'QC Low Hema', 'lot' => 'QC2024004', 'producer' => 'Streck', 'exp_date' => '2025-11-30'], - ['dept_id' => $deptIdMap[2], 'name' => 'QC Normal Immuno', 'lot' => 'QC2024005', 'producer' => 'Roche', 'exp_date' => '2025-10-31'], - ['dept_id' => $deptIdMap[3], 'name' => 'QC Normal Urine', 'lot' => 'QC2024006', 'producer' => 'Siemens', 'exp_date' => '2025-09-30'], + ['dept_id' => 1, 'control_name' => 'QC Normal Chemistry', 'lot' => 'QC2024001', 'producer' => 'BioRad', 'exp_date' => '2025-12-31'], + ['dept_id' => 1, 'control_name' => 'QC High Chemistry', 'lot' => 'QC2024002', 'producer' => 'BioRad', 'exp_date' => '2025-12-31'], + ['dept_id' => 2, 'control_name' => 'QC Normal Hema', 'lot' => 'QC2024003', 'producer' => 'Streck', 'exp_date' => '2025-11-30'], + ['dept_id' => 2, 'control_name' => 'QC Low Hema', 'lot' => 'QC2024004', 'producer' => 'Streck', 'exp_date' => '2025-11-30'], + ['dept_id' => 3, 'control_name' => 'QC Normal Immuno', 'lot' => 'QC2024005', 'producer' => 'Roche', 'exp_date' => '2025-10-31'], + ['dept_id' => 4, 'control_name' => 'QC Normal Urine', 'lot' => 'QC2024006', 'producer' => 'Siemens', 'exp_date' => '2025-09-30'], + // New controls for January 2026 + ['dept_id' => 1, 'control_name' => 'Trulab N', 'lot' => 'TN2026001', 'producer' => 'Trinity', 'exp_date' => '2026-12-31'], + ['dept_id' => 1, 'control_name' => 'Trulab P', 'lot' => 'TP2026001', 'producer' => 'Trinity', 'exp_date' => '2026-12-31'], + ['dept_id' => 1, 'control_name' => 'Cholestest', 'lot' => 'CT2026001', 'producer' => 'Roche', 'exp_date' => '2026-12-31'], ]; $this->db->table('master_controls')->insertBatch($controls); - $controlIds = $this->db->table('master_controls')->select('control_id')->get()->getResultArray(); - $controlIdMap = array_column($controlIds, 'control_id'); + } - // 3. Insert Tests (10 entries) + protected function seedTests() + { $tests = [ - ['dept_id' => $deptIdMap[0], 'name' => 'Glucose', 'unit' => 'mg/dL', 'method' => 'GOD-PAP', 'cva' => 5, 'ba' => 3, 'tea' => 10], - ['dept_id' => $deptIdMap[0], 'name' => 'Creatinine', 'unit' => 'mg/dL', 'method' => 'Jaffe', 'cva' => 4, 'ba' => 2, 'tea' => 8], - ['dept_id' => $deptIdMap[0], 'name' => 'Urea Nitrogen', 'unit' => 'mg/dL', 'method' => 'UREASE', 'cva' => 5, 'ba' => 3, 'tea' => 12], - ['dept_id' => $deptIdMap[0], 'name' => 'Cholesterol', 'unit' => 'mg/dL', 'method' => 'CHOD-PAP', 'cva' => 6, 'ba' => 4, 'tea' => 15], - ['dept_id' => $deptIdMap[1], 'name' => 'WBC', 'unit' => 'x10^3/uL', 'method' => 'Impedance', 'cva' => 8, 'ba' => 5, 'tea' => 20], - ['dept_id' => $deptIdMap[1], 'name' => 'RBC', 'unit' => 'x10^6/uL', 'method' => 'Impedance', 'cva' => 3, 'ba' => 2, 'tea' => 8], - ['dept_id' => $deptIdMap[1], 'name' => 'Hemoglobin', 'unit' => 'g/dL', 'method' => 'Cyanmethemoglobin', 'cva' => 2, 'ba' => 1, 'tea' => 5], - ['dept_id' => $deptIdMap[2], 'name' => 'TSH', 'unit' => 'mIU/L', 'method' => 'ECLIA', 'cva' => 10, 'ba' => 6, 'tea' => 25], - ['dept_id' => $deptIdMap[2], 'name' => 'Free T4', 'unit' => 'ng/dL', 'method' => 'ECLIA', 'cva' => 8, 'ba' => 5, 'tea' => 20], - ['dept_id' => $deptIdMap[3], 'name' => 'Urine Protein', 'unit' => 'mg/dL', 'method' => 'Dipstick', 'cva' => 10, 'ba' => 8, 'tea' => 30], + ['dept_id' => 1, 'test_code' => 'GLU', 'test_name' => 'Glucose', 'test_unit' => 'mg/dL', 'test_method' => 'GOD-PAP', 'cva' => 5, 'ba' => 3, 'tea' => 10], + ['dept_id' => 1, 'test_code' => 'CRE', 'test_name' => 'Creatinine', 'test_unit' => 'mg/dL', 'test_method' => 'Jaffe', 'cva' => 4, 'ba' => 2, 'tea' => 8], + ['dept_id' => 1, 'test_code' => 'BUN', 'test_name' => 'Urea Nitrogen', 'test_unit' => 'mg/dL', 'test_method' => 'UREASE', 'cva' => 5, 'ba' => 3, 'tea' => 12], + ['dept_id' => 1, 'test_code' => 'CHOL', 'test_name' => 'Cholesterol', 'test_unit' => 'mg/dL', 'test_method' => 'CHOD-PAP', 'cva' => 6, 'ba' => 4, 'tea' => 15], + ['dept_id' => 2, 'test_code' => 'WBC', 'test_name' => 'WBC', 'test_unit' => 'x10^3/uL', 'test_method' => 'Impedance', 'cva' => 8, 'ba' => 5, 'tea' => 20], + ['dept_id' => 2, 'test_code' => 'RBC', 'test_name' => 'RBC', 'test_unit' => 'x10^6/uL', 'test_method' => 'Impedance', 'cva' => 3, 'ba' => 2, 'tea' => 8], + ['dept_id' => 2, 'test_code' => 'HGB', 'test_name' => 'Hemoglobin', 'test_unit' => 'g/dL', 'test_method' => 'Cyanmethemoglobin', 'cva' => 2, 'ba' => 1, 'tea' => 5], + ['dept_id' => 3, 'test_code' => 'TSH', 'test_name' => 'TSH', 'test_unit' => 'mIU/L', 'test_method' => 'ECLIA', 'cva' => 10, 'ba' => 6, 'tea' => 25], + ['dept_id' => 3, 'test_code' => 'FT4', 'test_name' => 'Free T4', 'test_unit' => 'ng/dL', 'test_method' => 'ECLIA', 'cva' => 8, 'ba' => 5, 'tea' => 20], + ['dept_id' => 4, 'test_code' => 'UP', 'test_name' => 'Urine Protein', 'test_unit' => 'mg/dL', 'test_method' => 'Dipstick', 'cva' => 10, 'ba' => 8, 'tea' => 30], ]; $this->db->table('master_tests')->insertBatch($tests); - $testIds = $this->db->table('master_tests')->select('test_id')->get()->getResultArray(); - $testIdMap = array_column($testIds, 'test_id'); + } - // 4. Insert Control-Tests (15 entries - 3 per control for first 5 controls) + protected function seedControlTests() + { $controlTests = [ - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[0], 'mean' => 95, 'sd' => 5], - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[1], 'mean' => 1.0, 'sd' => 0.05], - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[2], 'mean' => 15, 'sd' => 1.2], - ['control_id' => $controlIdMap[1], 'test_id' => $testIdMap[0], 'mean' => 180, 'sd' => 12], - ['control_id' => $controlIdMap[1], 'test_id' => $testIdMap[1], 'mean' => 2.5, 'sd' => 0.15], - ['control_id' => $controlIdMap[1], 'test_id' => $testIdMap[3], 'mean' => 200, 'sd' => 15], - ['control_id' => $controlIdMap[2], 'test_id' => $testIdMap[4], 'mean' => 7.5, 'sd' => 0.6], - ['control_id' => $controlIdMap[2], 'test_id' => $testIdMap[5], 'mean' => 4.8, 'sd' => 0.2], - ['control_id' => $controlIdMap[2], 'test_id' => $testIdMap[6], 'mean' => 14.5, 'sd' => 0.5], - ['control_id' => $controlIdMap[3], 'test_id' => $testIdMap[4], 'mean' => 3.5, 'sd' => 0.3], - ['control_id' => $controlIdMap[3], 'test_id' => $testIdMap[5], 'mean' => 2.5, 'sd' => 0.15], - ['control_id' => $controlIdMap[4], 'test_id' => $testIdMap[7], 'mean' => 2.5, 'sd' => 0.3], - ['control_id' => $controlIdMap[4], 'test_id' => $testIdMap[8], 'mean' => 1.2, 'sd' => 0.1], - ['control_id' => $controlIdMap[5], 'test_id' => $testIdMap[9], 'mean' => 10, 'sd' => 1.5], - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[3], 'mean' => 150, 'sd' => 10], + ['control_id' => 1, 'test_id' => 1, 'mean' => 95, 'sd' => 5], + ['control_id' => 1, 'test_id' => 2, 'mean' => 1.0, 'sd' => 0.05], + ['control_id' => 1, 'test_id' => 3, 'mean' => 15, 'sd' => 1.2], + ['control_id' => 2, 'test_id' => 1, 'mean' => 180, 'sd' => 12], + ['control_id' => 2, 'test_id' => 2, 'mean' => 2.5, 'sd' => 0.15], + ['control_id' => 2, 'test_id' => 4, 'mean' => 200, 'sd' => 15], + ['control_id' => 3, 'test_id' => 5, 'mean' => 7.5, 'sd' => 0.6], + ['control_id' => 3, 'test_id' => 6, 'mean' => 4.8, 'sd' => 0.2], + ['control_id' => 3, 'test_id' => 7, 'mean' => 14.5, 'sd' => 0.5], + ['control_id' => 4, 'test_id' => 5, 'mean' => 3.5, 'sd' => 0.3], + ['control_id' => 4, 'test_id' => 6, 'mean' => 2.5, 'sd' => 0.15], + ['control_id' => 5, 'test_id' => 8, 'mean' => 2.5, 'sd' => 0.3], + ['control_id' => 5, 'test_id' => 9, 'mean' => 1.2, 'sd' => 0.1], + ['control_id' => 6, 'test_id' => 10, 'mean' => 10, 'sd' => 1.5], + ['control_id' => 1, 'test_id' => 4, 'mean' => 150, 'sd' => 10], + // New control-tests for January 2026 + ['control_id' => 7, 'test_id' => 1, 'mean' => 90, 'sd' => 4], // Trulab N - Glucose + ['control_id' => 7, 'test_id' => 2, 'mean' => 0.9, 'sd' => 0.04], // Trulab N - Creatinine + ['control_id' => 7, 'test_id' => 4, 'mean' => 145, 'sd' => 8], // Trulab N - Cholesterol + ['control_id' => 8, 'test_id' => 1, 'mean' => 175, 'sd' => 10], // Trulab P - Glucose + ['control_id' => 8, 'test_id' => 2, 'mean' => 2.4, 'sd' => 0.12], // Trulab P - Creatinine + ['control_id' => 8, 'test_id' => 4, 'mean' => 195, 'sd' => 12], // Trulab P - Cholesterol + ['control_id' => 9, 'test_id' => 4, 'mean' => 180, 'sd' => 10], // Cholestest - Cholesterol ]; $this->db->table('control_tests')->insertBatch($controlTests); - $ctRows = $this->db->table('control_tests')->select('control_test_id, control_id, test_id')->get()->getResultArray(); + } - // 5. Insert Results (50 entries - random values around mean) - $results = []; + protected function seedResults() + { $faker = \Faker\Factory::create(); - $resultDate = date('2024-12-01'); - - // Pre-calculate control_test info for result generation - $ctInfo = []; - foreach ($ctRows as $ct) { - $key = $ct['control_id'] . '-' . $ct['test_id']; - $ctInfo[$key] = $ct['control_test_id']; - } + $resultDate = '2026-01-01'; + $results = []; + $controlTests = $this->db->table('control_tests')->get()->getResultArray(); $resultCount = 0; - foreach ($ctRows as $ct) { - // Generate 3-4 results per control-test + + foreach ($controlTests as $ct) { $numResults = $faker->numberBetween(3, 4); for ($i = 0; $i < $numResults && $resultCount < 50; $i++) { - // Generate random date within December 2024 $resDate = date('Y-m-d', strtotime($resultDate . ' +' . $faker->numberBetween(0, 20) . ' days')); - - // Get mean/sd for value generation - $mean = $this->db->table('control_tests') - ->where('control_test_id', $ct['control_test_id']) - ->get() - ->getRowArray()['mean'] ?? 100; - $sd = $this->db->table('control_tests') - ->where('control_test_id', $ct['control_test_id']) - ->get() - ->getRowArray()['sd'] ?? 5; - - // Generate value within +/- 2-3 SD - $value = $mean + ($faker->randomFloat(2, -2.5, 2.5) * $sd); + $value = $ct['mean'] + ($faker->randomFloat(2, -2.5, 2.5) * $ct['sd']); $results[] = [ 'control_id' => $ct['control_id'], 'test_id' => $ct['test_id'], 'res_date' => $resDate, 'res_value' => round($value, 2), - 'res_comment' => null, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]; @@ -117,21 +119,59 @@ class CmodQcSeeder extends Seeder } } $this->db->table('results')->insertBatch($results); - $resultIds = $this->db->table('results')->select('result_id, control_id, test_id, res_date')->get()->getResultArray(); + } - // 6. Insert Result Comments (10 entries - monthly comments for various control-test combos) - $resultComments = [ - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[0], 'comment_month' => '2024-12', 'com_text' => 'Slight drift observed, instrument recalibrated on 12/15'], - ['control_id' => $controlIdMap[1], 'test_id' => $testIdMap[3], 'comment_month' => '2024-12', 'com_text' => 'High cholesterol values noted, lot change recommended'], - ['control_id' => $controlIdMap[2], 'test_id' => $testIdMap[4], 'comment_month' => '2024-12', 'com_text' => 'WBC controls stable throughout the month'], - ['control_id' => $controlIdMap[3], 'test_id' => $testIdMap[5], 'comment_month' => '2024-12', 'com_text' => 'RBC QC intermittent shift, probe cleaned'], - ['control_id' => $controlIdMap[4], 'test_id' => $testIdMap[7], 'comment_month' => '2024-12', 'com_text' => 'TSH assay maintenance performed on 12/10'], - ['control_id' => $controlIdMap[5], 'test_id' => $testIdMap[9], 'comment_month' => '2024-12', 'com_text' => 'Urine protein controls within range'], - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[1], 'comment_month' => '2024-12', 'com_text' => 'Creatinine QC stable, no issues'], - ['control_id' => $controlIdMap[1], 'test_id' => $testIdMap[0], 'comment_month' => '2024-12', 'com_text' => 'Glucose high QC showed consistent elevation, reagent lot changed'], - ['control_id' => $controlIdMap[2], 'test_id' => $testIdMap[6], 'comment_month' => '2024-12', 'com_text' => 'Hemoglobin QC performance acceptable'], - ['control_id' => $controlIdMap[4], 'test_id' => $testIdMap[8], 'comment_month' => '2024-12', 'com_text' => 'Free T4 calibration curve verified'], + protected function seedResultComments() + { + // Get all results to associate comments with specific results + $results = $this->db->table('results')->get()->getResultArray(); + + if (empty($results)) { + return; + } + + // Map control_id + test_id to result_ids + $resultMap = []; + foreach ($results as $result) { + $key = $result['control_id'] . '_' . $result['test_id']; + $resultMap[$key][] = $result['result_id']; + } + + // Comments data with control_id + test_id for mapping + $commentsData = [ + ['control_id' => 1, 'test_id' => 1, 'comment_text' => 'Slight drift observed, instrument recalibrated on 01/15'], + ['control_id' => 2, 'test_id' => 4, 'comment_text' => 'High cholesterol values noted, lot change recommended'], + ['control_id' => 3, 'test_id' => 5, 'comment_text' => 'WBC controls stable throughout the month'], + ['control_id' => 4, 'test_id' => 6, 'comment_text' => 'RBC QC intermittent shift, probe cleaned'], + ['control_id' => 5, 'test_id' => 8, 'comment_text' => 'TSH assay maintenance performed on 01/10'], + ['control_id' => 6, 'test_id' => 10, 'comment_text' => 'Urine protein controls within range'], + ['control_id' => 1, 'test_id' => 2, 'comment_text' => 'Creatinine QC stable, no issues'], + ['control_id' => 2, 'test_id' => 1, 'comment_text' => 'Glucose high QC showed consistent elevation, reagent lot changed'], + ['control_id' => 3, 'test_id' => 7, 'comment_text' => 'Hemoglobin QC performance acceptable'], + ['control_id' => 5, 'test_id' => 9, 'comment_text' => 'Free T4 calibration curve verified'], + // New control comments for January 2026 + ['control_id' => 7, 'test_id' => 1, 'comment_text' => 'Trulab N Glucose stable throughout January'], + ['control_id' => 7, 'test_id' => 2, 'comment_text' => 'Trulab N Creatinine within acceptable range'], + ['control_id' => 7, 'test_id' => 4, 'comment_text' => 'Trulab N Cholesterol performance satisfactory'], + ['control_id' => 8, 'test_id' => 1, 'comment_text' => 'Trulab P Glucose elevated, monitoring continued'], + ['control_id' => 8, 'test_id' => 2, 'comment_text' => 'Trulab P Creatinine QC stable'], + ['control_id' => 8, 'test_id' => 4, 'comment_text' => 'Trulab P Cholesterol consistent with expected values'], + ['control_id' => 9, 'test_id' => 4, 'comment_text' => 'Cholestest performance verified, no issues'], ]; - $this->db->table('result_comments')->insertBatch($resultComments); + + $comments = []; + foreach ($commentsData as $data) { + $key = $data['control_id'] . '_' . $data['test_id']; + if (isset($resultMap[$key]) && !empty($resultMap[$key])) { + // Attach comment to the first matching result + $comments[] = [ + 'result_id' => $resultMap[$key][0], + 'comment_text' => $data['comment_text'], + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]; + } + } + $this->db->table('result_comments')->insertBatch($comments); } } diff --git a/app/Database/Seeds/LongExpiryQcSeeder.php b/app/Database/Seeds/LongExpiryQcSeeder.php deleted file mode 100644 index 67e16d3..0000000 --- a/app/Database/Seeds/LongExpiryQcSeeder.php +++ /dev/null @@ -1,189 +0,0 @@ - 'Chemistry'], - ['dept_name' => 'Hematology'], - ['dept_name' => 'Immunology'], - ['dept_name' => 'Urinalysis'], - ]; - $this->db->table('master_depts')->insertBatch($depts); - $deptIds = $this->db->table('master_depts')->select('dept_id')->get()->getResultArray(); - $deptIdMap = array_column($deptIds, 'dept_id'); - - // 2. Insert Controls with long expiry dates (2027-12-31) - $controls = [ - ['dept_id' => $deptIdMap[0], 'control_name' => 'QC Normal Chemistry', 'lot' => 'QC2026001', 'producer' => 'BioRad', 'exp_date' => '2027-12-31'], - ['dept_id' => $deptIdMap[0], 'control_name' => 'QC High Chemistry', 'lot' => 'QC2026002', 'producer' => 'BioRad', 'exp_date' => '2027-12-31'], - ['dept_id' => $deptIdMap[1], 'control_name' => 'QC Normal Hema', 'lot' => 'QC2026003', 'producer' => 'Streck', 'exp_date' => '2027-11-30'], - ['dept_id' => $deptIdMap[1], 'control_name' => 'QC Low Hema', 'lot' => 'QC2026004', 'producer' => 'Streck', 'exp_date' => '2027-11-30'], - ['dept_id' => $deptIdMap[2], 'control_name' => 'QC Normal Immuno', 'lot' => 'QC2026005', 'producer' => 'Roche', 'exp_date' => '2027-10-31'], - ['dept_id' => $deptIdMap[3], 'control_name' => 'QC Normal Urine', 'lot' => 'QC2026006', 'producer' => 'Siemens', 'exp_date' => '2027-09-30'], - ]; - $this->db->table('master_controls')->insertBatch($controls); - $controlIds = $this->db->table('master_controls')->select('control_id')->get()->getResultArray(); - $controlIdMap = array_column($controlIds, 'control_id'); - - // 3. Insert Tests (10 entries) - $tests = [ - ['dept_id' => $deptIdMap[0], 'test_name' => 'Glucose', 'test_unit' => 'mg/dL', 'test_method' => 'GOD-PAP', 'cva' => 5, 'ba' => 3, 'tea' => 10], - ['dept_id' => $deptIdMap[0], 'test_name' => 'Creatinine', 'test_unit' => 'mg/dL', 'test_method' => 'Jaffe', 'cva' => 4, 'ba' => 2, 'tea' => 8], - ['dept_id' => $deptIdMap[0], 'test_name' => 'Urea Nitrogen', 'test_unit' => 'mg/dL', 'test_method' => 'UREASE', 'cva' => 5, 'ba' => 3, 'tea' => 12], - ['dept_id' => $deptIdMap[0], 'test_name' => 'Cholesterol', 'test_unit' => 'mg/dL', 'test_method' => 'CHOD-PAP', 'cva' => 6, 'ba' => 4, 'tea' => 15], - ['dept_id' => $deptIdMap[1], 'test_name' => 'WBC', 'test_unit' => 'x10^3/uL', 'test_method' => 'Impedance', 'cva' => 8, 'ba' => 5, 'tea' => 20], - ['dept_id' => $deptIdMap[1], 'test_name' => 'RBC', 'test_unit' => 'x10^6/uL', 'test_method' => 'Impedance', 'cva' => 3, 'ba' => 2, 'tea' => 8], - ['dept_id' => $deptIdMap[1], 'test_name' => 'Hemoglobin', 'test_unit' => 'g/dL', 'test_method' => 'Cyanmethemoglobin', 'cva' => 2, 'ba' => 1, 'tea' => 5], - ['dept_id' => $deptIdMap[2], 'test_name' => 'TSH', 'test_unit' => 'mIU/L', 'test_method' => 'ECLIA', 'cva' => 10, 'ba' => 6, 'tea' => 25], - ['dept_id' => $deptIdMap[2], 'test_name' => 'Free T4', 'test_unit' => 'ng/dL', 'test_method' => 'ECLIA', 'cva' => 8, 'ba' => 5, 'tea' => 20], - ['dept_id' => $deptIdMap[3], 'test_name' => 'Urine Protein', 'test_unit' => 'mg/dL', 'test_method' => 'Dipstick', 'cva' => 10, 'ba' => 8, 'tea' => 30], - ]; - $this->db->table('master_tests')->insertBatch($tests); - $testIds = $this->db->table('master_tests')->select('test_id')->get()->getResultArray(); - $testIdMap = array_column($testIds, 'test_id'); - - // 4. Insert Control-Tests (15 entries - 3 per control for first 5 controls) - $controlTests = [ - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[0], 'mean' => 95, 'sd' => 5], - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[1], 'mean' => 1.0, 'sd' => 0.05], - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[2], 'mean' => 15, 'sd' => 1.2], - ['control_id' => $controlIdMap[1], 'test_id' => $testIdMap[0], 'mean' => 180, 'sd' => 12], - ['control_id' => $controlIdMap[1], 'test_id' => $testIdMap[1], 'mean' => 2.5, 'sd' => 0.15], - ['control_id' => $controlIdMap[1], 'test_id' => $testIdMap[3], 'mean' => 200, 'sd' => 15], - ['control_id' => $controlIdMap[2], 'test_id' => $testIdMap[4], 'mean' => 7.5, 'sd' => 0.6], - ['control_id' => $controlIdMap[2], 'test_id' => $testIdMap[5], 'mean' => 4.8, 'sd' => 0.2], - ['control_id' => $controlIdMap[2], 'test_id' => $testIdMap[6], 'mean' => 14.5, 'sd' => 0.5], - ['control_id' => $controlIdMap[3], 'test_id' => $testIdMap[4], 'mean' => 3.5, 'sd' => 0.3], - ['control_id' => $controlIdMap[3], 'test_id' => $testIdMap[5], 'mean' => 2.5, 'sd' => 0.15], - ['control_id' => $controlIdMap[4], 'test_id' => $testIdMap[7], 'mean' => 2.5, 'sd' => 0.3], - ['control_id' => $controlIdMap[4], 'test_id' => $testIdMap[8], 'mean' => 1.2, 'sd' => 0.1], - ['control_id' => $controlIdMap[5], 'test_id' => $testIdMap[9], 'mean' => 10, 'sd' => 1.5], - ['control_id' => $controlIdMap[0], 'test_id' => $testIdMap[3], 'mean' => 150, 'sd' => 10], - ]; - $this->db->table('control_tests')->insertBatch($controlTests); - $ctRows = $this->db->table('control_tests')->select('control_test_id, control_id, test_id, mean, sd')->get()->getResultArray(); - - // 5. Insert Results (90 entries - 6 per control-test, daily data spanning ~3 months) - $results = []; - $faker = \Faker\Factory::create(); - - // Start date: 3 months ago, generate daily entries - $startDate = date('2025-10-01'); - $daysToGenerate = 90; // ~3 months of daily data - - foreach ($ctRows as $ct) { - // Generate 6 results per control-test, spread across the date range - for ($i = 0; $i < 6; $i++) { - // Distribute results across the 90-day period - $dayOffset = floor(($i * $daysToGenerate) / 6) + $faker->numberBetween(0, 5); - $resDate = date('Y-m-d', strtotime($startDate . ' +' . $dayOffset . ' days')); - - // Generate value within +/- 2.5 SD - $value = $ct['mean'] + ($faker->randomFloat(2, -2.5, 2.5) * $ct['sd']); - - $results[] = [ - 'control_id' => $ct['control_id'], - 'test_id' => $ct['test_id'], - 'res_date' => $resDate, - 'res_value' => round($value, 2), - 'res_comment' => null, - 'created_at' => date('Y-m-d H:i:s'), - 'updated_at' => date('Y-m-d H:i:s'), - ]; - } - } - $this->db->table('results')->insertBatch($results); - - // 6. Insert Result Comments (60 entries - monthly comments for all control-test combos) - $resultComments = []; - $months = ['2025-10', '2025-11', '2025-12', '2026-01']; - - $commentTemplates = [ - '2025-10' => [ - ['control_id' => 0, 'test_id' => 0, 'text' => 'QC performance stable, all parameters within range for October'], - ['control_id' => 0, 'test_id' => 1, 'text' => 'Creatinine controls stable, no issues observed'], - ['control_id' => 0, 'test_id' => 2, 'text' => 'BUN QC within acceptable limits'], - ['control_id' => 1, 'test_id' => 0, 'text' => 'High glucose QC showed slight elevation, monitoring continued'], - ['control_id' => 1, 'test_id' => 3, 'text' => 'Cholesterol lot QC2026002 performs within specifications'], - ['control_id' => 2, 'test_id' => 4, 'text' => 'WBC counts consistent throughout the month'], - ['control_id' => 2, 'test_id' => 5, 'text' => 'RBC QC stable, no drift detected'], - ['control_id' => 2, 'test_id' => 6, 'text' => 'Hemoglobin controls within expected range'], - ['control_id' => 3, 'test_id' => 4, 'text' => 'Low WBC QC verified, precision acceptable'], - ['control_id' => 3, 'test_id' => 5, 'text' => 'Low RBC controls passed QC checks'], - ['control_id' => 4, 'test_id' => 7, 'text' => 'TSH assay calibration verified on 10/15'], - ['control_id' => 4, 'test_id' => 8, 'text' => 'Free T4 controls stable, no maintenance required'], - ['control_id' => 5, 'test_id' => 9, 'text' => 'Urine protein dipstick QC performing well'], - ['control_id' => 0, 'test_id' => 3, 'text' => 'Normal cholesterol QC within target range'], - ], - '2025-11' => [ - ['control_id' => 0, 'test_id' => 0, 'text' => 'Glucose QC showed minor drift, recalibration performed 11/10'], - ['control_id' => 0, 'test_id' => 1, 'text' => 'November creatinine QC results acceptable'], - ['control_id' => 0, 'test_id' => 2, 'text' => 'BUN controls stable after reagent lot change'], - ['control_id' => 1, 'test_id' => 0, 'text' => 'High glucose QC consistent after recalibration'], - ['control_id' => 1, 'test_id' => 3, 'text' => 'Cholesterol QC performance improved after maintenance'], - ['control_id' => 2, 'test_id' => 4, 'text' => 'WBC QC acceptable, precision within specifications'], - ['control_id' => 2, 'test_id' => 5, 'text' => 'RBC controls verified, no issues'], - ['control_id' => 2, 'test_id' => 6, 'text' => 'Hemoglobin QC stable, Levey-Jenkins chart within limits'], - ['control_id' => 3, 'test_id' => 4, 'text' => 'Low WBC QC passed all QC checks for November'], - ['control_id' => 3, 'test_id' => 5, 'text' => 'Low RBC controls stable throughout month'], - ['control_id' => 4, 'test_id' => 7, 'text' => 'TSH controls within range, no action required'], - ['control_id' => 4, 'test_id' => 8, 'text' => 'Free T4 assay verification complete'], - ['control_id' => 5, 'test_id' => 9, 'text' => 'Urine protein QC lot QC2026006 performing well'], - ['control_id' => 0, 'test_id' => 3, 'text' => 'Normal cholesterol lot QC2026001 verified'], - ], - '2025-12' => [ - ['control_id' => 0, 'test_id' => 0, 'text' => 'December glucose QC stable, no issues'], - ['control_id' => 0, 'test_id' => 1, 'text' => 'Creatinine QC performance acceptable for year-end'], - ['control_id' => 0, 'test_id' => 2, 'text' => 'BUN controls within range, holiday period monitoring'], - ['control_id' => 1, 'test_id' => 0, 'text' => 'High glucose QC stable after lot stabilization'], - ['control_id' => 1, 'test_id' => 3, 'text' => 'Cholesterol QC trending well, within 2SD'], - ['control_id' => 2, 'test_id' => 4, 'text' => 'WBC QC stable, year-end verification complete'], - ['control_id' => 2, 'test_id' => 5, 'text' => 'RBC controls verified, all parameters acceptable'], - ['control_id' => 2, 'test_id' => 6, 'text' => 'Hemoglobin QC within expected range'], - ['control_id' => 3, 'test_id' => 4, 'text' => 'Low WBC QC passed December checks'], - ['control_id' => 3, 'test_id' => 5, 'text' => 'Low RBC controls stable'], - ['control_id' => 4, 'test_id' => 7, 'text' => 'TSH controls within specifications'], - ['control_id' => 4, 'test_id' => 8, 'text' => 'Free T4 QC acceptable for December'], - ['control_id' => 5, 'test_id' => 9, 'text' => 'Urine protein QC no issues reported'], - ['control_id' => 0, 'test_id' => 3, 'text' => 'Cholesterol QC lot QC2026001 verified'], - ], - '2026-01' => [ - ['control_id' => 0, 'test_id' => 0, 'text' => 'January glucose QC started well, new year verification'], - ['control_id' => 0, 'test_id' => 1, 'text' => 'Creatinine QC performance consistent'], - ['control_id' => 0, 'test_id' => 2, 'text' => 'BUN controls within expected parameters'], - ['control_id' => 1, 'test_id' => 0, 'text' => 'High glucose QC stable start to new year'], - ['control_id' => 1, 'test_id' => 3, 'text' => 'Cholesterol QC lot QC2026002 verified'], - ['control_id' => 2, 'test_id' => 4, 'text' => 'WBC QC started new year within specifications'], - ['control_id' => 2, 'test_id' => 5, 'text' => 'RBC controls performing as expected'], - ['control_id' => 2, 'test_id' => 6, 'text' => 'Hemoglobin QC acceptable'], - ['control_id' => 3, 'test_id' => 4, 'text' => 'Low WBC QC passed January checks'], - ['control_id' => 3, 'test_id' => 5, 'text' => 'Low RBC controls verified'], - ['control_id' => 4, 'test_id' => 7, 'text' => 'TSH controls within range'], - ['control_id' => 4, 'test_id' => 8, 'text' => 'Free T4 QC stable'], - ['control_id' => 5, 'test_id' => 9, 'text' => 'Urine protein QC performing well'], - ['control_id' => 0, 'test_id' => 3, 'text' => 'Cholesterol QC lot QC2026001 verified'], - ], - ]; - - foreach ($months as $month) { - if (isset($commentTemplates[$month])) { - foreach ($commentTemplates[$month] as $comment) { - $resultComments[] = [ - 'control_id' => $controlIdMap[$comment['control_id']], - 'test_id' => $testIdMap[$comment['test_id']], - 'comment_month' => $month, - 'com_text' => $comment['text'], - ]; - } - } - } - $this->db->table('result_comments')->insertBatch($resultComments); - } -} diff --git a/app/Models/Master/MasterTestsModel.php b/app/Models/Master/MasterTestsModel.php index 197f8ca..c19c7ee 100644 --- a/app/Models/Master/MasterTestsModel.php +++ b/app/Models/Master/MasterTestsModel.php @@ -8,6 +8,7 @@ class MasterTestsModel extends BaseModel { protected $primaryKey = 'test_id'; protected $allowedFields = [ 'dept_id', + 'test_code', 'test_name', 'test_unit', 'test_method', @@ -22,12 +23,30 @@ class MasterTestsModel extends BaseModel { protected $useSoftDeletes = true; public function search($keyword = null) { + $builder = $this->builder(); + $builder->select(' + master_tests.test_id as testId, + master_tests.test_code as testCode, + master_tests.test_name as testName, + master_tests.test_unit as testUnit, + master_tests.test_method as testMethod, + master_tests.cva, + master_tests.ba, + master_tests.tea, + master_depts.dept_name as deptName + '); + $builder->join('master_depts', 'master_depts.dept_id = master_tests.dept_id', 'left'); + $builder->where('master_tests.deleted_at', null); + if ($keyword) { - return $this->groupStart() - ->like('test_name', $keyword) - ->groupEnd() - ->findAll(); + $builder->groupStart() + ->like('master_tests.test_name', $keyword) + ->groupEnd(); } - return $this->findAll(); + + $builder->groupBy('master_tests.test_id, master_depts.dept_name'); + $builder->orderBy('master_tests.test_name', 'ASC'); + + return $builder->get()->getResultArray(); } } diff --git a/app/Models/Qc/ControlTestsModel.php b/app/Models/Qc/ControlTestsModel.php index c9f884b..77748a7 100644 --- a/app/Models/Qc/ControlTestsModel.php +++ b/app/Models/Qc/ControlTestsModel.php @@ -3,7 +3,8 @@ namespace App\Models\Qc; use App\Models\BaseModel; -class ControlTestsModel extends BaseModel { +class ControlTestsModel extends BaseModel +{ protected $table = 'control_tests'; protected $primaryKey = 'control_test_id'; protected $allowedFields = [ @@ -18,20 +19,44 @@ class ControlTestsModel extends BaseModel { protected $useTimestamps = true; protected $useSoftDeletes = true; - public function search($keyword = null) { + public function search($keyword = null) + { + $builder = $this->db->table($this->table . ' ct'); + $builder->select(' + ct.control_test_id, + ct.control_id, + ct.test_id, + ct.mean, + ct.sd, + c.control_name, + c.lot, + t.test_name, + t.test_unit + '); + $builder->join('master_controls c', 'c.control_id = ct.control_id'); + $builder->join('master_tests t', 't.test_id = ct.test_id'); + $builder->where('ct.deleted_at', null); + if ($keyword) { - return $this->groupStart() - ->like('mean', $keyword) - ->groupEnd() - ->findAll(); + $builder->groupStart() + ->like('c.control_name', $keyword) + ->orLike('t.test_name', $keyword) + ->orLike('c.lot', $keyword) + ->groupEnd(); } - return $this->findAll(); + + $builder->orderBy('c.control_name', 'ASC'); + $builder->orderBy('t.test_name', 'ASC'); + + $rows = $builder->get()->getResultArray(); + return $this->snakeToCamelRecursive($rows); } /** * Get control-test with control and test details */ - public function getWithDetails(int $controlTestId): ?array { + public function getWithDetails(int $controlTestId): ?array + { $builder = $this->db->table('control_tests ct'); $builder->select(' ct.control_test_id as id, @@ -55,7 +80,8 @@ class ControlTestsModel extends BaseModel { /** * Get tests for a control with QC parameters */ - public function getByControl(int $controlId): array { + public function getByControl(int $controlId): array + { $builder = $this->db->table('control_tests ct'); $builder->select(' ct.control_test_id as id, @@ -77,8 +103,10 @@ class ControlTestsModel extends BaseModel { /** * Get controls for a test with QC parameters + * Optionally filter by month to exclude expired controls */ - public function getByTest(int $testId): array { + public function getByTest(int $testId, ?string $month = null): array + { $builder = $this->db->table('control_tests ct'); $builder->select(' ct.control_test_id as id, @@ -88,12 +116,20 @@ class ControlTestsModel extends BaseModel { ct.sd, c.control_name as controlName, c.lot, - c.producer + c.producer, + c.exp_date as expDate '); $builder->join('master_controls c', 'c.control_id = ct.control_id'); $builder->where('ct.test_id', $testId); $builder->where('ct.deleted_at', null); $builder->where('c.deleted_at', null); + + // Filter out expired controls if month provided + if ($month) { + $monthEnd = $month . '-01'; + $builder->where('c.exp_date >=', $monthEnd); + } + $builder->orderBy('c.control_name', 'ASC'); return $builder->get()->getResultArray(); @@ -102,7 +138,8 @@ class ControlTestsModel extends BaseModel { /** * Get by control and test */ - public function getByControlAndTest(int $controlId, int $testId): ?array { + public function getByControlAndTest(int $controlId, int $testId): ?array + { $builder = $this->db->table('control_tests ct'); $builder->select(' ct.control_test_id as id, diff --git a/app/Models/Qc/ResultCommentsModel.php b/app/Models/Qc/ResultCommentsModel.php index 50fae4f..28af2d7 100644 --- a/app/Models/Qc/ResultCommentsModel.php +++ b/app/Models/Qc/ResultCommentsModel.php @@ -7,10 +7,8 @@ class ResultCommentsModel extends BaseModel { protected $table = 'result_comments'; protected $primaryKey = 'result_comment_id'; protected $allowedFields = [ - 'control_id', - 'test_id', - 'comment_month', - 'com_text', + 'result_id', + 'comment_text', 'created_at', 'updated_at', 'deleted_at' @@ -21,8 +19,7 @@ class ResultCommentsModel extends BaseModel { public function search($keyword = null) { if ($keyword) { return $this->groupStart() - ->like('comment_month', $keyword) - ->orLike('com_text', $keyword) + ->like('comment_text', $keyword) ->groupEnd() ->findAll(); } @@ -30,38 +27,115 @@ class ResultCommentsModel extends BaseModel { } /** - * Get comments by control, test and month + * Get comments by result_id */ - public function getByControlTestMonth(int $controlId, int $testId, string $month): ?array { - return $this->where('control_id', $controlId) - ->where('test_id', $testId) - ->where('comment_month', $month) + public function getByResult(int $resultId): ?array { + return $this->where('result_id', $resultId) ->where('deleted_at', null) ->first(); } /** - * Get all comments for a test and month + * Get all comments for a control+test combination (via results) */ - public function getByTestMonth(int $testId, string $month): array { - return $this->where('test_id', $testId) - ->where('comment_month', $month) + public function getByControlTest(int $controlId, int $testId): ?array { + // First get result IDs for this control+test + $db = \Config\Database::connect(); + $results = $db->table('results') + ->select('result_id') + ->where('control_id', $controlId) + ->where('test_id', $testId) + ->where('deleted_at', null) + ->get() + ->getResultArray(); + + if (empty($results)) { + return null; + } + + $resultIds = array_column($results, 'result_id'); + return $this->whereIn('result_id', $resultIds) + ->where('deleted_at', null) + ->orderBy('created_at', 'DESC') + ->findAll(); + } + + /** + * Get all comments for a test (via results) + */ + public function getByTest(int $testId): array { + $db = \Config\Database::connect(); + $results = $db->table('results') + ->select('result_id') + ->where('test_id', $testId) + ->where('deleted_at', null) + ->get() + ->getResultArray(); + + if (empty($results)) { + return []; + } + + $resultIds = array_column($results, 'result_id'); + return $this->whereIn('result_id', $resultIds) ->where('deleted_at', null) ->findAll(); } /** - * Upsert comment (insert or update based on control/test/month) + * Get comments by control, test, and month (for reports) + */ + public function getByControlTestMonth(int $controlId, int $testId, string $month): ?array { + $db = \Config\Database::connect(); + $results = $db->table('results') + ->select('result_id') + ->where('control_id', $controlId) + ->where('test_id', $testId) + ->where('res_date >=', $month . '-01') + ->where('res_date <=', $month . '-31') + ->where('deleted_at', null) + ->get() + ->getResultArray(); + + if (empty($results)) { + return null; + } + + $resultIds = array_column($results, 'result_id'); + $comments = $this->whereIn('result_id', $resultIds) + ->where('deleted_at', null) + ->orderBy('created_at', 'DESC') + ->findAll(); + + return $comments ?: null; + } + + /** + * Get comments for multiple results + */ + public function getByResultIds(array $resultIds): array { + if (empty($resultIds)) { + return []; + } + return $this->whereIn('result_id', $resultIds) + ->where('deleted_at', null) + ->findAll(); + } + + /** + * Upsert comment for a result */ public function upsertComment(array $data): int { - $existing = $this->where('control_id', $data['control_id']) - ->where('test_id', $data['test_id']) - ->where('comment_month', $data['comment_month']) + if (!isset($data['result_id'])) { + return 0; + } + + $existing = $this->where('result_id', $data['result_id']) ->where('deleted_at', null) ->first(); if ($existing) { - if (empty($data['com_text'])) { + if (empty($data['comment_text'])) { // If text is empty, soft delete $this->update($existing['result_comment_id'], ['deleted_at' => date('Y-m-d H:i:s')]); return $existing['result_comment_id']; @@ -69,7 +143,7 @@ class ResultCommentsModel extends BaseModel { $this->update($existing['result_comment_id'], $data); return $existing['result_comment_id']; } else { - if (empty($data['com_text'])) { + if (empty($data['comment_text'])) { return 0; // Don't insert empty comments } return $this->insert($data, true); diff --git a/app/Models/Qc/ResultsModel.php b/app/Models/Qc/ResultsModel.php index 64ce0f1..7db0510 100644 --- a/app/Models/Qc/ResultsModel.php +++ b/app/Models/Qc/ResultsModel.php @@ -11,7 +11,6 @@ class ResultsModel extends BaseModel { 'test_id', 'res_date', 'res_value', - 'res_comment', 'created_at', 'updated_at', 'deleted_at' @@ -40,8 +39,9 @@ class ResultsModel extends BaseModel { r.test_id as testId, r.res_date as resDate, r.res_value as resValue, - r.res_comment as resComment + rc.comment_text as resComment '); + $builder->join('result_comments rc', 'rc.result_id = r.result_id AND rc.deleted_at IS NULL', 'left'); $builder->where('r.res_date', $date); $builder->where('r.control_id', $controlId); $builder->where('r.deleted_at', null); @@ -60,8 +60,9 @@ class ResultsModel extends BaseModel { r.test_id as testId, r.res_date as resDate, r.res_value as resValue, - r.res_comment as resComment + rc.comment_text as resComment '); + $builder->join('result_comments rc', 'rc.result_id = r.result_id AND rc.deleted_at IS NULL', 'left'); $builder->where('r.test_id', $testId); $builder->where('r.res_date >=', $month . '-01'); $builder->where('r.res_date <=', $month . '-31'); @@ -79,8 +80,10 @@ class ResultsModel extends BaseModel { $builder->select(' r.result_id as id, r.res_date as resDate, - r.res_value as resValue + r.res_value as resValue, + rc.comment_text as resComment '); + $builder->join('result_comments rc', 'rc.result_id = r.result_id AND rc.deleted_at IS NULL', 'left'); $builder->where('r.control_id', $controlId); $builder->where('r.test_id', $testId); $builder->where('r.res_date >=', $month . '-01'); diff --git a/app/Views/dashboard.php b/app/Views/dashboard.php index 5c3a890..421129a 100644 --- a/app/Views/dashboard.php +++ b/app/Views/dashboard.php @@ -79,6 +79,7 @@ Date Control + Lot Test Value Range (Mean ± 2SD) @@ -90,6 +91,7 @@ + diff --git a/app/Views/entry/daily.php b/app/Views/entry/daily.php index 177503f..6f0bcdf 100644 --- a/app/Views/entry/daily.php +++ b/app/Views/entry/daily.php @@ -28,7 +28,7 @@ x-model="date" @change="fetchControls()" :max="today" - class="input input-bordered w-40"> + class="input input-bordered input-sm w-40">