From 14baa6b75877924f3eec812f97fcb4af663c530c Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Tue, 20 Jan 2026 14:44:46 +0700 Subject: [PATCH] docs: add comprehensive documentation and refactor API structure This commit introduces a complete documentation suite, refactors the API layer for better consistency, and updates the database schema and seeding logic. Key Changes: - Documentation: - Added `CLAUDE.md` with development guidelines and architecture overview. - Created `docs/` directory with detailed guides for architecture, development, and source tree analysis. - Database & Migrations: - Implemented `RenameMasterColumns` migration to standardize column naming (e.g., `name` -> `dept_name`, `name` -> `control_name`). - Added `CmodQcSeeder` to populate the system with realistic sample data for depts, controls, tests, and results. - Backend API: - Created `DashboardApiController` with `getRecent()` for dashboard stats. - Created `ReportApiController` for managed reporting access. - Updated `app/Config/Routes.php` with new API groupings and documentation routes. - Frontend & Views: - Refactored master data views (`dept`, `test`, `control`) to use Alpine.js and the updated API structure. - Modernized `dashboard.php` and `main_layout.php` with improved UI/UX. - Infrastructure: - Updated `.gitignore` to exclude development-specific artifacts (`_bmad/`, `.claude/`). --- .gitignore | 4 + CLAUDE.md | 172 +++++++ app/Config/Routes.php | 1 + .../Api/DashboardApiController.php | 85 ++++ app/Controllers/Api/ReportApiController.php | 119 +++++ ...2026-01-17-000001-QualityControlSystem.php | 107 ----- ...026-01-17-000001_QualityControlSystem.php} | 6 + ...-01-18-000001-RenameDictToMasterTables.php | 45 -- .../2026-01-20-000001_RenameMasterColumns.php | 36 ++ app/Database/Migrations/test.php | 10 - app/Database/Seeds/CmodQcSeeder.php | 137 ++++++ app/Models/Master/MasterControlsModel.php | 4 +- app/Models/Master/MasterDeptsModel.php | 4 +- app/Models/Master/MasterTestsModel.php | 8 +- app/Views/dashboard.php | 163 ++++--- app/Views/layout/main_layout.php | 75 +-- .../master/control/dialog_control_form.php | 2 +- app/Views/master/control/index.php | 22 +- app/Views/master/dept/index.php | 36 +- app/Views/master/test/index.php | 14 +- docs/architecture.md | 282 ++++++++++++ docs/development-guide.md | 435 ++++++++++++++++++ docs/index.md | 128 ++++++ docs/project-overview.md | 102 ++++ docs/source-tree-analysis.md | 178 +++++++ 25 files changed, 1889 insertions(+), 286 deletions(-) create mode 100644 CLAUDE.md create mode 100644 app/Controllers/Api/DashboardApiController.php create mode 100644 app/Controllers/Api/ReportApiController.php delete mode 100644 app/Database/Migrations/2026-01-17-000001-QualityControlSystem.php rename app/Database/Migrations/{2026-01-18-000001-QualityControlSystem.php => 2026-01-17-000001_QualityControlSystem.php} (94%) delete mode 100644 app/Database/Migrations/2026-01-18-000001-RenameDictToMasterTables.php create mode 100644 app/Database/Migrations/2026-01-20-000001_RenameMasterColumns.php delete mode 100644 app/Database/Migrations/test.php create mode 100644 app/Database/Seeds/CmodQcSeeder.php create mode 100644 docs/architecture.md create mode 100644 docs/development-guide.md create mode 100644 docs/index.md create mode 100644 docs/project-overview.md create mode 100644 docs/source-tree-analysis.md diff --git a/.gitignore b/.gitignore index 87e86b9..1c59c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -124,3 +124,7 @@ _modules/* /results/ /phpunit*.xml + +.claude/ +_bmad/ +_bmad-output/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..89d70ed --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,172 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Development server +php spark serve + +# Database migrations +php spark migrate # Run pending migrations +php spark migrate:rollback # Rollback last batch +php spark db seed CmodQcSeeder # Seed initial data + +# Run tests +./vendor/bin/phpunit # All tests +./vendor/bin/phpunit tests/unit/SomeTest.php # Specific test file +./vendor/bin/phpunit --coverage-html coverage/ # With coverage report +``` + +## Architecture + +This is a CodeIgniter 4 Quality Control management system with: + +- **Backend**: PHP 8.1+, CodeIgniter 4 +- **Database**: SQL Server (uses `SQLSRV` driver) +- **Frontend**: TailwindCSS + Alpine.js + DaisyUI (CDN-based, no build step) +- **Testing**: PHPUnit 10 +- **Icons**: FontAwesome 6 + +### Key Components + +**Models** (`app/Models/`): +- `BaseModel` - Custom base model with automatic camelCase/snake_case conversion + - `findAll()`, `find()`, `first()` return camelCase keys + - `insert()`, `update()` accept camelCase, convert to snake_case for DB +- Organized in subdirectories: `Master/`, `Qc/` + +**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) +- `Qc\*` - QC domain controllers (ControlTests, Results, ResultComments) + +**Views** (`app/Views/`): +- PHP templates extending `layout/main_layout` +- Alpine.js components in `x-data` blocks +- DaisyUI components for UI + +**Helpers** (`app/Helpers/`): +- `stringcase_helper.php` - `camel_to_snake_array()`, `snake_to_camel_array()` + +### Database Schema + +Tables use soft deletes (`deleted_at`) and timestamps (`created_at`, `updated_at`): +- `dict_depts`, `dict_tests`, `dict_controls` - Master data +- `control_tests` - Control-test associations with QC parameters (mean, sd) +- `results` - Daily test results +- `result_comments` - Comments per result + +## Conventions + +### Case Convention +- **Frontend/JS/API**: camelCase +- **Backend PHP variables**: camelCase +- **Database**: snake_case +- Models handle automatic conversion; use helpers for manual conversions + +### API Response Format +```php +return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => $rows +], 200); +``` + +### Controller Pattern +```php +namespace App\Controllers\Master; + +use CodeIgniter\API\ResponseTrait; +use App\Controllers\BaseController; + +class DeptsController extends BaseController { + use ResponseTrait; + protected $model; + protected $rules; + + public function __construct() { + $this->model = new MasterDeptsModel(); + $this->rules = ['name' => 'required|min_length[1]']; + } + + public function index() { + $keyword = $this->request->getGet('keyword'); + $rows = $this->model->search($keyword); + return $this->respond([...], 200); + } + + public function create() { + $input = camel_to_snake_array($this->request->getJSON(true)); + if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + $id = $this->model->insert($input, true); + return $this->respondCreated(['status' => 'success', 'message' => $id]); + } +} +``` + +### Model Pattern +```php +namespace App\Models\Master; + +use App\Models\BaseModel; + +class MasterDeptsModel extends BaseModel { + protected $table = 'dict_depts'; + protected $primaryKey = 'dept_id'; + protected $allowedFields = ['dept_name', 'deleted_at']; + protected $useTimestamps = true; + protected $useSoftDeletes = true; + + public function search(?string $keyword) { + if ($keyword) { + $this->like('dept_name', $keyword); + } + return $this->findAll(); + } +} +``` + +### Routes Pattern +- Page routes: `$routes->get('/path', 'PageController::method');` +- API routes: `$routes->group('api', function($routes) { ... });` +- API sub-groups: `api/master`, `api/qc` + +## Frontend Patterns + +- Alpine.js `x-data` for component state (inline or in ` +endSection(); ?> +``` + +## Things to Avoid + +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 diff --git a/app/Config/Routes.php b/app/Config/Routes.php index b608135..dd39897 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -19,6 +19,7 @@ $routes->get('/report', 'PageController::report'); $routes->get('/report/view', 'PageController::reportView'); $routes->group('api', function ($routes) { + $routes->get('dashboard/recent', 'Api\DashboardApiController::getRecent'); $routes->get('dept', 'Api\DeptApiController::index'); $routes->get('dept/(:num)', 'Api\DeptApiController::show/$1'); $routes->post('dept', 'Api\DeptApiController::store'); diff --git a/app/Controllers/Api/DashboardApiController.php b/app/Controllers/Api/DashboardApiController.php new file mode 100644 index 0000000..3fc3045 --- /dev/null +++ b/app/Controllers/Api/DashboardApiController.php @@ -0,0 +1,85 @@ +resultModel = new ResultsModel(); + $this->controlTestModel = new ControlTestsModel(); + } + + public function getRecent() + { + try { + $limit = $this->request->getGet('limit') ?? 10; + + $builder = $this->resultModel->db->table('results r'); + $builder->select(' + r.result_id as id, + r.res_date, + r.res_value, + r.created_at, + c.control_name as controlName, + t.test_name as testName, + ct.mean, + ct.sd + '); + $builder->join('master_controls c', 'c.control_id = r.control_id'); + $builder->join('master_tests t', 't.test_id = r.test_id'); + $builder->join('control_tests ct', 'ct.control_id = r.control_id AND ct.test_id = r.test_id', 'left'); + $builder->where('r.deleted_at', null); + $builder->orderBy('r.created_at', 'DESC'); + $builder->limit((int) $limit); + + $results = $builder->get()->getResultArray(); + + // Calculate QC status for each result + $data = []; + foreach ($results as $row) { + $inRange = false; + $rangeDisplay = 'N/A'; + + if (!empty($row['mean']) && !empty($row['sd']) && $row['res_value'] !== null) { + $lower = $row['mean'] - (2 * $row['sd']); + $upper = $row['mean'] + (2 * $row['sd']); + $resValue = (float) $row['res_value']; + $inRange = ($resValue >= $lower && $resValue <= $upper); + $rangeDisplay = number_format($lower, 2) . ' - ' . number_format($upper, 2); + } + + $data[] = [ + 'id' => $row['id'], + 'resDate' => $row['res_date'], + 'resValue' => $row['res_value'], + 'createdAt' => $row['created_at'], + 'controlName' => $row['controlName'], + 'testName' => $row['testName'], + 'mean' => $row['mean'], + 'sd' => $row['sd'], + 'inRange' => $inRange, + 'rangeDisplay' => $rangeDisplay + ]; + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => $data + ], 200); + } catch (\Exception $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } +} diff --git a/app/Controllers/Api/ReportApiController.php b/app/Controllers/Api/ReportApiController.php new file mode 100644 index 0000000..6984401 --- /dev/null +++ b/app/Controllers/Api/ReportApiController.php @@ -0,0 +1,119 @@ +dictControlModel = new MasterControlsModel(); + $this->dictTestModel = new MasterTestsModel(); + $this->controlTestModel = new ControlTestsModel(); + $this->resultModel = new ResultsModel(); + $this->commentModel = new ResultCommentsModel(); + } + + public function getReport() + { + try { + $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; + + $controlIds = array_filter([$control1, $control2, $control3]); + + $reportData = []; + foreach ($controlIds as $controlId) { + $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); + $testInfo = $this->dictTestModel->find($test); + + $outOfRangeCount = 0; + $processedResults = []; + if ($controlTest && $controlTest['sd'] > 0) { + foreach ($results as $res) { + $zScore = ($res['resvalue'] - $controlTest['mean']) / $controlTest['sd']; + $outOfRange = abs($zScore) > 2; + if ($outOfRange) $outOfRangeCount++; + + $processedResults[] = [ + 'resdate' => $res['resdate'], + 'resvalue' => $res['resvalue'], + 'zScore' => round($zScore, 2), + 'outOfRange' => $outOfRange, + 'status' => $zScore === null ? '-' : (abs($zScore) > 2 ? 'Out' : (abs($zScore) > 1 ? 'Warn' : 'OK')) + ]; + } + } else { + foreach ($results as $res) { + $processedResults[] = [ + 'resdate' => $res['resdate'], + 'resvalue' => $res['resvalue'], + 'zScore' => null, + 'outOfRange' => false, + 'status' => '-' + ]; + } + } + + $daysInMonth = date('t', strtotime($dates . '-01')); + $values = []; + for ($day = 1; $day <= $daysInMonth; $day++) { + $value = null; + foreach ($processedResults as $res) { + if (date('j', strtotime($res['resdate'])) == $day) { + $value = $res['resvalue']; + break; + } + } + $values[] = $value; + } + + $reportData[] = [ + 'control' => $control, + 'controlTest' => $controlTest, + 'results' => $processedResults, + 'values' => $values, + 'test' => $testInfo, + 'comment' => $comment, + 'outOfRange' => $outOfRangeCount + ]; + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => [ + 'reportData' => $reportData, + 'dates' => $dates, + 'test' => $test, + 'daysInMonth' => $daysInMonth + ] + ], 200); + } catch (\Exception $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } +} diff --git a/app/Database/Migrations/2026-01-17-000001-QualityControlSystem.php b/app/Database/Migrations/2026-01-17-000001-QualityControlSystem.php deleted file mode 100644 index cd5a9bf..0000000 --- a/app/Database/Migrations/2026-01-17-000001-QualityControlSystem.php +++ /dev/null @@ -1,107 +0,0 @@ -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-17-000001_QualityControlSystem.php similarity index 94% rename from app/Database/Migrations/2026-01-18-000001-QualityControlSystem.php rename to app/Database/Migrations/2026-01-17-000001_QualityControlSystem.php index 500f7c6..a399241 100644 --- a/app/Database/Migrations/2026-01-18-000001-QualityControlSystem.php +++ b/app/Database/Migrations/2026-01-17-000001_QualityControlSystem.php @@ -8,6 +8,7 @@ class QualityControlSystem extends Migration { public function up() { + // master_depts - No dependencies $this->forge->addField([ 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], 'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true], @@ -18,6 +19,7 @@ class QualityControlSystem extends Migration $this->forge->addKey('dept_id', true); $this->forge->createTable('master_depts'); + // master_controls - FK to master_depts $this->forge->addField([ 'control_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], @@ -33,6 +35,7 @@ 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 $this->forge->addField([ 'test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], 'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], @@ -50,6 +53,7 @@ class QualityControlSystem extends Migration $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 $this->forge->addField([ 'control_test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], 'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], @@ -65,6 +69,7 @@ class QualityControlSystem extends Migration $this->forge->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE'); $this->forge->createTable('control_tests'); + // results - FK to master_controls, master_tests $this->forge->addField([ 'result_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], 'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true], @@ -81,6 +86,7 @@ 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) $this->forge->addField([ 'result_comment_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true], 'control_id' => ['type' => 'INT', 'unsigned' => true], diff --git a/app/Database/Migrations/2026-01-18-000001-RenameDictToMasterTables.php b/app/Database/Migrations/2026-01-18-000001-RenameDictToMasterTables.php deleted file mode 100644 index 3bac25e..0000000 --- a/app/Database/Migrations/2026-01-18-000001-RenameDictToMasterTables.php +++ /dev/null @@ -1,45 +0,0 @@ -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/2026-01-20-000001_RenameMasterColumns.php b/app/Database/Migrations/2026-01-20-000001_RenameMasterColumns.php new file mode 100644 index 0000000..0104d64 --- /dev/null +++ b/app/Database/Migrations/2026-01-20-000001_RenameMasterColumns.php @@ -0,0 +1,36 @@ + 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/Migrations/test.php b/app/Database/Migrations/test.php deleted file mode 100644 index 1bfd141..0000000 --- a/app/Database/Migrations/test.php +++ /dev/null @@ -1,10 +0,0 @@ -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 new file mode 100644 index 0000000..4425577 --- /dev/null +++ b/app/Database/Seeds/CmodQcSeeder.php @@ -0,0 +1,137 @@ + 'Chemistry'], + ['name' => 'Hematology'], + ['name' => 'Immunology'], + ['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) + $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'], + ]; + $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], '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], + ]; + $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')->get()->getResultArray(); + + // 5. Insert Results (50 entries - random values around mean) + $results = []; + $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']; + } + + $resultCount = 0; + foreach ($ctRows as $ct) { + // Generate 3-4 results per control-test + $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); + + $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'), + ]; + $resultCount++; + } + } + $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'], + ]; + $this->db->table('result_comments')->insertBatch($resultComments); + } +} diff --git a/app/Models/Master/MasterControlsModel.php b/app/Models/Master/MasterControlsModel.php index ba73971..521828c 100644 --- a/app/Models/Master/MasterControlsModel.php +++ b/app/Models/Master/MasterControlsModel.php @@ -8,7 +8,7 @@ class MasterControlsModel extends BaseModel { protected $primaryKey = 'control_id'; protected $allowedFields = [ 'dept_id', - 'name', + 'control_name', 'lot', 'producer', 'exp_date', @@ -22,7 +22,7 @@ class MasterControlsModel extends BaseModel { public function search($keyword = null) { if ($keyword) { return $this->groupStart() - ->like('name', $keyword) + ->like('control_name', $keyword) ->orLike('lot', $keyword) ->groupEnd() ->findAll(); diff --git a/app/Models/Master/MasterDeptsModel.php b/app/Models/Master/MasterDeptsModel.php index 5e2a300..1ff4c7f 100644 --- a/app/Models/Master/MasterDeptsModel.php +++ b/app/Models/Master/MasterDeptsModel.php @@ -7,7 +7,7 @@ class MasterDeptsModel extends BaseModel { protected $table = 'master_depts'; protected $primaryKey = 'dept_id'; protected $allowedFields = [ - 'name', + 'dept_name', 'created_at', 'updated_at', 'deleted_at' @@ -18,7 +18,7 @@ class MasterDeptsModel extends BaseModel { public function search($keyword = null) { if ($keyword) { return $this->groupStart() - ->like('name', $keyword) + ->like('dept_name', $keyword) ->groupEnd() ->findAll(); } diff --git a/app/Models/Master/MasterTestsModel.php b/app/Models/Master/MasterTestsModel.php index e692759..197f8ca 100644 --- a/app/Models/Master/MasterTestsModel.php +++ b/app/Models/Master/MasterTestsModel.php @@ -8,9 +8,9 @@ class MasterTestsModel extends BaseModel { protected $primaryKey = 'test_id'; protected $allowedFields = [ 'dept_id', - 'name', - 'unit', - 'method', + 'test_name', + 'test_unit', + 'test_method', 'cva', 'ba', 'tea', @@ -24,7 +24,7 @@ class MasterTestsModel extends BaseModel { public function search($keyword = null) { if ($keyword) { return $this->groupStart() - ->like('name', $keyword) + ->like('test_name', $keyword) ->groupEnd() ->findAll(); } diff --git a/app/Views/dashboard.php b/app/Views/dashboard.php index c30c5a9..aeb1074 100644 --- a/app/Views/dashboard.php +++ b/app/Views/dashboard.php @@ -2,66 +2,125 @@ section("content"); ?>
-
-
-

Dashboard

-

Quality Control Overview

-
+
+

Dashboard

+

Quick actions and recent QC results

-
-
-
-
-

Total Controls

-

24

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

Recent QC Results

-

Dashboard content coming soon...

+
+
+

Recent Results

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

No results yet

+
+ + +
+ + + + + + + + + + + + + + +
DateControlTestValueRange (Mean ± 2SD)Status
+
endSection(); ?> + +section("script"); ?> + +endSection(); ?> diff --git a/app/Views/layout/main_layout.php b/app/Views/layout/main_layout.php index f8961c9..2e3a36b 100644 --- a/app/Views/layout/main_layout.php +++ b/app/Views/layout/main_layout.php @@ -1,5 +1,6 @@ + @@ -10,15 +11,17 @@ +
- +
-
- +
- + - + renderSection('script') ?> - + + \ No newline at end of file diff --git a/app/Views/master/control/dialog_control_form.php b/app/Views/master/control/dialog_control_form.php index 80a58ba..4afc3af 100644 --- a/app/Views/master/control/dialog_control_form.php +++ b/app/Views/master/control/dialog_control_form.php @@ -32,7 +32,7 @@ diff --git a/app/Views/master/control/index.php b/app/Views/master/control/index.php index 20dc3aa..8b965e6 100644 --- a/app/Views/master/control/index.php +++ b/app/Views/master/control/index.php @@ -68,7 +68,7 @@ - + @@ -116,18 +116,22 @@ form: { controlId: null, controlName: "", - lotNumber: "", + lot: "", producer: "", expDate: "", }, + init() { + this.fetchList(); + }, + async fetchList() { this.loading = true; this.error = null; this.list = null; try { const params = new URLSearchParams({ keyword: this.keyword }); - const response = await fetch(`${window.BASEURL}api/master/controls?${params}`, { + const response = await fetch(`${BASEURL}api/master/controls?${params}`, { method: "GET", headers: { "Content-Type": "application/json" } }); @@ -144,7 +148,7 @@ async loadData(id) { this.loading = true; try { - const response = await fetch(`${window.BASEURL}api/master/controls/${id}`, { + const response = await fetch(`${BASEURL}api/master/controls/${id}`, { method: "GET", headers: { "Content-Type": "application/json" } }); @@ -165,13 +169,13 @@ if (id) { await this.loadData(id); } else { - this.form = { controlId: null, controlName: "", lotNumber: "", producer: "", expDate: "" }; + this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "" }; } }, closeModal() { this.showModal = false; - this.form = { controlId: null, controlName: "", lotNumber: "", producer: "", expDate: "" }; + this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "" }; }, validate() { @@ -187,10 +191,10 @@ let url = ''; if (this.form.controlId) { method = 'PATCH'; - url = `${window.BASEURL}api/master/controls/${this.form.controlId}`; + url = `${BASEURL}api/master/controls/${this.form.controlId}`; } else { method = 'POST'; - url = `${window.BASEURL}api/master/controls`; + url = `${BASEURL}api/master/controls`; } try { const res = await fetch(url, { @@ -218,7 +222,7 @@ if (!confirm("Are you sure you want to delete this item?")) return; this.loading = true; try { - const res = await fetch(`${window.BASEURL}api/master/controls/${id}`, { + const res = await fetch(`${BASEURL}api/master/controls/${id}`, { method: "DELETE", headers: { "Content-Type": "application/json" } }); diff --git a/app/Views/master/dept/index.php b/app/Views/master/dept/index.php index d5e8fa1..325b831 100644 --- a/app/Views/master/dept/index.php +++ b/app/Views/master/dept/index.php @@ -9,8 +9,7 @@ @@ -19,18 +18,13 @@
- + x-model="keyword" @keyup.enter="fetchList()" />
@@ -43,14 +37,14 @@

Loading...

- + - +