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:
parent
5cae572916
commit
14baa6b758
4
.gitignore
vendored
4
.gitignore
vendored
@ -124,3 +124,7 @@ _modules/*
|
|||||||
|
|
||||||
/results/
|
/results/
|
||||||
/phpunit*.xml
|
/phpunit*.xml
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
_bmad/
|
||||||
|
_bmad-output/
|
||||||
172
CLAUDE.md
Normal file
172
CLAUDE.md
Normal 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
|
||||||
@ -19,6 +19,7 @@ $routes->get('/report', 'PageController::report');
|
|||||||
$routes->get('/report/view', 'PageController::reportView');
|
$routes->get('/report/view', 'PageController::reportView');
|
||||||
|
|
||||||
$routes->group('api', function ($routes) {
|
$routes->group('api', function ($routes) {
|
||||||
|
$routes->get('dashboard/recent', 'Api\DashboardApiController::getRecent');
|
||||||
$routes->get('dept', 'Api\DeptApiController::index');
|
$routes->get('dept', 'Api\DeptApiController::index');
|
||||||
$routes->get('dept/(:num)', 'Api\DeptApiController::show/$1');
|
$routes->get('dept/(:num)', 'Api\DeptApiController::show/$1');
|
||||||
$routes->post('dept', 'Api\DeptApiController::store');
|
$routes->post('dept', 'Api\DeptApiController::store');
|
||||||
|
|||||||
85
app/Controllers/Api/DashboardApiController.php
Normal file
85
app/Controllers/Api/DashboardApiController.php
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
app/Controllers/Api/ReportApiController.php
Normal file
119
app/Controllers/Api/ReportApiController.php
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,6 +8,7 @@ class QualityControlSystem extends Migration
|
|||||||
{
|
{
|
||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
|
// master_depts - No dependencies
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||||
'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
'name' => ['type' => 'VARCHAR', 'constraint' => 50, 'null' => true],
|
||||||
@ -18,6 +19,7 @@ class QualityControlSystem extends Migration
|
|||||||
$this->forge->addKey('dept_id', true);
|
$this->forge->addKey('dept_id', true);
|
||||||
$this->forge->createTable('master_depts');
|
$this->forge->createTable('master_depts');
|
||||||
|
|
||||||
|
// master_controls - FK to master_depts
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'control_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
'control_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||||
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => 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->addForeignKey('dept_id', 'master_depts', 'dept_id', 'SET NULL', 'CASCADE');
|
||||||
$this->forge->createTable('master_controls');
|
$this->forge->createTable('master_controls');
|
||||||
|
|
||||||
|
// master_tests - FK to master_depts
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
'test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||||
'dept_id' => ['type' => 'INT', 'unsigned' => true, 'null' => 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->addForeignKey('dept_id', 'master_depts', 'dept_id', 'SET NULL', 'CASCADE');
|
||||||
$this->forge->createTable('master_tests');
|
$this->forge->createTable('master_tests');
|
||||||
|
|
||||||
|
// control_tests - FK to master_controls, master_tests
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'control_test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
'control_test_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||||
'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => 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->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE');
|
||||||
$this->forge->createTable('control_tests');
|
$this->forge->createTable('control_tests');
|
||||||
|
|
||||||
|
// results - FK to master_controls, master_tests
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'result_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
'result_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||||
'control_id' => ['type' => 'INT', 'unsigned' => true, 'null' => 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->addForeignKey('test_id', 'master_tests', 'test_id', 'SET NULL', 'CASCADE');
|
||||||
$this->forge->createTable('results');
|
$this->forge->createTable('results');
|
||||||
|
|
||||||
|
// result_comments - FK to master_controls, master_tests (composite unique key)
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'result_comment_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
'result_comment_id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
|
||||||
'control_id' => ['type' => 'INT', 'unsigned' => true],
|
'control_id' => ['type' => 'INT', 'unsigned' => true],
|
||||||
@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
137
app/Database/Seeds/CmodQcSeeder.php
Normal file
137
app/Database/Seeds/CmodQcSeeder.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ class MasterControlsModel extends BaseModel {
|
|||||||
protected $primaryKey = 'control_id';
|
protected $primaryKey = 'control_id';
|
||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'dept_id',
|
'dept_id',
|
||||||
'name',
|
'control_name',
|
||||||
'lot',
|
'lot',
|
||||||
'producer',
|
'producer',
|
||||||
'exp_date',
|
'exp_date',
|
||||||
@ -22,7 +22,7 @@ class MasterControlsModel extends BaseModel {
|
|||||||
public function search($keyword = null) {
|
public function search($keyword = null) {
|
||||||
if ($keyword) {
|
if ($keyword) {
|
||||||
return $this->groupStart()
|
return $this->groupStart()
|
||||||
->like('name', $keyword)
|
->like('control_name', $keyword)
|
||||||
->orLike('lot', $keyword)
|
->orLike('lot', $keyword)
|
||||||
->groupEnd()
|
->groupEnd()
|
||||||
->findAll();
|
->findAll();
|
||||||
|
|||||||
@ -7,7 +7,7 @@ class MasterDeptsModel extends BaseModel {
|
|||||||
protected $table = 'master_depts';
|
protected $table = 'master_depts';
|
||||||
protected $primaryKey = 'dept_id';
|
protected $primaryKey = 'dept_id';
|
||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'name',
|
'dept_name',
|
||||||
'created_at',
|
'created_at',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
'deleted_at'
|
'deleted_at'
|
||||||
@ -18,7 +18,7 @@ class MasterDeptsModel extends BaseModel {
|
|||||||
public function search($keyword = null) {
|
public function search($keyword = null) {
|
||||||
if ($keyword) {
|
if ($keyword) {
|
||||||
return $this->groupStart()
|
return $this->groupStart()
|
||||||
->like('name', $keyword)
|
->like('dept_name', $keyword)
|
||||||
->groupEnd()
|
->groupEnd()
|
||||||
->findAll();
|
->findAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,9 @@ class MasterTestsModel extends BaseModel {
|
|||||||
protected $primaryKey = 'test_id';
|
protected $primaryKey = 'test_id';
|
||||||
protected $allowedFields = [
|
protected $allowedFields = [
|
||||||
'dept_id',
|
'dept_id',
|
||||||
'name',
|
'test_name',
|
||||||
'unit',
|
'test_unit',
|
||||||
'method',
|
'test_method',
|
||||||
'cva',
|
'cva',
|
||||||
'ba',
|
'ba',
|
||||||
'tea',
|
'tea',
|
||||||
@ -24,7 +24,7 @@ class MasterTestsModel extends BaseModel {
|
|||||||
public function search($keyword = null) {
|
public function search($keyword = null) {
|
||||||
if ($keyword) {
|
if ($keyword) {
|
||||||
return $this->groupStart()
|
return $this->groupStart()
|
||||||
->like('name', $keyword)
|
->like('test_name', $keyword)
|
||||||
->groupEnd()
|
->groupEnd()
|
||||||
->findAll();
|
->findAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,66 +2,125 @@
|
|||||||
|
|
||||||
<?= $this->section("content"); ?>
|
<?= $this->section("content"); ?>
|
||||||
<main class="flex-1 p-6 overflow-auto">
|
<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 class="mb-6">
|
||||||
<div>
|
<h1 class="text-2xl font-bold tracking-tight text-base-content">Dashboard</h1>
|
||||||
<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>
|
||||||
<p class="text-sm mt-1 opacity-70">Quality Control Overview</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
<!-- Quick Action Cards -->
|
||||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
||||||
<div class="flex items-center justify-between">
|
<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>
|
<div class="card-body items-center text-center py-4">
|
||||||
<p class="text-sm text-base-content/60">Total Controls</p>
|
<i class="fa-solid fa-pen-to-square text-2xl mb-2"></i>
|
||||||
<p class="text-2xl font-bold text-base-content mt-1">24</p>
|
<span class="font-medium">QC Entry</span>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</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="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
<div class="card-body items-center text-center py-4">
|
||||||
<div class="flex items-center justify-between">
|
<i class="fa-solid fa-building text-2xl mb-2"></i>
|
||||||
<div>
|
<span class="font-medium">Departments</span>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
<div class="card-body items-center text-center py-4">
|
||||||
<div class="flex items-center justify-between">
|
<i class="fa-solid fa-flask-vial text-2xl mb-2"></i>
|
||||||
<div>
|
<span class="font-medium">Tests</span>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</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="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
<div class="card-body items-center text-center py-4">
|
||||||
<div class="flex items-center justify-between">
|
<i class="fa-solid fa-vial text-2xl mb-2"></i>
|
||||||
<div>
|
<span class="font-medium">Controls</span>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="bg-base-100 rounded-xl border border-base-300 shadow-sm p-6">
|
<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>
|
x-data="dashboardRecentResults()">
|
||||||
<p class="text-base-content/60 text-center py-8">Dashboard content coming soon...</p>
|
<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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<?= $this->endSection(); ?>
|
<?= $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(); ?>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="autumn">
|
<html lang="en" data-theme="autumn">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
<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">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
<script>
|
<script>
|
||||||
const BASEURL = '<?= base_url('/') ?>';
|
const BASEURL = '<?= base_url(''); ?>';
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-base-200 text-base-content" x-data="appData()">
|
<body class="bg-base-200 text-base-content" x-data="appData()">
|
||||||
<div class="drawer lg:drawer-open">
|
<div class="drawer lg:drawer-open">
|
||||||
<input id="sidebar-drawer" type="checkbox" class="drawer-toggle" x-ref="sidebarDrawer">
|
<input id="sidebar-drawer" type="checkbox" class="drawer-toggle" x-ref="sidebarDrawer">
|
||||||
|
|
||||||
<div class="drawer-content flex flex-col min-h-screen">
|
<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">
|
<div class="flex-none lg:hidden">
|
||||||
<label for="sidebar-drawer" class="btn btn-square btn-ghost text-primary">
|
<label for="sidebar-drawer" class="btn btn-square btn-ghost text-primary">
|
||||||
<i class="fa-solid fa-bars text-xl"></i>
|
<i class="fa-solid fa-bars text-xl"></i>
|
||||||
@ -26,7 +29,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1 px-4">
|
<div class="flex-1 px-4">
|
||||||
<div class="flex items-center gap-3">
|
<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>
|
<i class="fa-solid fa-flask text-white text-lg"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -41,22 +45,32 @@
|
|||||||
<i class="fa-solid fa-moon text-neutral-content" x-show="currentTheme === themeConfig.dark"></i>
|
<i class="fa-solid fa-moon text-neutral-content" x-show="currentTheme === themeConfig.dark"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown dropdown-end" x-data="{ dropdownOpen: false }">
|
<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 tabindex="0" role="button" class="btn btn-ghost btn-circle avatar placeholder"
|
||||||
<div class="bg-primary text-primary-content rounded-full w-10 h-10 flex items-center justify-center">
|
@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>
|
<span><?= $pageData['userInitials'] ?? 'DR' ?></span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<li class="menu-title px-4 py-2">
|
||||||
<span class="text-base-content font-bold"><?= $pageData['userName'] ?? 'Lab User' ?></span>
|
<span
|
||||||
<span class="text-xs text-primary font-medium"><?= $pageData['userRole'] ?? 'Administrator' ?></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>
|
</li>
|
||||||
<div class="divider my-0 h-px opacity-10"></div>
|
<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-user text-primary"></i> Profile</a>
|
||||||
<li><a class="hover:bg-base-200"><i class="fa-solid fa-gear text-primary"></i> Settings</a></li>
|
</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-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>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -70,7 +84,8 @@
|
|||||||
<aside class="bg-base-300 border-r border-base-300 w-64 min-h-full flex flex-col">
|
<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">
|
<ul class="menu p-4 text-base-content flex-1 w-full">
|
||||||
<li class="mb-1 min-h-0">
|
<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>
|
<i class="fa-solid fa-chart-line w-5"></i>
|
||||||
Dashboard
|
Dashboard
|
||||||
</a>
|
</a>
|
||||||
@ -80,19 +95,22 @@
|
|||||||
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">Master Data</p>
|
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">Master Data</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1 min-h-0">
|
<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>
|
<i class="fa-solid fa-building w-5"></i>
|
||||||
Departments
|
Departments
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1 min-h-0">
|
<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>
|
<i class="fa-solid fa-flask-vial w-5"></i>
|
||||||
Tests
|
Tests
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1 min-h-0">
|
<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>
|
<i class="fa-solid fa-vial w-5"></i>
|
||||||
Controls
|
Controls
|
||||||
</a>
|
</a>
|
||||||
@ -102,13 +120,15 @@
|
|||||||
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">QC Operations</p>
|
<p class="px-4 text-xs font-semibold opacity-40 uppercase tracking-wider">QC Operations</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1 min-h-0">
|
<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>
|
<i class="fa-solid fa-pen-to-square w-5"></i>
|
||||||
QC Entry
|
QC Entry
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1 min-h-0">
|
<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>
|
<i class="fa-solid fa-chart-bar w-5"></i>
|
||||||
Reports
|
Reports
|
||||||
</a>
|
</a>
|
||||||
@ -168,4 +188,5 @@
|
|||||||
|
|
||||||
<?= $this->renderSection('script') ?>
|
<?= $this->renderSection('script') ?>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -32,7 +32,7 @@
|
|||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
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"
|
placeholder="e.g., LOT12345"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
<tr class="hover:bg-base-200 transition-colors">
|
<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 font-medium text-base-content" x-text="item.controlName"></td>
|
||||||
<td class="py-3 px-5">
|
<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>
|
||||||
<td class="py-3 px-5" x-text="item.producer"></td>
|
<td class="py-3 px-5" x-text="item.producer"></td>
|
||||||
<td class="py-3 px-5" x-text="item.expDate"></td>
|
<td class="py-3 px-5" x-text="item.expDate"></td>
|
||||||
@ -116,18 +116,22 @@
|
|||||||
form: {
|
form: {
|
||||||
controlId: null,
|
controlId: null,
|
||||||
controlName: "",
|
controlName: "",
|
||||||
lotNumber: "",
|
lot: "",
|
||||||
producer: "",
|
producer: "",
|
||||||
expDate: "",
|
expDate: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
async fetchList() {
|
async fetchList() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.list = null;
|
this.list = null;
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ keyword: this.keyword });
|
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",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
@ -144,7 +148,7 @@
|
|||||||
async loadData(id) {
|
async loadData(id) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${window.BASEURL}api/master/controls/${id}`, {
|
const response = await fetch(`${BASEURL}api/master/controls/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
@ -165,13 +169,13 @@
|
|||||||
if (id) {
|
if (id) {
|
||||||
await this.loadData(id);
|
await this.loadData(id);
|
||||||
} else {
|
} else {
|
||||||
this.form = { controlId: null, controlName: "", lotNumber: "", producer: "", expDate: "" };
|
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "" };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
closeModal() {
|
closeModal() {
|
||||||
this.showModal = false;
|
this.showModal = false;
|
||||||
this.form = { controlId: null, controlName: "", lotNumber: "", producer: "", expDate: "" };
|
this.form = { controlId: null, controlName: "", lot: "", producer: "", expDate: "" };
|
||||||
},
|
},
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
@ -187,10 +191,10 @@
|
|||||||
let url = '';
|
let url = '';
|
||||||
if (this.form.controlId) {
|
if (this.form.controlId) {
|
||||||
method = 'PATCH';
|
method = 'PATCH';
|
||||||
url = `${window.BASEURL}api/master/controls/${this.form.controlId}`;
|
url = `${BASEURL}api/master/controls/${this.form.controlId}`;
|
||||||
} else {
|
} else {
|
||||||
method = 'POST';
|
method = 'POST';
|
||||||
url = `${window.BASEURL}api/master/controls`;
|
url = `${BASEURL}api/master/controls`;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
@ -218,7 +222,7 @@
|
|||||||
if (!confirm("Are you sure you want to delete this item?")) return;
|
if (!confirm("Are you sure you want to delete this item?")) return;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${window.BASEURL}api/master/controls/${id}`, {
|
const res = await fetch(`${BASEURL}api/master/controls/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,8 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
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
|
<i class="fa-solid fa-plus"></i> New Department
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -19,18 +18,13 @@
|
|||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="relative flex-1 max-w-md">
|
<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>
|
<i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 opacity-50 text-sm"></i>
|
||||||
<input
|
<input type="text" placeholder="Search by name..."
|
||||||
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"
|
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"
|
x-model="keyword" @keyup.enter="fetchList()" />
|
||||||
@keyup.enter="fetchList()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
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
|
<i class="fa-solid fa-magnifying-glass text-xs"></i> Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -67,14 +61,12 @@
|
|||||||
<td class="py-3 px-5 text-right">
|
<td class="py-3 px-5 text-right">
|
||||||
<button
|
<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"
|
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
|
<i class="fa-solid fa-pencil"></i> Edit
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@ -116,7 +108,7 @@
|
|||||||
this.list = null;
|
this.list = null;
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ keyword: this.keyword });
|
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",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
@ -133,7 +125,7 @@
|
|||||||
async loadData(id) {
|
async loadData(id) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${window.BASEURL}api/master/depts/${id}`, {
|
const response = await fetch(`${BASEURL}api/master/depts/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
@ -176,10 +168,10 @@
|
|||||||
let url = '';
|
let url = '';
|
||||||
if (this.form.deptId) {
|
if (this.form.deptId) {
|
||||||
method = 'PATCH';
|
method = 'PATCH';
|
||||||
url = `${window.BASEURL}api/master/depts/${this.form.deptId}`;
|
url = `${BASEURL}api/master/depts/${this.form.deptId}`;
|
||||||
} else {
|
} else {
|
||||||
method = 'POST';
|
method = 'POST';
|
||||||
url = `${window.BASEURL}api/master/depts`;
|
url = `${BASEURL}api/master/depts`;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
@ -207,7 +199,7 @@
|
|||||||
if (!confirm("Are you sure you want to delete this item?")) return;
|
if (!confirm("Are you sure you want to delete this item?")) return;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${window.BASEURL}api/master/depts/${id}`, {
|
const res = await fetch(`${BASEURL}api/master/depts/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
|
|||||||
@ -119,13 +119,17 @@
|
|||||||
tea: "",
|
tea: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchList();
|
||||||
|
},
|
||||||
|
|
||||||
async fetchList() {
|
async fetchList() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.list = null;
|
this.list = null;
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ keyword: this.keyword });
|
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",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
@ -142,7 +146,7 @@
|
|||||||
async loadData(id) {
|
async loadData(id) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${window.BASEURL}api/master/tests/${id}`, {
|
const response = await fetch(`${BASEURL}api/master/tests/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
@ -185,10 +189,10 @@
|
|||||||
let url = '';
|
let url = '';
|
||||||
if (this.form.testId) {
|
if (this.form.testId) {
|
||||||
method = 'PATCH';
|
method = 'PATCH';
|
||||||
url = `${window.BASEURL}api/master/tests/${this.form.testId}`;
|
url = `${BASEURL}api/master/tests/${this.form.testId}`;
|
||||||
} else {
|
} else {
|
||||||
method = 'POST';
|
method = 'POST';
|
||||||
url = `${window.BASEURL}api/master/tests`;
|
url = `${BASEURL}api/master/tests`;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
@ -216,7 +220,7 @@
|
|||||||
if (!confirm("Are you sure you want to delete this item?")) return;
|
if (!confirm("Are you sure you want to delete this item?")) return;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${window.BASEURL}api/master/tests/${id}`, {
|
const res = await fetch(`${BASEURL}api/master/tests/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
|
|||||||
282
docs/architecture.md
Normal file
282
docs/architecture.md
Normal 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
435
docs/development-guide.md
Normal 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
128
docs/index.md
Normal 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
102
docs/project-overview.md
Normal 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
|
||||||
178
docs/source-tree-analysis.md
Normal file
178
docs/source-tree-analysis.md
Normal 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 |
|
||||||
Loading…
x
Reference in New Issue
Block a user