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/`).
This commit is contained in:
mahdahar 2026-01-20 14:44:46 +07:00
parent 5cae572916
commit 14baa6b758
25 changed files with 1889 additions and 286 deletions

4
.gitignore vendored
View File

@ -124,3 +124,7 @@ _modules/*
/results/
/phpunit*.xml
.claude/
_bmad/
_bmad-output/

172
CLAUDE.md Normal file
View File

@ -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 `<script>` blocks)
- Fetch API for AJAX (no jQuery)
- DaisyUI components for UI
- Modals with `x-show` and `x-transition`
- `window.BASEURL` available globally for API calls
- Views access page data via `$pageData['title']`, `$pageData['userInitials']`, `$pageData['userName']`, `$pageData['userRole']`
### View Template Pattern
```php
<?= $this->extend("layout/main_layout"); ?>
<?= $this->section("content"); ?>
<main x-data="componentName()">
<!-- UI content -->
</main>
<?= $this->endSection(); ?>
<?= $this->section("script"); ?>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data("componentName", () => ({
// state and methods
}));
});
</script>
<?= $this->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

View File

@ -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');

View File

@ -0,0 +1,85 @@
<?php
namespace App\Controllers\Api;
use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;
use App\Models\Qc\ResultsModel;
use App\Models\Qc\ControlTestsModel;
class DashboardApiController extends BaseController
{
use ResponseTrait;
protected $resultModel;
protected $controlTestModel;
public function __construct()
{
$this->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());
}
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace App\Controllers\Api;
use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;
use App\Models\Master\MasterControlsModel;
use App\Models\Master\MasterTestsModel;
use App\Models\Qc\ControlTestsModel;
use App\Models\Qc\ResultsModel;
use App\Models\Qc\ResultCommentsModel;
class ReportApiController extends BaseController
{
use ResponseTrait;
protected $dictControlModel;
protected $dictTestModel;
protected $controlTestModel;
protected $resultModel;
protected $commentModel;
public function __construct()
{
$this->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());
}
}
}

View File

@ -1,107 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class QualityControlSystem extends Migration {
public function up() {
$this->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);
}
}

View File

@ -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],

View File

@ -1,45 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class RenameDictToMasterTables extends Migration {
public function up() {
$db = \Config\Database::connect();
$db->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');
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class RenameMasterColumns extends Migration
{
public function up()
{
// master_depts: name -> 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");
}
}

View File

@ -1,10 +0,0 @@
<?php
$db = \Config\Database::connect();
echo "Current migrations table:\n";
$results = $db->query("SELECT * FROM migrations")->getResult();
print_r($results);
echo "\nChecking for dict_ tables:\n";
$tables = $db->listTables();
print_r($tables);

View File

@ -0,0 +1,137 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class CmodQcSeeder extends Seeder
{
public function run()
{
// 1. Insert Departments (4 entries)
$depts = [
['name' => '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);
}
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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();
}

View File

@ -2,66 +2,125 @@
<?= $this->section("content"); ?>
<main class="flex-1 p-6 overflow-auto">
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
<div>
<h1 class="text-2xl font-bold tracking-tight text-base-content">Dashboard</h1>
<p class="text-sm mt-1 opacity-70">Quality Control Overview</p>
</div>
<div class="mb-6">
<h1 class="text-2xl font-bold tracking-tight text-base-content">Dashboard</h1>
<p class="text-sm mt-1 opacity-70">Quick actions and recent QC results</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/60">Total Controls</p>
<p class="text-2xl font-bold text-base-content mt-1">24</p>
</div>
<div class="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center">
<i class="fa-solid fa-vial text-primary text-xl"></i>
</div>
<!-- Quick Action Cards -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<a href="<?= base_url('entry') ?>" class="card bg-primary text-primary-content hover:bg-primary/90 cursor-pointer no-underline hover:scale-[1.02] transition">
<div class="card-body items-center text-center py-4">
<i class="fa-solid fa-pen-to-square text-2xl mb-2"></i>
<span class="font-medium">QC Entry</span>
</div>
</div>
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/60">Tests Today</p>
<p class="text-2xl font-bold text-base-content mt-1">156</p>
</div>
<div class="w-12 h-12 rounded-lg bg-success/10 flex items-center justify-center">
<i class="fa-solid fa-check text-success text-xl"></i>
</div>
</a>
<a href="<?= base_url('master/dept') ?>" class="card bg-base-100 border border-base-300 hover:border-primary cursor-pointer no-underline hover:scale-[1.02] transition">
<div class="card-body items-center text-center py-4">
<i class="fa-solid fa-building text-2xl mb-2"></i>
<span class="font-medium">Departments</span>
</div>
</div>
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/60">Pass Rate</p>
<p class="text-2xl font-bold text-base-content mt-1">98.5%</p>
</div>
<div class="w-12 h-12 rounded-lg bg-warning/10 flex items-center justify-center">
<i class="fa-solid fa-chart-line text-warning text-xl"></i>
</div>
</a>
<a href="<?= base_url('master/test') ?>" class="card bg-base-100 border border-base-300 hover:border-primary cursor-pointer no-underline hover:scale-[1.02] transition">
<div class="card-body items-center text-center py-4">
<i class="fa-solid fa-flask-vial text-2xl mb-2"></i>
<span class="font-medium">Tests</span>
</div>
</div>
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-base-content/60">Alerts</p>
<p class="text-2xl font-bold text-base-content mt-1">3</p>
</div>
<div class="w-12 h-12 rounded-lg bg-error/10 flex items-center justify-center">
<i class="fa-solid fa-triangle-exclamation text-error text-xl"></i>
</div>
</a>
<a href="<?= base_url('master/control') ?>" class="card bg-base-100 border border-base-300 hover:border-primary cursor-pointer no-underline hover:scale-[1.02] transition">
<div class="card-body items-center text-center py-4">
<i class="fa-solid fa-vial text-2xl mb-2"></i>
<span class="font-medium">Controls</span>
</div>
</div>
</a>
<a href="<?= base_url('report') ?>" class="card bg-base-100 border border-base-300 hover:border-primary cursor-pointer no-underline hover:scale-[1.02] transition">
<div class="card-body items-center text-center py-4">
<i class="fa-solid fa-chart-bar text-2xl mb-2"></i>
<span class="font-medium">Reports</span>
</div>
</a>
</div>
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
<h2 class="text-lg font-semibold text-base-content mb-4">Recent QC Results</h2>
<p class="text-base-content/60 text-center py-8">Dashboard content coming soon...</p>
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6"
x-data="dashboardRecentResults()">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-base-content">Recent Results</h2>
<button @click="fetchResults()" class="btn btn-ghost btn-sm">
<i class="fa-solid fa-rotate-right" :class="{ 'fa-spin': loading }"></i>
</button>
</div>
<!-- Loading State -->
<div x-show="loading" class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
<!-- Empty State -->
<div x-show="!loading && results.length === 0" class="text-center py-12">
<i class="fa-solid fa-flask text-4xl text-base-content/20 mb-3"></i>
<p class="text-base-content/60">No results yet</p>
</div>
<!-- Table -->
<div x-show="!loading && results.length > 0" class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>Date</th>
<th>Control</th>
<th>Test</th>
<th class="text-right">Value</th>
<th>Range (Mean ± 2SD)</th>
<th class="text-center">Status</th>
</tr>
</thead>
<tbody>
<template x-for="row in results" :key="row.id">
<tr class="hover">
<td x-text="row.resDate || '-'"></td>
<td x-text="row.controlName || '-'"></td>
<td x-text="row.testName || '-'"></td>
<td class="text-right font-mono" x-text="row.resValue ?? '-'"></td>
<td class="font-mono text-sm" x-text="row.rangeDisplay"></td>
<td class="text-center">
<span class="badge"
:class="row.inRange ? 'badge-success' : 'badge-error'"
x-text="row.inRange ? 'Pass' : 'Fail'">
</span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</main>
<?= $this->endSection(); ?>
<?= $this->section("script"); ?>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data("dashboardRecentResults", () => ({
results: [],
loading: true,
init() {
this.fetchResults();
},
async fetchResults() {
this.loading = true;
try {
const response = await fetch(`${BASEURL}api/dashboard/recent?limit=10`);
const json = await response.json();
this.results = json.data || [];
} catch (e) {
console.error(e);
} finally {
this.loading = false;
}
}
}));
});
</script>
<?= $this->endSection(); ?>

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en" data-theme="autumn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -10,15 +11,17 @@
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script>
const BASEURL = '<?= base_url('/') ?>';
const BASEURL = '<?= base_url(''); ?>';
</script>
</head>
<body class="bg-base-200 text-base-content" x-data="appData()">
<div class="drawer lg:drawer-open">
<input id="sidebar-drawer" type="checkbox" class="drawer-toggle" x-ref="sidebarDrawer">
<div class="drawer-content flex flex-col min-h-screen">
<nav class="navbar bg-base-200/80 backdrop-blur-md border-b border-base-300 sticky top-0 z-30 w-full shadow-sm">
<nav
class="navbar bg-base-200/80 backdrop-blur-md border-b border-base-300 sticky top-0 z-30 w-full shadow-sm">
<div class="flex-none lg:hidden">
<label for="sidebar-drawer" class="btn btn-square btn-ghost text-primary">
<i class="fa-solid fa-bars text-xl"></i>
@ -26,7 +29,8 @@
</div>
<div class="flex-1 px-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-primary flex items-center justify-center shadow-lg shadow-primary/20">
<div
class="w-10 h-10 rounded-lg bg-primary flex items-center justify-center shadow-lg shadow-primary/20">
<i class="fa-solid fa-flask text-white text-lg"></i>
</div>
<div>
@ -41,22 +45,32 @@
<i class="fa-solid fa-moon text-neutral-content" x-show="currentTheme === themeConfig.dark"></i>
</button>
<div class="dropdown dropdown-end" x-data="{ dropdownOpen: false }">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder" @click="dropdownOpen = !dropdownOpen">
<div class="bg-primary text-primary-content rounded-full w-10 h-10 flex items-center justify-center">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder"
@click="dropdownOpen = !dropdownOpen">
<div
class="bg-primary text-primary-content rounded-full w-10 h-10 flex items-center justify-center">
<span><?= $pageData['userInitials'] ?? 'DR' ?></span>
</div>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300" x-show="dropdownOpen" @click.outside="dropdownOpen = false" x-transition>
<ul tabindex="0"
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow-xl bg-base-100 rounded-box w-52 border border-base-300"
x-show="dropdownOpen" @click.outside="dropdownOpen = false" x-transition>
<li class="menu-title px-4 py-2">
<span class="text-base-content font-bold"><?= $pageData['userName'] ?? 'Lab User' ?></span>
<span class="text-xs text-primary font-medium"><?= $pageData['userRole'] ?? 'Administrator' ?></span>
<span
class="text-base-content font-bold"><?= $pageData['userName'] ?? 'Lab User' ?></span>
<span
class="text-xs text-primary font-medium"><?= $pageData['userRole'] ?? 'Administrator' ?></span>
</li>
<div class="divider my-0 h-px opacity-10"></div>
<li><a class="hover:bg-base-200"><i class="fa-solid fa-user text-primary"></i> Profile</a></li>
<li><a class="hover:bg-base-200"><i class="fa-solid fa-gear text-primary"></i> Settings</a></li>
<li><a class="hover:bg-base-200"><i class="fa-solid fa-question-circle text-primary"></i> Help</a></li>
<li><a class="hover:bg-base-200"><i class="fa-solid fa-user text-primary"></i> Profile</a>
</li>
<li><a class="hover:bg-base-200"><i class="fa-solid fa-gear text-primary"></i> Settings</a>
</li>
<li><a class="hover:bg-base-200"><i class="fa-solid fa-question-circle text-primary"></i>
Help</a></li>
<div class="divider my-0 h-px opacity-10"></div>
<li><a href="<?= base_url('/logout') ?>" class="text-error hover:bg-error/10"><i class="fa-solid fa-sign-out-alt"></i> Logout</a></li>
<li><a href="<?= base_url('/logout') ?>" class="text-error hover:bg-error/10"><i
class="fa-solid fa-sign-out-alt"></i> Logout</a></li>
</ul>
</div>
</div>
@ -70,7 +84,8 @@
<aside class="bg-base-300 border-r border-base-300 w-64 min-h-full flex flex-col">
<ul class="menu p-4 text-base-content flex-1 w-full">
<li class="mb-1 min-h-0">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= uri_string() === '' ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/') ?>">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= uri_string() === '' ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
href="<?= base_url('/') ?>">
<i class="fa-solid fa-chart-line w-5"></i>
Dashboard
</a>
@ -80,19 +95,22 @@
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">Master Data</p>
</li>
<li class="mb-1 min-h-0">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/dept') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/master/dept') ?>">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/dept') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
href="<?= base_url('/master/dept') ?>">
<i class="fa-solid fa-building w-5"></i>
Departments
</a>
</li>
<li class="mb-1 min-h-0">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/test') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/master/test') ?>">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/test') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
href="<?= base_url('/master/test') ?>">
<i class="fa-solid fa-flask-vial w-5"></i>
Tests
</a>
</li>
<li class="mb-1 min-h-0">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/control') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/master/control') ?>">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'master/control') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
href="<?= base_url('/master/control') ?>">
<i class="fa-solid fa-vial w-5"></i>
Controls
</a>
@ -102,13 +120,15 @@
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">QC Operations</p>
</li>
<li class="mb-1 min-h-0">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'entry') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/entry') ?>">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'entry') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
href="<?= base_url('/entry') ?>">
<i class="fa-solid fa-pen-to-square w-5"></i>
QC Entry
</a>
</li>
<li class="mb-1 min-h-0">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'report') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full" href="<?= base_url('/report') ?>">
<a class="flex items-center gap-3 px-4 py-3 rounded-lg <?= str_contains(uri_string(), 'report') ? 'bg-primary/10 text-primary font-medium' : 'opacity-70 hover:bg-base-200 hover:opacity-100' ?> transition-colors h-full"
href="<?= base_url('/report') ?>">
<i class="fa-solid fa-chart-bar w-5"></i>
Reports
</a>
@ -168,4 +188,5 @@
<?= $this->renderSection('script') ?>
</body>
</html>

View File

@ -32,7 +32,7 @@
<input
type="text"
class="input input-bordered w-full focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary bg-base-200 border-base-300 text-base-content placeholder:opacity-50"
x-model="form.lotNumber"
x-model="form.lot"
placeholder="e.g., LOT12345"
/>
</div>

View File

@ -68,7 +68,7 @@
<tr class="hover:bg-base-200 transition-colors">
<td class="py-3 px-5 font-medium text-base-content" x-text="item.controlName"></td>
<td class="py-3 px-5">
<span class="font-mono text-xs bg-base-200 text-base-content/70 px-2 py-1 rounded" x-text="item.lotNumber"></span>
<span class="font-mono text-xs bg-base-200 text-base-content/70 px-2 py-1 rounded" x-text="item.lot"></span>
</td>
<td class="py-3 px-5" x-text="item.producer"></td>
<td class="py-3 px-5" x-text="item.expDate"></td>
@ -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" }
});

View File

@ -9,8 +9,7 @@
</div>
<button
class="btn btn-sm gap-2 shadow-sm border-0 bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 text-white transition-all duration-200"
@click="showForm()"
>
@click="showForm()">
<i class="fa-solid fa-plus"></i> New Department
</button>
</div>
@ -19,18 +18,13 @@
<div class="flex items-center gap-3">
<div class="relative flex-1 max-w-md">
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-sm"></i>
<input
type="text"
placeholder="Search by name..."
<input type="text" placeholder="Search by name..."
class="w-full pl-10 pr-4 py-2.5 text-sm bg-base-200 border border-base-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all"
x-model="keyword"
@keyup.enter="fetchList()"
/>
x-model="keyword" @keyup.enter="fetchList()" />
</div>
<button
class="px-4 py-2.5 text-sm font-medium bg-base-content text-base-100 rounded-lg hover:bg-base-content/90 transition-all duration-200 flex items-center gap-2"
@click="fetchList()"
>
@click="fetchList()">
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
</button>
</div>
@ -67,14 +61,12 @@
<td class="py-3 px-5 text-right">
<button
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors"
@click="showForm(item.deptId)"
>
@click="showForm(item.deptId)">
<i class="fa-solid fa-pencil"></i> Edit
</button>
<button
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-error bg-error/10 hover:bg-error/20 rounded-lg transition-colors ml-1"
@click="deleteData(item.deptId)"
>
@click="deleteData(item.deptId)">
<i class="fa-solid fa-trash"></i>
</button>
</td>
@ -116,7 +108,7 @@
this.list = null;
try {
const params = new URLSearchParams({ keyword: this.keyword });
const response = await fetch(`${window.BASEURL}api/master/depts?${params}`, {
const response = await fetch(`${BASEURL}api/master/depts?${params}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
});
@ -133,7 +125,7 @@
async loadData(id) {
this.loading = true;
try {
const response = await fetch(`${window.BASEURL}api/master/depts/${id}`, {
const response = await fetch(`${BASEURL}api/master/depts/${id}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
});
@ -176,10 +168,10 @@
let url = '';
if (this.form.deptId) {
method = 'PATCH';
url = `${window.BASEURL}api/master/depts/${this.form.deptId}`;
url = `${BASEURL}api/master/depts/${this.form.deptId}`;
} else {
method = 'POST';
url = `${window.BASEURL}api/master/depts`;
url = `${BASEURL}api/master/depts`;
}
try {
const res = await fetch(url, {
@ -207,7 +199,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/depts/${id}`, {
const res = await fetch(`${BASEURL}api/master/depts/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" }
});

View File

@ -119,13 +119,17 @@
tea: "",
},
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/tests?${params}`, {
const response = await fetch(`${BASEURL}api/master/tests?${params}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
});
@ -142,7 +146,7 @@
async loadData(id) {
this.loading = true;
try {
const response = await fetch(`${window.BASEURL}api/master/tests/${id}`, {
const response = await fetch(`${BASEURL}api/master/tests/${id}`, {
method: "GET",
headers: { "Content-Type": "application/json" }
});
@ -185,10 +189,10 @@
let url = '';
if (this.form.testId) {
method = 'PATCH';
url = `${window.BASEURL}api/master/tests/${this.form.testId}`;
url = `${BASEURL}api/master/tests/${this.form.testId}`;
} else {
method = 'POST';
url = `${window.BASEURL}api/master/tests`;
url = `${BASEURL}api/master/tests`;
}
try {
const res = await fetch(url, {
@ -216,7 +220,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/tests/${id}`, {
const res = await fetch(`${BASEURL}api/master/tests/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" }
});

282
docs/architecture.md Normal file
View File

@ -0,0 +1,282 @@
# Architecture Documentation - TinyQC
## Overview
TinyQC follows the **Model-View-Controller (MVC)** architectural pattern as defined by the CodeIgniter 4 framework. This document provides a detailed analysis of the application's architecture, components, and design patterns.
---
## Architecture Pattern
### MVC (Model-View-Controller)
```
┌─────────────────────────────────────────────────────────────────┐
│ HTTP Request │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Front Controller │
│ (public/index.php) │
└────────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Router │
│ (app/Config/Routes.php) │
└────────────────────────────┬────────────────────────────────────┘
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Controller │ │ Filter │ │ Middleware │
│ (Handles │ │ (Auth, │ │ (Pre/post │
│ request) │ │ CSRF) │ │ processing)│
└───────┬───────┘ └───────────────┘ └───────────────┘
│ │
▼ │
┌───────────────┐ │
│ Model │◄────────────────────────────────────┘
│ (Data & │ Business Logic
│ Business │
│ Logic) │
└───────┬───────┘
┌───────────────┐
│ View │
│ (Template) │
└───────┬───────┘
┌─────────────────────────────────────────────────────────────────┐
│ HTTP Response │
│ (HTML, JSON, Redirect) │
└─────────────────────────────────────────────────────────────────┘
```
---
## Component Details
### 1. Models Layer
The Models layer handles all data operations using CodeIgniter's Model class.
#### Base Model (`BaseModel.php`)
- Provides automatic camelCase/snake_case conversion
- Extends `CodeIgniter\Model` with custom functionality
- Standardized data handling across all models
#### Dictionary Models
| Model | Table | Purpose |
|-------|-------|---------|
| `DictDeptModel.php` | dict_dept | Department master data |
| `DictTestModel.php` | dict_test | Test/parameter master data |
| `DictControlModel.php` | dict_control | Control master data |
#### Entity Models
| Model | Table | Purpose |
|-------|-------|---------|
| `DeptModel.php` | dept | Department CRUD |
| `TestModel.php` | test | Test CRUD |
| `ControlModel.php` | control | Control CRUD |
| `ControlTestModel.php` | control_test | Control-test relationships |
#### Result Models
| Model | Table | Purpose |
|-------|-------|---------|
| `ResultModel.php` | results | Result data |
| `DailyResultModel.php` | daily_results | Daily QC entries |
| `MonthlyCommentModel.php` | monthly_comments | Monthly comments |
| `ResultCommentModel.php` | result_comments | Result annotations |
---
### 2. Controllers Layer
Controllers handle HTTP requests, interact with models, and return responses.
#### Page Controllers
| Controller | Base Class | Responsibilities |
|------------|------------|------------------|
| `BaseController.php` | `\CodeIgniter\BaseController` | Common controller setup |
| `Dashboard.php` | BaseController | Dashboard rendering |
| `Dept.php` | BaseController | Department management UI |
| `Test.php` | BaseController | Test management UI |
| `Control.php` | BaseController | Control management UI |
| `Entry.php` | BaseController | Entry forms (daily/monthly) |
| `Report.php` | BaseController | Report generation UI |
| `PageController.php` | BaseController | Generic page handling |
#### API Controllers
All API controllers extend base functionality and return JSON responses.
| Controller | Endpoints | Format |
|------------|-----------|--------|
| `DeptApiController.php` | GET/POST/PUT/DELETE /api/dept | JSON |
| `TestApiController.php` | GET/POST/PUT/DELETE /api/test | JSON |
| `ControlApiController.php` | GET/POST/PUT/DELETE /api/control | JSON |
| `EntryApiController.php` | POST /api/entry/* | JSON |
---
### 3. Views Layer
Views use PHP templates with TailwindCSS and DaisyUI for styling.
#### View Structure
```
app/Views/
├── layout/
│ └── form_layout.php # Main layout template
├── dashboard.php # Dashboard view
├── dept/
│ ├── index.php # Department list
│ └── dialog_form.php # Department form dialog
├── test/
│ ├── index.php # Test list
│ └── dialog_form.php # Test form dialog
├── control/
│ ├── index.php # Control list
│ └── dialog_form.php # Control form dialog
├── entry/
│ ├── daily.php # Daily entry form
│ └── monthly.php # Monthly entry form
├── report/
│ ├── index.php # Report selection
│ └── view.php # Report display
└── errors/ # Error page templates
```
#### Frontend Stack
- **TailwindCSS:** Utility-first CSS framework
- **Alpine.js:** Lightweight JavaScript framework
- **DaisyUI:** TailwindCSS component library
- **FontAwesome 7:** Icon library
---
## Data Architecture
### Database (SQL Server)
The application uses SQL Server as its primary database with the following schema pattern:
#### Core Tables
- `dict_dept` - Department dictionary
- `dict_test` - Test/parameter dictionary
- `dict_control` - Control dictionary
- `dept` - Department data
- `test` - Test data
- `control` - Control data
- `control_test` - Control-test relationships
#### Result Tables
- `results` - Result storage
- `daily_results` - Daily QC data
- `monthly_comments` - Monthly comments
- `result_comments` - Result annotations
#### Naming Convention
- Dictionary tables: `dict_*`
- Data tables: Singular lowercase
- Junction tables: `*_test` (noun-noun)
---
## API Design
### RESTful Endpoints
#### Department API (`/api/dept`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/dept | List all departments |
| GET | /api/dept/:id | Get department by ID |
| POST | /api/dept | Create department |
| PUT | /api/dept/:id | Update department |
| DELETE | /api/dept/:id | Delete department |
#### Test API (`/api/test`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/test | List all tests |
| GET | /api/test/:id | Get test by ID |
| POST | /api/test | Create test |
| PUT | /api/test/:id | Update test |
| DELETE | /api/test/:id | Delete test |
#### Control API (`/api/control`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/control | List all controls |
| GET | /api/control/:id | Get control by ID |
| POST | /api/control | Create control |
| PUT | /api/control/:id | Update control |
| DELETE | /api/control/:id | Delete control |
#### Entry API (`/api/entry`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/entry/controls | Get controls for entry |
| GET | /api/entry/tests | Get tests for entry |
| POST | /api/entry/daily | Save daily result |
| POST | /api/entry/monthly | Save monthly entry |
| POST | /api/entry/comment | Save comment |
---
## Configuration Management
### Environment Configuration
- File: `env` (copy to `.env`)
- Database settings in `app/Config/Database.php`
- Route definitions in `app/Config/Routes.php`
### Database Configuration
```php
database.default.hostname = localhost
database.default.port = 1433
database.default.database = tinyqc
database.default.username = sa
database.default.password = your_password
database.default.DBDriver = SQLSRV
```
---
## Development Workflow
### Code Organization
1. Models in `app/Models/` - Data access
2. Controllers in `app/Controllers/` - Request handling
3. Views in `app/Views/` - UI templates
4. Routes in `app/Config/Routes.php` - URL mapping
### Adding New Features
1. Create model in `app/Models/`
2. Create API controller in `app/Controllers/Api/`
3. Add routes in `app/Config/Routes.php`
4. Create views in `app/Views/[module]/`
5. Add menu item in layout if needed
---
## Security Considerations
- **Input Validation:** CodeIgniter 4 Validation library
- **CSRF Protection:** Built-in CodeIgniter CSRF filter
- **SQL Injection:** Parameterized queries via Query Builder
- **XSS Protection:** Output escaping in Views
- **Session Management:** CodeIgniter Session library
---
## Performance Considerations
- **Caching:** CodeIgniter 4 cache system available
- **Database:** SQL Server with optimized queries
- **Assets:** Static files served from `public/`
- **Debugbar:** Debug toolbar in development mode

435
docs/development-guide.md Normal file
View File

@ -0,0 +1,435 @@
# Development Guide - TinyQC
## Prerequisites
| Requirement | Version | Description |
|-------------|---------|-------------|
| PHP | 8.1+ | Server-side scripting |
| SQL Server | 2016+ | Database server |
| Composer | Latest | PHP dependency manager |
| Web Server | Any | Apache/Nginx/IIS |
---
## Installation
### 1. Clone the Repository
```bash
git clone <repository-url> tinyqc
cd tinyqc
```
### 2. Install Dependencies
```bash
composer install
```
### 3. Configure Environment
```bash
copy env .env
```
Edit `.env` with your database settings:
```env
database.default.hostname = localhost
database.default.port = 1433
database.default.database = tinyqc
database.default.username = sa
database.default.password = your_password
database.default.DBDriver = SQLSRV
```
### 4. Set Up Database
1. Create a new SQL Server database
2. Run migrations if applicable
3. Seed initial data if needed
### 5. Configure Web Server
Point your web server to the `public` directory:
**Apache (httpd.conf or virtual host):**
```apache
<VirtualHost *:80>
ServerName tinyqc.local
DocumentRoot "D:/data/www/tinyqc/public"
<Directory "D:/data/www/tinyqc">
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```
**Nginx:**
```nginx
server {
listen 80;
server_name tinyqc.local;
root D:/data/www/tinyqc/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
```
### 6. Access the Application
Open http://localhost in your browser (or your configured domain).
---
## Project Structure
```
tinyqc/
├── app/
│ ├── Config/ # Configuration files
│ │ ├── Database.php # Database settings
│ │ └── Routes.php # Route definitions
│ ├── Controllers/ # Application controllers
│ │ ├── Api/ # API controllers
│ │ ├── Dashboard.php
│ │ ├── Dept.php
│ │ ├── Test.php
│ │ ├── Control.php
│ │ ├── Entry.php
│ │ ├── PageController.php
│ │ └── Report.php
│ ├── Models/ # Database models
│ │ ├── BaseModel.php
│ │ ├── DeptModel.php
│ │ ├── TestModel.php
│ │ ├── ControlModel.php
│ │ ├── DictDeptModel.php
│ │ ├── DictTestModel.php
│ │ ├── DictControlModel.php
│ │ ├── ControlTestModel.php
│ │ ├── ResultModel.php
│ │ ├── DailyResultModel.php
│ │ ├── MonthlyCommentModel.php
│ │ └── ResultCommentModel.php
│ └── Views/ # View templates
│ ├── layout/ # Layout templates
│ ├── dashboard.php
│ ├── dept/ # Department views
│ ├── test/ # Test views
│ ├── control/ # Control views
│ ├── entry/ # Entry views
│ └── report/ # Report views
├── public/ # Web root
│ ├── index.php # Entry point
│ ├── js/
│ │ ├── app.js
│ │ ├── tables.js
│ │ └── charts.js
│ └── .htaccess
├── tests/ # Unit tests
├── writable/ # Writable directory
├── env # Environment template
├── composer.json
└── phpunit.xml.dist
```
---
## Development Commands
### Running the Application
**Using PHP built-in server (development):**
```bash
php spark serve
```
**Or using built-in server:**
```bash
php -S localhost:8080 -t public
```
### Running Tests
```bash
# Run all tests
./vendor/bin/phpunit
# Run with coverage report
./vendor/bin/phpunit --coverage-html coverage/
```
### Code Style
Follow these coding standards:
- **PSR-12:** PHP coding standard
- **CamelCase:** For variables and functions
- **PascalCase:** For classes
- **snake_case:** For database tables and columns
---
## Adding New Features
### Step 1: Create the Model
Location: `app/Models/`
```php
<?php
namespace App\Models;
use App\Models\BaseModel;
class NewFeatureModel extends BaseModel
{
protected $table = 'new_feature';
protected $primaryKey = 'id';
protected $allowedFields = ['field1', 'field2', 'created_at', 'updated_at'];
}
```
### Step 2: Create the API Controller
Location: `app/Controllers/Api/`
```php
<?php
namespace App\Controllers\Api;
use App\Controllers\BaseController;
use App\Models\NewFeatureModel;
class NewFeatureApiController extends BaseController
{
protected $model;
public function __construct()
{
$this->model = new NewFeatureModel();
}
public function index()
{
$data = $this->model->findAll();
return $this->response->setJSON($data);
}
// Add CRUD methods...
}
```
### Step 3: Add Routes
Location: `app/Config/Routes.php`
```php
$routes->group('api', ['filter' => 'cors'], function ($routes) {
$routes->get('new-feature', 'NewFeatureApiController::index');
$routes->post('new-feature', 'NewFeatureApiController::create');
// Add more routes...
});
```
### Step 4: Create Views
Location: `app/Views/newfeature/`
```php
<?= $this->extend('layout/form_layout') ?>
<?= $this->section('content') ?>
<!-- View content -->
<?= $this->endSection() ?>
```
### Step 5: Add Menu Item
Update the layout file to include the new feature in navigation.
---
## Frontend Development
### JavaScript Files
| File | Purpose |
|------|---------|
| `public/js/app.js` | Main application JavaScript |
| `public/js/tables.js` | Table functionality |
| `public/js/charts.js` | Chart functionality |
### UI Components
The application uses:
- **TailwindCSS:** Utility-first CSS
- **Alpine.js:** Reactive JavaScript
- **DaisyUI:** Component library
- **FontAwesome:** Icons
### Modal Dialogs
Views use modal-based dialogs for form interactions:
- `dialog_form.php` pattern for create/edit forms
- AJAX submission via API controllers
---
## Database Operations
### Using the Model
```php
// Get all records
$model = new DeptModel();
$depts = $model->findAll();
// Find by ID
$dept = $model->find($id);
// Create
$model->insert(['name' => 'New Dept']);
// Update
$model->update($id, ['name' => 'Updated Name']);
// Delete
$model->delete($id);
```
### BaseModel Features
The `BaseModel` provides:
- Automatic camelCase/snake_case conversion
- Standardized CRUD operations
- Soft delete support (if enabled)
---
## Debugging
### Debug Bar
The application includes a debug toolbar for development:
- Access via `writable/debugbar/` directory
- Review queries, logs, and timing
### Logging
```php
log_message('error', 'Something went wrong');
log_message('debug', 'Debug information');
```
Logs are stored in `writable/logs/`.
---
## Common Tasks
### Database Migrations
```bash
# Create migration
php spark make:migration create_users_table
# Run migrations
php spark migrate
# Rollback migrations
php spark migrate:rollback
```
### Database Seeding
```bash
# Create seeder
php spark make:seeder UserSeeder
# Run seeder
php spark db:seed UserSeeder
```
### Clear Cache
```bash
php spark cache:clear
```
---
## Testing
### Writing Tests
Location: `tests/`
```php
<?php
use Tests\Support\TestCase;
class DeptModelTest extends TestCase
{
public function testCanCreateDept()
{
$model = new \App\Models\DeptModel();
$result = $model->insert(['name' => 'Test Dept']);
$this->assertTrue($result > 0);
}
}
```
### Running Tests
```bash
# Run all tests
./vendor/bin/phpunit
# Run specific test file
./vendor/bin/phpunit tests/Models/DeptModelTest.php
# Run with coverage
./vendor/bin/phpunit --coverage-html coverage/
```
---
## Deployment
### Production Checklist
1. Set `CI_ENVIRONMENT` to `production` in `.env`
2. Disable debugging in `app/Config/Boot/production.php`
3. Set proper file permissions on `writable/` directory
4. Configure web server for production
5. Set up HTTPS/SSL
6. Configure error logging
7. Test backup and restore procedures
### Directory Permissions
```bash
# writable directory must be writable
chmod 755 writable/
chmod 644 app/Config/*.php
```
---
## Additional Resources
- [CodeIgniter 4 Documentation](https://codeigniter.com/docs)
- [TailwindCSS Documentation](https://tailwindcss.com/docs)
- [Alpine.js Documentation](https://alpinejs.dev/start)
- [DaisyUI Documentation](https://daisyui.com/docs/)
- [PHPUnit Documentation](https://phpunit.de/documentation.html)

128
docs/index.md Normal file
View File

@ -0,0 +1,128 @@
# Project Documentation Index - TinyQC
## Project Overview
- **Type:** Monolith (single cohesive codebase)
- **Primary Language:** PHP 8.1+
- **Architecture:** MVC (Model-View-Controller)
- **Framework:** CodeIgniter 4
- **Database:** SQL Server 2016+
### Quick Reference
- **Tech Stack:** PHP 8.1, CodeIgniter 4, SQL Server, TailwindCSS, Alpine.js, DaisyUI
- **Entry Point:** `public/index.php`
- **Architecture Pattern:** Model-View-Controller (MVC)
---
## Generated Documentation
### Core Documentation
- [Project Overview](./project-overview.md) - Executive summary and technology stack
- [Architecture](./architecture.md) - Detailed architecture documentation
- [Source Tree Analysis](./source-tree-analysis.md) - Annotated directory structure
- [Development Guide](./development-guide.md) - Setup, development commands, and best practices
### Supplementary Documentation
- [README.md](../README.md) - Original project readme
---
## Documentation Guide
### For New Developers
1. Start with [Project Overview](./project-overview.md) to understand the system
2. Review [Architecture](./architecture.md) for component details
3. Read [Development Guide](./development-guide.md) for setup instructions
4. Use [Source Tree Analysis](./source-tree-analysis.md) for code navigation
### For Feature Development
1. Reference [Architecture](./architecture.md) for patterns and conventions
2. Check [Source Tree Analysis](./source-tree-analysis.md) for file locations
3. Follow [Development Guide](./development-guide.md) for implementation steps
### For API Development
1. Review [Architecture](./architecture.md) - API Design section
2. Check existing API controllers in `app/Controllers/Api/`
3. Follow naming conventions from [Development Guide](./development-guide.md)
---
## Technology Stack Reference
| Layer | Technology |
|-------|------------|
| Backend | PHP 8.1+ |
| Framework | CodeIgniter 4 |
| Database | SQL Server |
| Frontend | TailwindCSS + Alpine.js + DaisyUI |
| Icons | FontAwesome 7 |
| Testing | PHPUnit |
---
## Key Directories
| Directory | Purpose | Documentation |
|-----------|---------|---------------|
| `app/Config/` | Configuration files | [Source Tree](./source-tree-analysis.md) |
| `app/Controllers/` | Request handlers | [Architecture](./architecture.md) |
| `app/Models/` | Data models | [Architecture](./architecture.md) |
| `app/Views/` | UI templates | [Architecture](./architecture.md) |
| `public/` | Web root | [Source Tree](./source-tree-analysis.md) |
| `tests/` | Unit tests | [Development Guide](./development-guide.md) |
---
## Common Tasks
| Task | Documentation |
|------|---------------|
| Setup development environment | [Development Guide - Installation](./development-guide.md#installation) |
| Add new feature | [Development Guide - Adding New Features](./development-guide.md#adding-new-features) |
| Run tests | [Development Guide - Running Tests](./development-guide.md#running-tests) |
| Database operations | [Development Guide - Database Operations](./development-guide.md#database-operations) |
| Debug application | [Development Guide - Debugging](./development-guide.md#debugging) |
| Deploy to production | [Development Guide - Deployment](./development-guide.md#deployment) |
---
## Module Documentation
### Dictionary Management
- Manage departments, tests, and control parameters
- Controllers: `Dept.php`, `Test.php`, `Control.php`
- API: `DeptApiController.php`, `TestApiController.php`, `ControlApiController.php`
### Data Entry
- Record daily and monthly QC results
- Controller: `Entry.php`
- API: `EntryApiController.php`
### Reporting
- Generate quality control reports
- Controller: `Report.php`
---
## API Endpoints Quick Reference
| Endpoint | Description |
|----------|-------------|
| `/api/dept` | Department CRUD |
| `/api/test` | Test CRUD |
| `/api/control` | Control CRUD |
| `/api/entry/*` | Entry operations |
See [Architecture](./architecture.md) for detailed API documentation.
---
*Documentation generated on 2026-01-20*
*For updates, run the document-project workflow*

102
docs/project-overview.md Normal file
View File

@ -0,0 +1,102 @@
# TinyQC - Quality Control Management System
## Executive Summary
TinyQC is a CodeIgniter 4 PHP application designed for quality control data management in laboratory settings. The system provides comprehensive tools for managing departments, tests, control parameters, daily/monthly entries, and generating QC reports. Built with a modern frontend stack (TailwindCSS, Alpine.js, DaisyUI) and SQL Server database.
**Repository Type:** Monolith (single cohesive codebase)
---
## Technology Stack
| Layer | Technology | Version |
|-------|------------|---------|
| Backend | PHP | 8.1+ |
| Backend Framework | CodeIgniter 4 | ^4.0 |
| Database | SQL Server | 2016+ |
| Frontend | TailwindCSS | Latest |
| Frontend | Alpine.js | Latest |
| UI Components | DaisyUI | Latest |
| Icons | FontAwesome | 7 |
| Testing | PHPUnit | 10.5.16 |
| Development | Composer | Latest |
---
## Architecture Classification
- **Architecture Pattern:** MVC (Model-View-Controller)
- **Application Type:** Backend Web Application
- **Project Type:** Backend (PHP/CodeIgniter 4)
- **Entry Point:** `public/index.php`
---
## Core Modules
### 1. Dictionary Management
- **Departments (Dept):** Manage department/category entries
- **Tests:** Maintain test parameters and specifications
- **Controls:** Configure control standards and limits
### 2. Data Entry
- **Daily Entry:** Record daily QC test results
- **Monthly Entry:** Aggregate monthly data and comments
### 3. Reporting
- Generate quality control reports based on date ranges, test types, and control parameters
### 4. Comments System
- Add notes and comments to results
---
## Key Features
- CRUD operations for departments, tests, and controls
- Daily and monthly quality control data recording
- Comment system for results annotation
- Report generation and analysis
- RESTful API endpoints for all modules
- Responsive UI with modal-based interactions
---
## Project Structure Overview
```
tinyqc/
├── app/
│ ├── Config/ # Configuration files
│ ├── Controllers/ # Application controllers
│ │ └── Api/ # API controllers
│ ├── Models/ # Database models
│ └── Views/ # View templates
├── public/ # Web root
├── tests/ # Unit tests
├── writable/ # Writable directory
├── _bmad/ # BMAD development artifacts
├── composer.json
└── phpunit.xml.dist
```
---
## Quick Reference
- **Documentation Root:** `/docs`
- **API Documentation:** See README.md and API endpoints section
- **Development Guide:** See [development-guide.md](./development-guide.md)
- **Architecture Details:** See [architecture.md](./architecture.md)
- **Source Tree:** See [source-tree-analysis.md](./source-tree-analysis.md)
---
## Related Documentation
- [README.md](../README.md) - Original project readme
- [AGENTS.md](../_bmad/AGENTS.md) - Development agent guidelines
- [development-guide.md](./development-guide.md) - Development setup and commands
- [architecture.md](./architecture.md) - Detailed architecture documentation
- [source-tree-analysis.md](./source-tree-analysis.md) - Annotated directory structure

View File

@ -0,0 +1,178 @@
# Source Tree Analysis - TinyQC
## Project Root Structure
```
tinyqc/
├── app/ # Application source code (MVC)
│ ├── Config/ # Framework and app configurations
│ ├── Controllers/ # HTTP request handlers
│ │ └── Api/ # REST API controllers
│ ├── Database/ # Database utilities
│ │ ├── Migrations/ # Database migrations
│ │ └── Seeds/ # Database seeders
│ ├── Filters/ # Request filters
│ ├── Helpers/ # Helper functions
│ ├── Language/ # Language files
│ ├── Libraries/ # Custom libraries
│ ├── Models/ # Data models
│ ├── ThirdParty/ # Third-party code
│ ├── Views/ # View templates
│ ├── BaseController.php # Controller base class
│ └── Common.php # Common functions
├── public/ # Web root directory
│ ├── index.php # Application entry point
│ ├── js/ # JavaScript files
│ ├── css/ # CSS files (if any)
│ ├── favicon.ico # Site icon
│ └── .htaccess # Apache configuration
├── tests/ # Unit tests
├── writable/ # Writable files (logs, cache, debugbar)
├── _bmad/ # BMAD development artifacts
├── vendor/ # Composer dependencies
├── composer.json # Composer configuration
├── env # Environment template
├── phpunit.xml.dist # PHPUnit configuration
└── spark # CodeIgniter CLI tool
```
---
## Application Directory (app/)
### Config Directory
| File | Purpose |
|------|---------|
| `App.php` | Main application configuration |
| `Database.php` | Database connection settings (SQL Server) |
| `Routes.php` | URL routing definitions |
| `Autoload.php` | Class autoloading configuration |
| `Boot/*.php` | Environment-specific boot configs |
| `Filters.php` | Request filter configurations |
| `Services.php` | Service container configurations |
| `Session.php` | Session settings |
| `Validation.php` | Form validation rules |
**Purpose:** Contains all CodeIgniter 4 framework configurations and application-specific settings.
---
### Controllers Directory
| Controller | Purpose |
|------------|---------|
| `BaseController.php` | Base controller with common functionality |
| `Dashboard.php` | Main dashboard controller |
| `Dept.php` | Department management |
| `Test.php` | Test/parameter management |
| `Control.php` | Control standards management |
| `Entry.php` | Daily/monthly entry management |
| `Report.php` | Report generation |
| `PageController.php` | Generic page controller |
**API Controllers:**
| Controller | Purpose |
|------------|---------|
| `Api/DeptApiController.php` | Department CRUD API |
| `Api/TestApiController.php` | Test/parameter CRUD API |
| `Api/ControlApiController.php` | Control CRUD API |
| `Api/EntryApiController.php` | Entry data API |
**Purpose:** Handle HTTP requests, process business logic, and return responses.
---
### Models Directory
| Model | Purpose |
|-------|---------|
| `BaseModel.php` | Base model with camel/snake case conversion |
| `DictDeptModel.php` | Department dictionary |
| `DictTestModel.php` | Test dictionary |
| `DictControlModel.php` | Control dictionary |
| `ControlModel.php` | Control management |
| `ControlTestModel.php` | Control-test relationships |
| `DeptModel.php` | Department operations |
| `TestModel.php` | Test operations |
| `ResultModel.php` | Result management |
| `DailyResultModel.php` | Daily QC results |
| `MonthlyCommentModel.php` | Monthly comments |
| `ResultCommentModel.php` | Result comments |
**Purpose:** Handle database operations, data validation, and business logic for each domain entity.
---
### Views Directory
| Directory/File | Purpose |
|----------------|---------|
| `layout/` | Layout templates |
| `dashboard.php` | Dashboard view |
| `dept/` | Department views |
| `test/` | Test views |
| `control/` | Control views |
| `entry/` | Entry views (daily, monthly) |
| `report/` | Report views |
| `errors/` | Error page templates |
**Purpose:** Presentation layer using PHP templates with TailwindCSS and DaisyUI styling.
---
## Public Directory
| File | Purpose |
|------|---------|
| `index.php` | Application bootstrap and entry point |
| `js/app.js` | Main JavaScript application |
| `js/tables.js` | Table functionality |
| `js/charts.js` | Chart/rendering functionality |
| `.htaccess` | URL rewriting for Apache |
| `favicon.ico` | Site icon |
| `robots.txt` | Search engine rules |
**Purpose:** Web root directory served by web server.
---
## Writable Directory
Contains runtime-generated files:
- `debugbar/` - Debug toolbar data
- `logs/` - Application logs
- `cache/` - Application cache
- `session/` - Session files
---
## Entry Points
| Entry Point | Type | Description |
|-------------|------|-------------|
| `public/index.php` | Web | Main application entry point |
| `spark` | CLI | CodeIgniter 4 CLI tool |
---
## Integration Points
This is a **monolith application** - all components are contained within a single codebase. No external service integrations detected in quick scan.
---
## Critical Folders Summary
| Folder | Critical | Purpose |
|--------|----------|---------|
| `app/Config/` | Yes | All configuration |
| `app/Controllers/` | Yes | Request handling |
| `app/Models/` | Yes | Data access layer |
| `app/Views/` | Yes | UI templates |
| `public/` | Yes | Web entry point |
| `writable/` | Yes | Runtime files |
| `tests/` | No | Unit tests |
| `_bmad/` | No | Development artifacts |
| `vendor/` | No | Dependencies |