diff --git a/AGENTS.md b/AGENTS.md
index ca17ce0..6ccfb87 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,340 +1,597 @@
-# AGENTS.md - QC Application Development Guide
+# AGENTS.md - AI Agent Guidelines for [PROJECT NAME]
-This document provides guidelines for agentic coding agents working on this PHP QC (Quality Control) application built with CodeIgniter 4.
+## AI Agent Guidelines
-## Project Overview
+1. **Readability**: Write code that is easy to read and understand.
+2. **Maintainability**: Write code that is easy to maintain and update.
+3. **Performance**: Write code that is fast and efficient.
+4. **Security**: Write code that is secure and protected against attacks.
+5. **Testing**: Write code that is tested and verified.
-This is a CodeIgniter 4 PHP application using SQL Server database for quality control data management. The app handles control tests, daily/monthly entries, and reporting. Uses Tailwind CSS and Alpine.js for UI.
+## Technology Stack
-## Build/Lint/Test Commands
+| Layer | Technology |
+|-------|------------|
+| Backend | CodeIgniter 4 (PHP 8.1+) |
+| Frontend | Alpine.js + TailwindCSS |
+| Database | MySQL/MariaDB |
-```bash
-# PHP syntax check single file
-php -l app/Controllers/Dashboard.php
+## Key Files & Locations
-# PHP syntax check all files recursively
-find . -name "*.php" -exec php -l {} \; 2>&1 | grep -v "No syntax errors"
+### Backend
-# Run all PHPUnit tests
-./vendor/bin/phpunit
-
-# Run single test class
-./vendor/bin/phpunit tests/unit/HealthTest.php
-
-# Run single test method
-./vendor/bin/phpunit tests/unit/HealthTest.php --filter=testIsDefinedAppPath
-
-# Run with coverage report
-./vendor/bin/phpunit --coverage-html coverage/
-
-# Start development server
-php spark serve
+```
+app/Controllers/ # API & page controllers
+app/Models/ # Eloquent-style models
+app/Database/Migrations/ # Schema definitions
+app/Config/Routes.php # All routes defined here
+app/Helpers/ # Helper functions
```
-## Code Style Guidelines
+### Frontend
-### General Principles
+```
+app/Views/ # PHP views with Alpine.js
+app/Views/layout/ # Base templates
+public/ # Static assets (css, js)
+```
-- Follow CodeIgniter 4 MVC patterns
-- Maintain consistency with surrounding code
-- Keep files focused (<200 lines preferred)
-- Use clear, descriptive names
+## Coding Conventions
-### PHP Style
+### PHP / CodeIgniter 4
-- Use `respond([
+ 'status' => 'success',
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ ```
-### Naming Conventions
+7. **Use camelCase for input/output**, snake_case for database:
+ - Controllers convert camel→snake before insert/update
+ - Models convert snake→camel after fetch
+ - Use helper functions: `camel_to_snake()`, `camel_to_snake_array()`, `snake_to_camel()`
-- Classes: `PascalCase` (e.g., `Dashboard`, `DictTestModel`)
-- Methods/functions: `$camelCase` (e.g., `getWithDept`, `saveResult`)
-- Variables: `$camelCase` (e.g., `$dictTestModel`, `$resultData`)
-- Constants: `UPPER_SNAKE_CASE`
-- Views: `lowercase_with_underscores` (e.g., `dashboard.php`, `test_index.php`)
-- Routes: kebab-case URLs (e.g., `/test/edit/(:num)` → `/test/edit/1`)
+### Database
-### CodeIgniter 4 Patterns
+1. **Primary keys**: `{table_singular}_id` (e.g., `item_id`, `pat_id`)
+2. **Foreign keys**: Match the referenced primary key name
+3. **Naming**: All lowercase, underscores
+4. **Soft deletes**: All tables have `deleted_at` DATETIME column
+5. **Master data tables**: Prefix with `master_` (e.g., `master_items`)
+6. **Timestamps**: `created_at`, `updated_at` DATETIME columns
+7. **Unique constraints**: Add on code fields (e.g., `item_code`)
+
+### Frontend / Alpine.js
+
+1. **x-data** on container elements
+2. **Fetch API** for AJAX calls (no jQuery)
+3. **DaisyUI components** for UI elements
+4. **camelCase** for JavaScript, **snake_case** for PHP/DB
+5. **Modals** with x-show and x-transition
+6. **ES6 modules** importing Alpine from `app.js`
+
+### File Naming
+
+| Component | Pattern | Example |
+|-----------|---------|---------|
+| Controller | `PascalCase + Controller` | `ItemsController` |
+| Model | `PascalCase + Model` | `ItemsModel` |
+| Migration | `YYYY-MM-DD-XXXXXX_Description.php` | `2026-01-15-000001_Items.php` |
+| View | `module/action.php` | `items/index.php` |
+| Helper | `snake_case + _helper.php` | `stringcase_helper.php` |
+| Filter | `PascalCase + Filter.php` | `JwtAuthFilter.php` |
+
+### Directory Structure
+
+```
+app/
+├── Config/
+│ ├── Routes.php
+│ └── Filters.php
+├── Controllers/
+│ ├── BaseController.php
+│ ├── ItemsController.php
+│ └── Master/
+│ └── ItemsController.php
+├── Database/
+│ └── Migrations/
+├── Filters/
+│ └── JwtAuthFilter.php
+├── Helpers/
+│ ├── stringcase_helper.php
+│ └── utc_helper.php
+├── Models/
+│ ├── BaseModel.php
+│ ├── ItemsModel.php
+│ └── Master/
+│ └── ItemsModel.php
+└── Views/
+ ├── layout/
+ │ ├── main_layout.php
+ │ └── form_layout.php
+ ├── items/
+ │ ├── items_index.php
+ │ └── dialog_items_form.php
+ └── login.php
+```
+
+## Common Tasks
+
+### Adding a New Master Data Entity
+
+1. Create migration in `app/Database/Migrations/`
+2. Create model in `app/Models/[Module]/`
+3. Create controller in `app/Controllers/[Module]/`
+4. Add routes in `app/Config/Routes.php`
+5. Create view in `app/Views/[module]/`
+
+### Adding a New API Endpoint
-**Controllers** extend `App\Controllers\BaseController`:
```php
-namespace App\Controllers;
-
-use App\Models\DictTestModel;
-
-class Test extends BaseController
-{
- protected $dictTestModel;
-
- public function __construct()
- {
- $this->dictTestModel = new DictTestModel();
- }
-
- public function index(): string
- {
- $data = [
- 'title' => 'Test Dictionary',
- 'tests' => $this->dictTestModel->findAll(),
- ];
-
- return view('layout', [
- 'content' => view('test/index', $data),
- 'page_title' => 'Test Dictionary',
- 'active_menu' => 'test'
- ]);
- }
-}
+// In Routes.php
+$routes->get('api/resource', 'ResourceController::index');
+$routes->get('api/resource/(:num)', 'ResourceController::show/$1');
+$routes->post('api/resource', 'ResourceController::create');
+$routes->patch('api/resource/(:num)', 'ResourceController::update/$1');
```
-**Models** extend `CodeIgniter\Model`:
-```php
-namespace App\Models;
+### Controller Template
-use CodeIgniter\Model;
-
-class TestModel extends Model
-{
- protected $table = 'dict_test';
- protected $primaryKey = 'id';
- protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['deptid', 'name', 'unit', 'method'];
-}
-```
-
-**Views** use PHP short tags `= ?>` for output:
-```php
-
-
= $title ?>
-
-
= $test['name'] ?>
-
-
-```
-
-### Database Operations
-
-- Configure connections in `app/Config/Database.php`
-- Use CodeIgniter's Query Builder for queries
-- Use parameterized queries to prevent SQL injection:
- ```php
- $builder = $this->db->table('results');
- $builder->select('*');
- $builder->where('control_ref_id', $controlId);
- $results = $builder->get()->getResultArray();
- ```
-- For SQL Server, set `DBDriver` to `'SQLSRV'` in config
-
-### Error Handling
-
-- Use CodeIgniter's exception handling: `throw new \CodeIgniter\Exceptions\PageNotFoundException('Not found')`
-- Return JSON responses for AJAX endpoints:
- ```php
- return $this->response->setJSON(['success' => true, 'data' => $results]);
- ```
-- Use `log_message('error', $message)` for logging
-- Validate input with `$this->validate()` in controllers
-
-### Frontend (Tailwind CSS + Alpine.js)
-
-The layout already includes Tailwind via CDN and Alpine.js:
-```html
-
-
-```
-
-**Common JavaScript (`public/js/app.js`):**
-- Contains global `App` object with shared utilities
-- Handles sidebar toggle functionality
-- Provides `App.showToast()`, `App.confirmSave()`, `App.closeAllModals()`
-- Initialize with `App.init()` on DOMContentLoaded
-
-**Page-Specific Alpine.js:**
-- Complex Alpine.js components should be defined inline in the view PHP file
-- Use `x-data` with an object containing all properties and methods
-- Example:
- ```html
-
-
-
- ```
-
-### File Organization
-
-- Controllers: `app/Controllers/`
-- Models: `app/Models/`
-- Views: `app/Views/` (subfolders for modules: `entry/`, `test/`, `control/`, `report/`, `dept/`)
-- Config: `app/Config/`
-- Routes: `app/Config/Routes.php`
-
-### Modal-based CRUD Pattern
-
-All CRUD operations use a single modal dialog file (`dialog.php`) that handles both add and edit modes.
-
-**File Naming:**
-- Single dialog: `dialog.php` (no `_add` or `_edit` suffix)
-
-**Controller Pattern:**
-```php
-class Dept extends BaseController
-{
- protected $dictDeptModel;
-
- public function __construct()
- {
- $this->dictDeptModel = new DictDeptModel();
- }
-
- public function index(): string
- {
- $data = [
- 'title' => 'Department Dictionary',
- 'depts' => $this->dictDeptModel->findAll(),
- ];
-
- return view('layout', [
- 'content' => view('dept/index', $data),
- 'page_title' => 'Department Dictionary',
- 'active_menu' => 'dept'
- ]);
- }
-
- public function save(): \CodeIgniter\HTTP\RedirectResponse
- {
- $data = ['name' => $this->request->getPost('name')];
- $this->dictDeptModel->insert($data);
- return redirect()->to('/dept');
- }
-
- public function update($id): \CodeIgniter\HTTP\RedirectResponse
- {
- $data = ['name' => $this->request->getPost('name')];
- $this->dictDeptModel->update($id, $data);
- return redirect()->to('/dept');
- }
-
- public function delete($id): \CodeIgniter\HTTP\RedirectResponse
- {
- $this->dictDeptModel->delete($id);
- return redirect()->to('/dept');
- }
-}
-```
-
-**Dialog View Pattern (`dialog.php`):**
```php
+namespace App\Controllers\Module;
-
-
-
Add
-
+use CodeIgniter\API\ResponseTrait;
+use App\Controllers\BaseController;
+use App\Models\Module\ItemModel;
-
-
= $title ?>
-
-
-
-```
+class ItemsController extends BaseController {
+ use ResponseTrait;
-**Index View Pattern (`index.php`):**
-```php
-
-
-
-
-
-
- = $record['name'] ?>
-
- Edit
- Delete
-
-
-
-
-
-
+ protected $model;
+ protected $rules;
-
-= view('module/dialog') ?>
+ public function __construct() {
+ $this->model = new ItemsModel();
+ $this->rules = [
+ 'itemCode' => 'required|min_length[1]',
+ 'itemName' => 'required',
+ ];
+ }
-
+ }
+
+ public function show($id = null) {
+ try {
+ $rows = $this->model->where('item_id', $id)->findAll();
+ if (empty($rows)) {
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'data not found.'
+ ], 200);
+ }
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
+
+ public function create() {
+ $input = $this->request->getJSON(true);
+ $input = camel_to_snake_array($input);
+ if (!$this->validate($this->rules)) {
+ return $this->failValidationErrors($this->validator->getErrors());
+ }
+ try {
+ $id = $this->model->insert($input, true);
+ return $this->respondCreated([
+ 'status' => 'success',
+ 'message' => $id
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
+
+ public function update($id = null) {
+ $input = $this->request->getJSON(true);
+ $input = camel_to_snake_array($input);
+ if (!$this->validate($this->rules)) {
+ return $this->failValidationErrors($this->validator->getErrors());
+ }
+ try {
+ $this->model->update($id, $input);
+ return $this->respondCreated([
+ 'status' => 'success',
+ 'message' => 'update success',
+ 'data' => $id
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
+}
```
-**Searchable Multi-Select:**
-- Use Select2 for searchable dropdowns (already included in layout.php)
-- Initialize with: `$('#selectId').select2({ placeholder: 'Search...', allowClear: true })`
+### Model Template
-### Security Considerations
+```php
+validate('csrf')`)
-- Escape all output in views with `= ?>` (auto-escaped)
-- Use `$this->request->getPost()` instead of `$_POST`
-- Never commit `.env` files with credentials
+use App\Models\BaseModel;
-## Common Operations
+class ItemsModel extends BaseModel {
+ protected $table = 'module_items';
+ protected $primaryKey = 'item_id';
+ protected $allowedFields = [
+ 'item_code',
+ 'item_name',
+ 'created_at',
+ 'updated_at',
+ 'deleted_at'
+ ];
+ protected $useTimestamps = true;
+ protected $useSoftDeletes = true;
-**Add a new CRUD resource:**
-1. Create model in `app/Models/`
-2. Create controller in `app/Controllers/`
-3. Add routes in `app/Config/Routes.php`
-4. Create views in `app/Views/[module]/`
+ public function getItems($keyword = null) {
+ if ($keyword) {
+ return $this->groupStart()
+ ->like('item_code', $keyword)
+ ->orLike('item_name', $keyword)
+ ->groupEnd()
+ ->findAll();
+ }
+ return $this->findAll();
+ }
+}
+```
-**Add a menu item:**
-- Add to sidebar in `app/Views/layout.php`
-- Add route in `app/Config/Routes.php`
-- Create controller method
-- Set `active_menu` parameter in view() call
+### Migration Template
+
+```php
+forge->addField([
+ 'item_id' => [
+ 'type' => 'int',
+ 'unsigned' => true,
+ 'auto_increment' => true
+ ],
+ 'item_code' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 50
+ ],
+ 'item_name' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 150
+ ],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true
+ ],
+ 'updated_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true
+ ],
+ 'deleted_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true
+ ]
+ ]);
+ $this->forge->addKey('item_id', true);
+ $this->forge->addUniqueKey('item_code');
+ $this->forge->createTable('module_items');
+ }
+
+ public function down() {
+ $this->forge->dropTable('module_items', true);
+ }
+}
+```
+
+### Routes Template
+
+```php
+post('/login', 'AuthController::login');
+$routes->get('/logout', 'AuthController::logout');
+
+$routes->group('', ['filter' => 'jwt-auth'], function ($routes) {
+ $routes->get('/', 'PagesController::dashboard');
+ $routes->get('/module', 'PagesController::module');
+ $routes->get('/master/items', 'PagesController::masterItems');
+});
+
+$routes->group('api', function ($routes) {
+ $routes->get('module/items', 'Module\ItemsController::index');
+ $routes->get('module/items/(:num)', 'Module\ItemsController::show/$1');
+ $routes->post('module/items', 'Module\ItemsController::create');
+ $routes->patch('module/items/(:num)', 'Module\ItemsController::update/$1');
+});
+```
+
+### View Template (Index with Alpine.js)
+
+```php
+= $this->extend("layout/main_layout"); ?>
+
+= $this->section("content"); ?>
+
+
+
+
Items
+
Manage your items
+
+
+ New Item
+
+
+
+
+
+
+
+
+
+
+
+ Code
+ Name
+ Action
+
+
+
+
+
+
+
+
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+
+
+ = $this->include('module/items/dialog_items_form'); ?>
+
+= $this->endSection(); ?>
+
+= $this->section("script"); ?>
+
+= $this->endSection(); ?>
+```
+
+### Running Migrations
+
+```bash
+php spark migrate # Run all pending
+php spark migrate:rollback # Rollback last batch
+php spark migrate:refresh # Rollback all + re-run
+```
+
+## Testing
+
+```bash
+# Run all tests
+./vendor/bin/phpunit
+
+# Run specific test file
+./vendor/bin/phpunit tests/unit/SomeTest.php
+```
+
+## Things to Avoid
+
+1. **Don't use jQuery** - Use Alpine.js or vanilla JS
+2. **Don't over-engineer** - This is a "no-nonsense" project
+3. **Don't skip soft deletes** - Always use `deleted_at`
+4. **Don't hardcode** - Use `.env` for configuration
+5. **Don't mix concerns** - Controllers handle HTTP, Models handle data
+6. **Don't forget camel/snake conversion** - Frontend uses camelCase, DB uses snake_case
+
+## Questions to Ask Before Making Changes
+
+1. Does this follow the existing patterns?
+2. Is there a simpler way to do this?
+3. Did I add the route?
+4. Did I handle errors gracefully?
+5. Does the API response match the standard format?
+6. Did I convert camelCase to snake_case before DB operations?
+7. Did I convert snake_case to camelCase after fetching?
+
+## Post-Change Requirements
+
+**After every significant code change, update `README.md`:**
+
+| Change Type | README Section to Update |
+|-------------|--------------------------|
+| New Controller | Project Structure, API Endpoints |
+| New Model | Project Structure |
+| New Migration/Table | Database Schema |
+| New View | Project Structure |
+| New Route | API Endpoints |
+| New Command | Development Commands |
+| Config Change | Setup |
+
+Keep the README accurate and up-to-date with the actual codebase.
diff --git a/README.md b/README.md
index d14b4c9..b43eb20 100644
--- a/README.md
+++ b/README.md
@@ -1,68 +1,207 @@
-# CodeIgniter 4 Application Starter
+# TinyQC - Quality Control Management System
-## What is CodeIgniter?
+A CodeIgniter 4 PHP application for quality control data management in laboratory settings. Built with Tailwind CSS, Alpine.js, and DaisyUI.
-CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure.
-More information can be found at the [official site](https://codeigniter.com).
+## Features
-This repository holds a composer-installable app starter.
-It has been built from the
-[development repository](https://github.com/codeigniter4/CodeIgniter4).
+- **Dictionary Management**: Maintain departments, tests, and control parameters
+- **Control Management**: Configure and manage quality control standards
+- **Daily Entries**: Record daily quality control test results
+- **Monthly Entries**: Aggregate and track monthly quality control data
+- **Comments System**: Add notes and comments to results
+- **Reporting**: Generate quality control reports and analysis
-More information about the plans for version 4 can be found in [CodeIgniter 4](https://forum.codeigniter.com/forumdisplay.php?fid=28) on the forums.
+## Technology Stack
-You can read the [user guide](https://codeigniter.com/user_guide/)
-corresponding to the latest version of the framework.
+| Layer | Technology |
+|-------|------------|
+| Backend | CodeIgniter 4 (PHP 8.1+) |
+| Database | SQL Server |
+| Frontend | TailwindCSS + Alpine.js + DaisyUI |
+| Icons | FontAwesome 7 |
+| Testing | PHPUnit |
-## Installation & updates
+## Requirements
-`composer create-project codeigniter4/appstarter` then `composer update` whenever
-there is a new release of the framework.
+- PHP 8.1 or higher
+- SQL Server 2016+
+- Composer
+- Web server (Apache/Nginx/IIS)
-When updating, check the release notes to see if there are any changes you might need to apply
-to your `app` folder. The affected files can be copied or merged from
-`vendor/codeigniter4/framework/app`.
+## Installation
-## Setup
+1. **Clone the repository**
+ ```bash
+ git clone tinyqc
+ cd tinyqc
+ ```
-Copy `env` to `.env` and tailor for your app, specifically the baseURL
-and any database settings.
+2. **Install dependencies**
+ ```bash
+ composer install
+ ```
-## Important Change with index.php
+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
+ ```
-`index.php` is no longer in the root of the project! It has been moved inside the *public* folder,
-for better security and separation of components.
+4. **Set up database**
+ - Create a new SQL Server database
+ - Run migrations if applicable
+ - Seed initial data if needed
-This means that you should configure your web server to "point" to your project's *public* folder, and
-not to the project root. A better practice would be to configure a virtual host to point there. A poor practice would be to point your web server to the project root and expect to enter *public/...*, as the rest of your logic and the
-framework are exposed.
+5. **Configure web server**
+ Point your web server to the `public` directory:
+ ```apache
+
+ ServerName tinyqc.local
+ DocumentRoot "D:/data/www/tinyqc/public"
+
+ AllowOverride All
+ Require all granted
+
+
+ ```
-**Please** read the user guide for a better explanation of how CI4 works!
+6. **Access the application**
+ Open http://localhost in your browser
-## Repository Management
+## Project Structure
-We use GitHub issues, in our main repository, to track **BUGS** and to track approved **DEVELOPMENT** work packages.
-We use our [forum](http://forum.codeigniter.com) to provide SUPPORT and to discuss
-FEATURE REQUESTS.
+```
+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
+│ │ ├── DictDeptModel.php
+│ │ ├── DictTestModel.php
+│ │ ├── DictControlModel.php
+│ │ ├── ControlModel.php
+│ │ ├── ControlTestModel.php
+│ │ ├── DailyResultModel.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
+├── tests/ # Unit tests
+├── writable/ # Writable directory
+├── env # Environment template
+├── composer.json
+└── phpunit.xml.dist
+```
-This repository is a "distribution" one, built by our release preparation script.
-Problems with it can be raised on our forum, or as issues in the main repository.
+## Usage
-## Server Requirements
+### Dashboard
+The main dashboard provides an overview of quality control status and quick access to all modules.
-PHP version 8.1 or higher is required, with the following extensions installed:
+### Dictionary Management
-- [intl](http://php.net/manual/en/intl.requirements.php)
-- [mbstring](http://php.net/manual/en/mbstring.installation.php)
+- **Departments**: Manage department/category entries
+- **Tests**: Maintain test parameters and specifications
+- **Controls**: Configure control standards and limits
-> [!WARNING]
-> - The end of life date for PHP 7.4 was November 28, 2022.
-> - The end of life date for PHP 8.0 was November 26, 2023.
-> - If you are still using PHP 7.4 or 8.0, you should upgrade immediately.
-> - The end of life date for PHP 8.1 will be December 31, 2025.
+### Data Entry
-Additionally, make sure that the following extensions are enabled in your PHP:
+- **Daily Entry**: Record daily QC test results
+- **Monthly Entry**: Aggregate monthly data and comments
-- json (enabled by default - don't turn it off)
-- [mysqlnd](http://php.net/manual/en/mysqlnd.install.php) if you plan to use MySQL
-- [libcurl](http://php.net/manual/en/curl.requirements.php) if you plan to use the HTTP\CURLRequest library
+### Reports
+
+Generate quality control reports based on:
+- Date ranges
+- Test types
+- Control parameters
+
+## API Endpoints
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | /api/dept | List departments |
+| GET | /api/dept/:id | Get department details |
+| POST | /api/dept | Create department |
+| PUT | /api/dept/:id | Update department |
+| DELETE | /api/dept/:id | Delete department |
+| GET | /api/test | List tests |
+| GET | /api/test/:id | Get test details |
+| POST | /api/test | Create test |
+| PUT | /api/test/:id | Update test |
+| DELETE | /api/test/:id | Delete test |
+| GET | /api/control | List controls |
+| GET | /api/control/:id | Get control details |
+| POST | /api/control | Create control |
+| PUT | /api/control/:id | Update control |
+| DELETE | /api/control/:id | Delete control |
+| 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 |
+
+## Development
+
+### Running Tests
+
+```bash
+# Run all tests
+./vendor/bin/phpunit
+
+# Run with coverage
+./vendor/bin/phpunit --coverage-html coverage/
+```
+
+### Code Style
+
+Follow these guidelines:
+- PSR-12 coding standards
+- CamelCase for variables/functions
+- PascalCase for classes
+- snake_case for database tables/columns
+
+### 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
+
+## Documentation
+
+- [agent1.md](agent1.md) - Complete development guide
+- [VIEWS_RULES.md](VIEWS_RULES.md) - View patterns and conventions
+
+## License
+
+This project is proprietary software.
+
+## Support
+
+For support, please contact the development team.
diff --git a/VIEWS_RULES.md b/VIEWS_RULES.md
deleted file mode 100644
index 36d8feb..0000000
--- a/VIEWS_RULES.md
+++ /dev/null
@@ -1,531 +0,0 @@
-# TinyLab View Rules
-
-This document defines the view patterns and conventions used in TinyLab. Follow these rules when creating new views or modifying existing ones.
-
-## 1. Directory Structure
-
-```
-app/Views/
-├── layout/
-│ ├── main_layout.php # Primary app layout (sidebar, navbar, dark mode)
-│ └── form_layout.php # Lightweight layout for standalone forms
-├── [module_name]/ # Feature module (e.g., patients, requests)
-│ ├── index.php # Main list page
-│ ├── dialog_form.php # Modal form (Create/Edit)
-│ └── drawer_filter.php # Filter drawer (optional)
-├── master/
-│ └── [entity]/ # Master data entity (e.g., doctors, samples)
-│ ├── index.php
-│ └── dialog_form.php
-└── errors/
- └── html/
- ├── error_404.php
- └── error_exception.php
-```
-
-## 2. Layout Extension
-
-All pages must extend the appropriate layout:
-
-```php
-= $this->extend("layout/main_layout"); ?>
-= $this->section("content") ?>
-
-= $this->endSection(); ?>
-= $this->section("script") ?>
-
-= $this->endSection(); ?>
-```
-
-Use `form_layout.php` for standalone pages like login:
-```php
-= $this->extend("layout/form_layout"); ?>
-```
-
-## 3. Alpine.js Integration
-
-### 3.1 Component Definition
-
-Always use `alpine:init` event listener:
-
-```php
-= $this->section("script") ?>
-
-= $this->endSection(); ?>
-```
-
-### 3.2 Alpine Data Attribute
-
-Wrap page content in the component:
-
-```php
-
-
-
-```
-
-## 4. Modal/Dialog Pattern
-
-### 4.1 Standard Modal Structure
-
-```php
-
-
-
-
-
-
-```
-
-## 5. Page Structure Pattern
-
-### 5.1 Standard CRUD Page
-
-```php
-= $this->extend("layout/main_layout"); ?>
-= $this->section("content") ?>
-
-
-
-
-
Module Title
-
Module description
-
-
- New Item
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- #
- Code
- Name
- Actions
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- = $this->include('module/dialog_form'); ?>
-
-= $this->endSection(); ?>
-```
-
-### 5.2 Master-Detail Pattern
-
-```php
-
-
-
-
-
-
-
-
-
-
-
-
Select an item to view details
-
-
-
-
-
-
-
-
-
-
-```
-
-## 6. Form Field Patterns
-
-### 6.1 Input with Icon
-```php
-
-
-
-
-```
-
-### 6.2 Select Dropdown
-```php
-
- -- Select --
-
-
-
-
-```
-
-### 6.3 Checkbox
-```php
-
-
- Checkbox label
-
-```
-
-### 6.4 Radio Group
-```php
-
-
-
-
-
-
-
-
-```
-
-## 7. Status Badges
-
-```php
-
-
-```
-
-## 8. CSS Classes Reference
-
-### Layout
-- `flex`, `flex-col`, `grid`, `grid-cols-2`, `gap-4`
-- `w-full`, `max-w-2xl`, `min-h-0`
-- `overflow-hidden`, `overflow-y-auto`
-
-### Spacing
-- `p-4`, `px-6`, `py-3`, `m-4`, `mb-6`
-- `space-x-4`, `gap-3`
-
-### Typography
-- `text-sm`, `text-xs`, `text-lg`, `text-2xl`
-- `font-medium`, `font-semibold`, `font-bold`
-- `text-slate-500`, `text-slate-800`
-
-### Borders & Backgrounds
-- `bg-white`, `bg-slate-50`, `bg-blue-50`
-- `border`, `border-slate-100`, `border-red-300`
-- `rounded-lg`, `rounded-xl`, `rounded-2xl`
-
-### Components
-- `btn btn-primary`, `btn btn-ghost`
-- `checkbox checkbox-sm`
-- `radio radio-primary`
-- `input`, `select`, `textarea`
-
-### Icons
-FontAwesome 7 via CDN: ` `
-
-## 9. API Response Format
-
-Views expect API responses in this format:
-
-```json
-{
- "status": "success",
- "message": "Operation successful",
- "data": [...]
-}
-```
-
-## 10. Validation Pattern
-
-```javascript
-validate() {
- this.errors = {};
-
- if (!this.form.field1) {
- this.errors.field1 = 'Field 1 is required';
- }
- if (!this.form.field2) {
- this.errors.field2 = 'Field 2 is required';
- }
- if (this.form.field3 && !/^\d+$/.test(this.form.field3)) {
- this.errors.field3 = 'Must be numeric';
- }
-
- return Object.keys(this.errors).length === 0;
-}
-```
-
-## 11. Error Handling
-
-```javascript
-async save() {
- this.loading = true;
- try {
- const res = await fetch(url, { ... });
- const data = await res.json();
- if (data.status === 'success') {
- this.showToast('Saved successfully');
- this.fetchList();
- } else {
- this.error = data.message || 'An error occurred';
- }
- } catch (err) {
- this.error = 'Network error. Please try again.';
- console.error(err);
- } finally {
- this.loading = false;
- }
-}
-```
-
-## 12. Quick Reference: File Templates
-
-### Standard List Page Template
-See: `app/Views/master/doctors/doctors_index.php`
-
-### Modal Form Template
-See: `app/Views/master/doctors/dialog_doctors_form.php`
-
-### Master-Detail Page Template
-See: `app/Views/patients/patients_index.php`
-
-### Result Entry Table Template
-See: `app/Views/requests/dialog_result_entry.php`
-
-## 13. Checklist for New Views
-
-- [ ] Create view file in correct directory
-- [ ] Extend appropriate layout (`main_layout` or `form_layout`)
-- [ ] Define `content` and `script` sections
-- [ ] Wrap content in `x-data` component
-- [ ] Include modal form if needed
-- [ ] Use DaisyUI components for buttons, inputs, selects
-- [ ] Add loading and empty states
-- [ ] Implement proper error handling
-- [ ] Use FontAwesome icons consistently
-- [ ] Follow naming conventions (snake_case for PHP, camelCase for JS)
-
-## 14. Conventions Summary
-
-| Aspect | Convention |
-|--------|------------|
-| File naming | snake_case (e.g., `patients_index.php`) |
-| PHP variables | snake_case |
-| JavaScript variables | camelCase |
-| HTML classes | kebab-case (Tailwind) |
-| Icons | FontAwesome 7 |
-| UI Framework | TailwindCSS + DaisyUI |
-| State management | Alpine.js |
-| API format | JSON with `status`, `message`, `data` |
-| Primary key | `{table}_id` (e.g., `pat_id`) |
-| Soft deletes | All tables use `deleted_at` |
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 96d5e0a..86276b9 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -19,20 +19,20 @@ $routes->group('api', function ($routes) {
$routes->get('dept', 'Api\DeptApiController::index');
$routes->get('dept/(:num)', 'Api\DeptApiController::show/$1');
$routes->post('dept', 'Api\DeptApiController::store');
- $routes->put('dept/(:num)', 'Api\DeptApiController::update/$1');
+ $routes->patch('dept/(:num)', 'Api\DeptApiController::update/$1');
$routes->delete('dept/(:num)', 'Api\DeptApiController::delete/$1');
$routes->get('test', 'Api\TestApiController::index');
$routes->get('test/(:num)', 'Api\TestApiController::show/$1');
$routes->post('test', 'Api\TestApiController::store');
- $routes->put('test/(:num)', 'Api\TestApiController::update/$1');
+ $routes->patch('test/(:num)', 'Api\TestApiController::update/$1');
$routes->delete('test/(:num)', 'Api\TestApiController::delete/$1');
$routes->get('control', 'Api\ControlApiController::index');
$routes->get('control/(:num)', 'Api\ControlApiController::show/$1');
$routes->get('control/(:num)/tests', 'Api\ControlApiController::getTests/$1');
$routes->post('control', 'Api\ControlApiController::store');
- $routes->put('control/(:num)', 'Api\ControlApiController::update/$1');
+ $routes->patch('control/(:num)', 'Api\ControlApiController::update/$1');
$routes->delete('control/(:num)', 'Api\ControlApiController::delete/$1');
$routes->get('entry/controls', 'Api\EntryApiController::getControls');
diff --git a/app/Controllers/Api/ControlApiController.php b/app/Controllers/Api/ControlApiController.php
index 09b5de7..89389bb 100644
--- a/app/Controllers/Api/ControlApiController.php
+++ b/app/Controllers/Api/ControlApiController.php
@@ -10,142 +10,137 @@ class ControlApiController extends BaseController
{
protected $dictControlModel;
protected $controlTestModel;
+ protected $rules;
public function __construct()
{
$this->dictControlModel = new DictControlModel();
$this->controlTestModel = new ControlTestModel();
+ $this->rules = [
+ 'name' => 'required|min_length[1]',
+ 'dept_ref_id' => 'required',
+ ];
}
public function index()
{
- $controls = $this->dictControlModel->getWithDept();
- return $this->response->setJSON([
- 'status' => 'success',
- 'message' => 'Controls fetched successfully',
- 'data' => $controls
- ]);
- }
-
- public function show($id)
- {
- $control = $this->dictControlModel->find($id);
- if (!$control) {
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Control not found'
- ])->setStatusCode(404);
- }
- return $this->response->setJSON([
- 'status' => 'success',
- 'data' => $control
- ]);
- }
-
- public function getTests($id)
- {
- $tests = $this->controlTestModel->where('control_ref_id', $id)->findAll();
- $testIds = array_column($tests, 'test_ref_id');
-
- return $this->response->setJSON([
- 'status' => 'success',
- 'data' => $testIds
- ]);
- }
-
- public function store()
- {
- $post = $this->request->getJSON(true);
-
- $controlData = [
- 'dept_ref_id' => $post['dept_ref_id'] ?? null,
- 'name' => $post['name'] ?? '',
- 'lot' => $post['lot'] ?? '',
- 'producer' => $post['producer'] ?? '',
- 'expdate' => $post['expdate'] ?? null,
- ];
-
- $controlId = $this->dictControlModel->insert($controlData, true);
-
- if (!empty($post['test_ids'])) {
- foreach ($post['test_ids'] as $testId) {
- $this->controlTestModel->insert([
- 'control_ref_id' => $controlId,
- 'test_ref_id' => $testId,
- 'mean' => 0,
- 'sd' => 0,
- ]);
- }
- }
-
- if ($controlId) {
- return $this->response->setJSON([
+ try {
+ $rows = $this->dictControlModel->getWithDept();
+ return $this->respond([
'status' => 'success',
- 'message' => 'Control saved successfully',
- 'data' => ['control_id' => $controlId]
- ]);
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Exception: ' . $e->getMessage());
}
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to save control'
- ])->setStatusCode(500);
}
- public function update($id)
+ public function show($id = null)
{
- $post = $this->request->getJSON(true);
-
- $controlData = [
- 'dept_ref_id' => $post['dept_ref_id'] ?? null,
- 'name' => $post['name'] ?? '',
- 'lot' => $post['lot'] ?? '',
- 'producer' => $post['producer'] ?? '',
- 'expdate' => $post['expdate'] ?? null,
- ];
+ try {
+ $rows = $this->dictControlModel->where('control_id', $id)->findAll();
+ if (empty($rows)) {
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'data not found.'
+ ], 200);
+ }
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
- $success = $this->dictControlModel->update($id, $controlData);
+ public function getTests($id = null)
+ {
+ try {
+ $rows = $this->controlTestModel->where('control_ref_id', $id)->findAll();
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
- if (!empty($post['test_ids'])) {
+ public function create()
+ {
+ $input = $this->request->getJSON(true);
+ if (!$this->validate($this->rules)) {
+ return $this->failValidationErrors($this->validator->getErrors());
+ }
+ try {
+ $controlId = $this->dictControlModel->insert($input, true);
+
+ if (!empty($input['test_ids'])) {
+ foreach ($input['test_ids'] as $testId) {
+ $this->controlTestModel->insert([
+ 'control_ref_id' => $controlId,
+ 'test_ref_id' => $testId,
+ 'mean' => 0,
+ 'sd' => 0,
+ ]);
+ }
+ }
+
+ return $this->respondCreated([
+ 'status' => 'success',
+ 'message' => $controlId
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
+
+ public function update($id = null)
+ {
+ $input = $this->request->getJSON(true);
+ if (!$this->validate($this->rules)) {
+ return $this->failValidationErrors($this->validator->getErrors());
+ }
+ try {
+ $this->dictControlModel->update($id, $input);
+
+ if (!empty($input['test_ids'])) {
+ $this->controlTestModel->where('control_ref_id', $id)->delete();
+ foreach ($input['test_ids'] as $testId) {
+ $this->controlTestModel->insert([
+ 'control_ref_id' => $id,
+ 'test_ref_id' => $testId,
+ 'mean' => 0,
+ 'sd' => 0,
+ ]);
+ }
+ }
+
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'update success',
+ 'data' => $id
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
+
+ public function delete($id = null)
+ {
+ try {
$this->controlTestModel->where('control_ref_id', $id)->delete();
- foreach ($post['test_ids'] as $testId) {
- $this->controlTestModel->insert([
- 'control_ref_id' => $id,
- 'test_ref_id' => $testId,
- 'mean' => 0,
- 'sd' => 0,
- ]);
- }
- }
-
- if ($success) {
- return $this->response->setJSON([
+ $this->dictControlModel->delete($id);
+ return $this->respond([
'status' => 'success',
- 'message' => 'Control updated successfully'
+ 'message' => 'delete success'
]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to update control'
- ])->setStatusCode(500);
- }
-
- public function delete($id)
- {
- $this->controlTestModel->where('control_ref_id', $id)->delete();
- $success = $this->dictControlModel->delete($id);
-
- if ($success) {
- return $this->response->setJSON([
- 'status' => 'success',
- 'message' => 'Control deleted successfully'
- ]);
- }
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to delete control'
- ])->setStatusCode(500);
}
}
diff --git a/app/Controllers/Api/DeptApiController.php b/app/Controllers/Api/DeptApiController.php
index ffdfd6c..a7c701d 100644
--- a/app/Controllers/Api/DeptApiController.php
+++ b/app/Controllers/Api/DeptApiController.php
@@ -8,98 +8,95 @@ use App\Models\DictDeptModel;
class DeptApiController extends BaseController
{
protected $dictDeptModel;
+ protected $rules;
public function __construct()
{
$this->dictDeptModel = new DictDeptModel();
+ $this->rules = [
+ 'name' => 'required|min_length[1]',
+ ];
}
public function index()
{
- $depts = $this->dictDeptModel->findAll();
- return $this->response->setJSON([
- 'status' => 'success',
- 'message' => 'Departments fetched successfully',
- 'data' => $depts
- ]);
- }
-
- public function show($id)
- {
- $dept = $this->dictDeptModel->find($id);
- if (!$dept) {
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Department not found'
- ])->setStatusCode(404);
- }
- return $this->response->setJSON([
- 'status' => 'success',
- 'data' => $dept
- ]);
- }
-
- public function store()
- {
- $post = $this->request->getJSON(true);
-
- $data = [
- 'name' => $post['name'] ?? '',
- ];
-
- $id = $this->dictDeptModel->insert($data);
-
- if ($id) {
- return $this->response->setJSON([
+ try {
+ $rows = $this->dictDeptModel->findAll();
+ return $this->respond([
'status' => 'success',
- 'message' => 'Department saved successfully',
- 'data' => ['dept_id' => $id]
- ]);
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Exception: ' . $e->getMessage());
}
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to save department'
- ])->setStatusCode(500);
}
- public function update($id)
+ public function show($id = null)
{
- $post = $this->request->getJSON(true);
-
- $data = [
- 'name' => $post['name'] ?? '',
- ];
-
- $success = $this->dictDeptModel->update($id, $data);
-
- if ($success) {
- return $this->response->setJSON([
+ try {
+ $rows = $this->dictDeptModel->where('dept_id', $id)->findAll();
+ if (empty($rows)) {
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'data not found.'
+ ], 200);
+ }
+ return $this->respond([
'status' => 'success',
- 'message' => 'Department updated successfully'
- ]);
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to update department'
- ])->setStatusCode(500);
}
- public function delete($id)
+ public function create()
{
- $success = $this->dictDeptModel->delete($id);
-
- if ($success) {
- return $this->response->setJSON([
- 'status' => 'success',
- 'message' => 'Department deleted successfully'
- ]);
+ $input = $this->request->getJSON(true);
+ if (!$this->validate($this->rules)) {
+ return $this->failValidationErrors($this->validator->getErrors());
}
+ try {
+ $id = $this->dictDeptModel->insert($input, true);
+ return $this->respondCreated([
+ 'status' => 'success',
+ 'message' => $id
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to delete department'
- ])->setStatusCode(500);
+ public function update($id = null)
+ {
+ $input = $this->request->getJSON(true);
+ if (!$this->validate($this->rules)) {
+ return $this->failValidationErrors($this->validator->getErrors());
+ }
+ try {
+ $this->dictDeptModel->update($id, $input);
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'update success',
+ 'data' => $id
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
+
+ public function delete($id = null)
+ {
+ try {
+ $this->dictDeptModel->delete($id);
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'delete success'
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
}
}
diff --git a/app/Controllers/Api/EntryApiController.php b/app/Controllers/Api/EntryApiController.php
index 7805883..db0906a 100644
--- a/app/Controllers/Api/EntryApiController.php
+++ b/app/Controllers/Api/EntryApiController.php
@@ -25,119 +25,111 @@ class EntryApiController extends BaseController
public function getControls()
{
- $date = $this->request->getGet('date');
- $deptid = $this->request->getGet('deptid');
+ try {
+ $date = $this->request->getGet('date');
+ $deptId = $this->request->getGet('deptId');
- $controls = $this->dictControlModel->getActiveByDate($date, $deptid);
-
- return $this->response->setJSON([
- 'status' => 'success',
- 'message' => 'Controls fetched successfully',
- 'data' => $controls
- ]);
+ $rows = $this->dictControlModel->getActiveByDate($date, $deptId);
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
}
public function getTests()
{
- $controlid = $this->request->getGet('controlid');
+ try {
+ $controlId = $this->request->getGet('controlId');
- $tests = $this->controlTestModel->getByControl($controlid);
-
- return $this->response->setJSON([
- 'status' => 'success',
- 'message' => 'Tests fetched successfully',
- 'data' => $tests
- ]);
+ $rows = $this->controlTestModel->getByControl($controlId);
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
}
public function saveDaily()
{
- $post = $this->request->getPost();
+ $input = $this->request->getJSON(true);
+
+ try {
+ $resultData = [
+ 'control_ref_id' => $input['controlId'] ?? 0,
+ 'test_ref_id' => $input['testId'] ?? 0,
+ 'resdate' => $input['resdate'] ?? date('Y-m-d'),
+ 'resvalue' => $input['resvalue'] ?? '',
+ 'rescomment' => $input['rescomment'] ?? '',
+ ];
- $resultData = [
- 'control_ref_id' => $post['controlid'] ?? 0,
- 'test_ref_id' => $post['testid'] ?? 0,
- 'resdate' => $post['resdate'] ?? date('Y-m-d'),
- 'resvalue' => $post['resvalue'] ?? '',
- 'rescomment' => $post['rescomment'] ?? '',
- ];
-
- $success = $this->resultModel->saveResult($resultData);
-
- if ($success) {
- return $this->response->setJSON([
+ $this->resultModel->saveResult($resultData);
+ return $this->respond([
'status' => 'success',
- 'message' => 'Result saved successfully'
+ 'message' => 'save success'
]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to save result'
- ])->setStatusCode(500);
}
public function saveMonthly()
{
- $post = $this->request->getPost();
+ $input = $this->request->getJSON(true);
+
+ try {
+ $controlId = $input['controlId'] ?? 0;
+ $testId = $input['testId'] ?? 0;
+ $dates = $input['dates'] ?? '';
+ $resvalues = $input['resvalue'] ?? [];
- $controlid = $post['controlid'] ?? 0;
- $testid = $post['testid'] ?? 0;
- $dates = $post['dates'] ?? '';
- $resvalues = $post['resvalue'] ?? [];
-
- $success = true;
- foreach ($resvalues as $day => $value) {
- if (!empty($value)) {
- $resultData = [
- 'control_ref_id' => $controlid,
- 'test_ref_id' => $testid,
- 'resdate' => $dates . '-' . str_pad($day, 2, '0', STR_PAD_LEFT),
- 'resvalue' => $value,
- 'rescomment' => '',
- ];
- if (!$this->resultModel->saveResult($resultData)) {
- $success = false;
+ foreach ($resvalues as $day => $value) {
+ if (!empty($value)) {
+ $resultData = [
+ 'control_ref_id' => $controlId,
+ 'test_ref_id' => $testId,
+ 'resdate' => $dates . '-' . str_pad($day, 2, '0', STR_PAD_LEFT),
+ 'resvalue' => $value,
+ 'rescomment' => '',
+ ];
+ $this->resultModel->saveResult($resultData);
}
}
- }
- if ($success) {
- return $this->response->setJSON([
+ return $this->respond([
'status' => 'success',
- 'message' => 'Monthly data saved successfully'
+ 'message' => 'save success'
]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to save some entries'
- ])->setStatusCode(500);
}
public function saveComment()
{
- $post = $this->request->getPost();
+ $input = $this->request->getJSON(true);
+
+ try {
+ $commentData = [
+ 'control_ref_id' => $input['controlId'] ?? 0,
+ 'test_ref_id' => $input['testId'] ?? 0,
+ 'commonth' => $input['commonth'] ?? '',
+ 'comtext' => $input['comtext'] ?? '',
+ ];
- $commentData = [
- 'control_ref_id' => $post['controlid'] ?? 0,
- 'test_ref_id' => $post['testid'] ?? 0,
- 'commonth' => $post['commonth'] ?? '',
- 'comtext' => $post['comtext'] ?? '',
- ];
-
- $success = $this->commentModel->saveComment($commentData);
-
- if ($success) {
- return $this->response->setJSON([
+ $this->commentModel->saveComment($commentData);
+ return $this->respond([
'status' => 'success',
- 'message' => 'Comment saved successfully'
+ 'message' => 'save success'
]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to save comment'
- ])->setStatusCode(500);
}
}
diff --git a/app/Controllers/Api/TestApiController.php b/app/Controllers/Api/TestApiController.php
index 958fff8..37f0cb1 100644
--- a/app/Controllers/Api/TestApiController.php
+++ b/app/Controllers/Api/TestApiController.php
@@ -10,111 +10,97 @@ class TestApiController extends BaseController
{
protected $dictTestModel;
protected $dictDeptModel;
+ protected $rules;
public function __construct()
{
$this->dictTestModel = new DictTestModel();
$this->dictDeptModel = new DictDeptModel();
+ $this->rules = [
+ 'name' => 'required|min_length[1]',
+ 'dept_ref_id' => 'required',
+ ];
}
public function index()
{
- $tests = $this->dictTestModel->getWithDept();
- return $this->response->setJSON([
- 'status' => 'success',
- 'message' => 'Tests fetched successfully',
- 'data' => $tests
- ]);
- }
-
- public function show($id)
- {
- $test = $this->dictTestModel->find($id);
- if (!$test) {
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Test not found'
- ])->setStatusCode(404);
- }
- return $this->response->setJSON([
- 'status' => 'success',
- 'data' => $test
- ]);
- }
-
- public function store()
- {
- $post = $this->request->getJSON(true);
-
- $testData = [
- 'dept_ref_id' => $post['dept_ref_id'] ?? null,
- 'name' => $post['name'] ?? '',
- 'unit' => $post['unit'] ?? '',
- 'method' => $post['method'] ?? '',
- 'cva' => $post['cva'] ?? '',
- 'ba' => $post['ba'] ?? '',
- 'tea' => $post['tea'] ?? '',
- ];
-
- $id = $this->dictTestModel->insert($testData);
-
- if ($id) {
- return $this->response->setJSON([
+ try {
+ $rows = $this->dictTestModel->getWithDept();
+ return $this->respond([
'status' => 'success',
- 'message' => 'Test saved successfully',
- 'data' => ['test_id' => $id]
- ]);
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Exception: ' . $e->getMessage());
}
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to save test'
- ])->setStatusCode(500);
}
- public function update($id)
+ public function show($id = null)
{
- $post = $this->request->getJSON(true);
-
- $testData = [
- 'dept_ref_id' => $post['dept_ref_id'] ?? null,
- 'name' => $post['name'] ?? '',
- 'unit' => $post['unit'] ?? '',
- 'method' => $post['method'] ?? '',
- 'cva' => $post['cva'] ?? '',
- 'ba' => $post['ba'] ?? '',
- 'tea' => $post['tea'] ?? '',
- ];
-
- $success = $this->dictTestModel->update($id, $testData);
-
- if ($success) {
- return $this->response->setJSON([
+ try {
+ $rows = $this->dictTestModel->where('test_id', $id)->findAll();
+ if (empty($rows)) {
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'data not found.'
+ ], 200);
+ }
+ return $this->respond([
'status' => 'success',
- 'message' => 'Test updated successfully'
- ]);
+ 'message' => 'fetch success',
+ 'data' => $rows
+ ], 200);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
}
-
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to update test'
- ])->setStatusCode(500);
}
- public function delete($id)
+ public function create()
{
- $success = $this->dictTestModel->delete($id);
-
- if ($success) {
- return $this->response->setJSON([
- 'status' => 'success',
- 'message' => 'Test deleted successfully'
- ]);
+ $input = $this->request->getJSON(true);
+ if (!$this->validate($this->rules)) {
+ return $this->failValidationErrors($this->validator->getErrors());
}
+ try {
+ $id = $this->dictTestModel->insert($input, true);
+ return $this->respondCreated([
+ 'status' => 'success',
+ 'message' => $id
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
- return $this->response->setJSON([
- 'status' => 'error',
- 'message' => 'Failed to delete test'
- ])->setStatusCode(500);
+ public function update($id = null)
+ {
+ $input = $this->request->getJSON(true);
+ if (!$this->validate($this->rules)) {
+ return $this->failValidationErrors($this->validator->getErrors());
+ }
+ try {
+ $this->dictTestModel->update($id, $input);
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'update success',
+ 'data' => $id
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
+ }
+
+ public function delete($id = null)
+ {
+ try {
+ $this->dictTestModel->delete($id);
+ return $this->respond([
+ 'status' => 'success',
+ 'message' => 'delete success'
+ ]);
+ } catch (\Exception $e) {
+ return $this->failServerError('Something went wrong: ' . $e->getMessage());
+ }
}
}
diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php
index 744f77f..343b084 100644
--- a/app/Controllers/BaseController.php
+++ b/app/Controllers/BaseController.php
@@ -3,18 +3,21 @@
namespace App\Controllers;
use CodeIgniter\Controller;
+use CodeIgniter\API\ResponseTrait;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
abstract class BaseController extends Controller
{
+ use ResponseTrait;
+
protected $session;
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
parent::initController($request, $response, $logger);
$this->session = \Config\Services::session();
- $this->helpers = ['form', 'url'];
+ $this->helpers = ['form', 'url', 'json'];
}
}
diff --git a/app/Database/Migrations/2026-01-14-000001_CreateCmodQcTables.php b/app/Database/Migrations/2026-01-14-000001_CreateCmodQcTables.php
index 0373742..ae2c90a 100644
--- a/app/Database/Migrations/2026-01-14-000001_CreateCmodQcTables.php
+++ b/app/Database/Migrations/2026-01-14-000001_CreateCmodQcTables.php
@@ -33,6 +33,18 @@ class CreateCmodQcTables extends Migration
'type' => 'FLOAT',
'null' => true,
],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'updated_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'deleted_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
]);
$this->forge->addKey('control_test_id', true);
$this->forge->createTable('control_tests');
@@ -67,6 +79,18 @@ class CreateCmodQcTables extends Migration
'type' => 'TEXT',
'null' => true,
],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'updated_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'deleted_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
]);
$this->forge->addKey('result_id', true);
$this->forge->createTable('results');
@@ -101,6 +125,18 @@ class CreateCmodQcTables extends Migration
'type' => 'DATE',
'null' => true,
],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'updated_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'deleted_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
]);
$this->forge->addKey('control_id', true);
$this->forge->createTable('dict_controls');
@@ -117,6 +153,18 @@ class CreateCmodQcTables extends Migration
'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');
@@ -163,6 +211,18 @@ class CreateCmodQcTables extends Migration
'constraint' => 50,
'null' => true,
],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'updated_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'deleted_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
]);
$this->forge->addKey('test_id', true);
$this->forge->createTable('dict_tests');
@@ -193,6 +253,18 @@ class CreateCmodQcTables extends Migration
'type' => 'TEXT',
'null' => true,
],
+ 'created_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'updated_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
+ 'deleted_at' => [
+ 'type' => 'DATETIME',
+ 'null' => true,
+ ],
]);
$this->forge->addKey('result_comment_id', true);
$this->forge->createTable('result_comments');
diff --git a/app/Helpers/stringcase_helper.php b/app/Helpers/stringcase_helper.php
new file mode 100644
index 0000000..2f80e37
--- /dev/null
+++ b/app/Helpers/stringcase_helper.php
@@ -0,0 +1,47 @@
+ $value) {
+ $snakeKey = camel_to_snake($key);
+ $converted[$snakeKey] = $value;
+ }
+ return $converted;
+ }
+}
+
+if (!function_exists('snake_to_camel')) {
+ function snake_to_camel(string $string): string
+ {
+ $parts = explode('_', $string);
+ $camel = array_map(function ($part, $index) {
+ if ($index === 0) {
+ return $part;
+ }
+ return ucfirst($part);
+ }, $parts, array_keys($parts));
+ return implode('', $camel);
+ }
+}
+
+if (!function_exists('snake_to_camel_array')) {
+ function snake_to_camel_array(array $data): array
+ {
+ $converted = [];
+ foreach ($data as $key => $value) {
+ $camelKey = snake_to_camel($key);
+ $converted[$camelKey] = $value;
+ }
+ return $converted;
+ }
+}
diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php
new file mode 100644
index 0000000..c1992e7
--- /dev/null
+++ b/app/Models/BaseModel.php
@@ -0,0 +1,113 @@
+returnType)) {
+ $this->returnType = 'array';
+ }
+ }
+
+ public function findAll(?int $limit = null, int $offset = 0)
+ {
+ $rows = parent::findAll($limit, $offset);
+ return $this->snakeToCamelRecursive($rows);
+ }
+
+ public function find($id = null)
+ {
+ $row = parent::find($id);
+ if ($row) {
+ return $this->snakeToCamel($row);
+ }
+ return null;
+ }
+
+ public function first()
+ {
+ $row = parent::first();
+ if ($row) {
+ return $this->snakeToCamel($row);
+ }
+ return null;
+ }
+
+ protected function snakeToCamel(array $row): array
+ {
+ $converted = [];
+ foreach ($row as $key => $value) {
+ $camelKey = $this->snakeToCamelKey($key);
+ $converted[$camelKey] = $value;
+ }
+ return $converted;
+ }
+
+ protected function snakeToCamelRecursive($data)
+ {
+ if (is_array($data)) {
+ if (isset($data[0]) || empty($data)) {
+ return array_map([$this, 'snakeToCamelRecursive'], $data);
+ }
+ return $this->snakeToCamel($data);
+ }
+ return $data;
+ }
+
+ protected function snakeToCamelKey(string $key): string
+ {
+ $parts = explode('_', $key);
+ $camel = array_map(function ($part, $index) {
+ if ($index === 0) {
+ return $part;
+ }
+ return ucfirst($part);
+ }, $parts, array_keys($parts));
+ return implode('', $camel);
+ }
+
+ public function insert($data = null, bool $returnID = true)
+ {
+ $snakeData = $this->camelToSnakeArray($data);
+ return parent::insert($snakeData, $returnID);
+ }
+
+ public function update($id = null, $row = null): bool
+ {
+ $snakeData = $this->camelToSnakeArray($row);
+ return parent::update($id, $snakeData);
+ }
+
+ protected function camelToSnakeArray(array $data): array
+ {
+ $converted = [];
+ foreach ($data as $key => $value) {
+ $snakeKey = $this->camelToSnakeKey($key);
+ $converted[$snakeKey] = $value;
+ }
+ return $converted;
+ }
+
+ protected function camelToSnakeKey(string $key): string
+ {
+ $pattern = '/([a-z])([A-Z])/';
+ $snake = preg_replace($pattern, '$1_$2', $key);
+ return strtolower($snake);
+ }
+}
diff --git a/app/Models/ControlModel.php b/app/Models/ControlModel.php
index a8dd435..3aec9bd 100644
--- a/app/Models/ControlModel.php
+++ b/app/Models/ControlModel.php
@@ -2,38 +2,38 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class ControlModel extends Model
+class ControlModel extends BaseModel
{
- protected $table = 'dict_control';
- protected $primaryKey = 'id';
+ protected $table = 'dict_controls';
+ protected $primaryKey = 'control_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['deptid', 'name', 'lot', 'producer', 'expdate'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['dept_ref_id', 'name', 'lot', 'producer', 'expdate'];
- public function getByDept($deptid)
+ public function getByDept($deptId)
{
- return $this->where('deptid', $deptid)->findAll();
+ return $this->where('dept_ref_id', $deptId)->findAll();
}
public function getWithDept()
{
- $builder = $this->db->table('dict_control c');
+ $builder = $this->db->table('dict_controls c');
$builder->select('c.*, d.name as dept_name');
- $builder->join('dict_dept d', 'd.id = c.deptid', 'left');
+ $builder->join('dict_depts d', 'd.dept_id = c.dept_ref_id', 'left');
return $builder->get()->getResultArray();
}
- public function getActiveByDate($date, $deptid = null)
+ public function getActiveByDate($date, $deptId = null)
{
- $builder = $this->db->table('dict_control c');
+ $builder = $this->db->table('dict_controls c');
$builder->select('c.*');
$builder->where('c.expdate >=', $date);
- if ($deptid) {
- $builder->where('c.deptid', $deptid);
+ if ($deptId) {
+ $builder->where('c.dept_ref_id', $deptId);
}
$builder->orderBy('c.name', 'ASC');
return $builder->get()->getResultArray();
diff --git a/app/Models/ControlTestModel.php b/app/Models/ControlTestModel.php
index f7a8283..7ba7530 100644
--- a/app/Models/ControlTestModel.php
+++ b/app/Models/ControlTestModel.php
@@ -2,17 +2,17 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class ControlTestModel extends Model
+class ControlTestModel extends BaseModel
{
protected $table = 'control_tests';
protected $primaryKey = 'control_test_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['control_test_id', 'control_ref_id', 'test_ref_id', 'mean', 'sd'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['control_ref_id', 'test_ref_id', 'mean', 'sd'];
public function getByControl($controlId)
{
diff --git a/app/Models/DailyResultModel.php b/app/Models/DailyResultModel.php
index ad9227f..323634e 100644
--- a/app/Models/DailyResultModel.php
+++ b/app/Models/DailyResultModel.php
@@ -2,41 +2,41 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class DailyResultModel extends Model
+class DailyResultModel extends BaseModel
{
protected $table = 'daily_result';
- protected $primaryKey = 'id';
+ protected $primaryKey = 'daily_result_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['controlid', 'testid', 'resdate', 'resvalue', 'rescomment'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['control_ref_id', 'test_ref_id', 'resdate', 'resvalue', 'rescomment'];
- public function getByMonth($controlid, $testid, $yearMonth)
+ public function getByMonth($controlId, $testId, $yearMonth)
{
$startDate = $yearMonth . '-01';
$endDate = $yearMonth . '-31';
$builder = $this->db->table('daily_result');
$builder->select('*');
- $builder->where('controlid', $controlid);
- $builder->where('testid', $testid);
+ $builder->where('control_ref_id', $controlId);
+ $builder->where('test_ref_id', $testId);
$builder->where('resdate >=', $startDate);
$builder->where('resdate <=', $endDate);
$builder->orderBy('resdate', 'ASC');
return $builder->get()->getResultArray();
}
- public function getByControlMonth($controlid, $yearMonth)
+ public function getByControlMonth($controlId, $yearMonth)
{
$startDate = $yearMonth . '-01';
$endDate = $yearMonth . '-31';
$builder = $this->db->table('daily_result');
$builder->select('*');
- $builder->where('controlid', $controlid);
+ $builder->where('control_ref_id', $controlId);
$builder->where('resdate >=', $startDate);
$builder->where('resdate <=', $endDate);
return $builder->get()->getResultArray();
@@ -46,14 +46,14 @@ class DailyResultModel extends Model
{
$builder = $this->db->table('daily_result');
$existing = $builder->select('*')
- ->where('controlid', $data['controlid'])
- ->where('testid', $data['testid'])
+ ->where('control_ref_id', $data['control_ref_id'])
+ ->where('test_ref_id', $data['test_ref_id'])
->where('resdate', $data['resdate'])
->get()
->getRowArray();
if ($existing) {
- return $builder->where('id', $existing['id'])->update($data);
+ return $builder->where('daily_result_id', $existing['daily_result_id'])->update($data);
} else {
return $builder->insert($data);
}
diff --git a/app/Models/DeptModel.php b/app/Models/DeptModel.php
index 3dca9d9..02351b0 100644
--- a/app/Models/DeptModel.php
+++ b/app/Models/DeptModel.php
@@ -2,15 +2,15 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class DeptModel extends Model
+class DeptModel extends BaseModel
{
- protected $table = 'dict_dept';
- protected $primaryKey = 'id';
+ protected $table = 'dict_depts';
+ protected $primaryKey = 'dept_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
protected $allowedFields = ['name'];
- protected $useTimestamps = false;
}
diff --git a/app/Models/DictControlModel.php b/app/Models/DictControlModel.php
index 1942ed1..ac56d3d 100644
--- a/app/Models/DictControlModel.php
+++ b/app/Models/DictControlModel.php
@@ -2,17 +2,17 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class DictControlModel extends Model
+class DictControlModel extends BaseModel
{
protected $table = 'dict_controls';
protected $primaryKey = 'control_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['control_id', 'dept_ref_id', 'name', 'lot', 'producer', 'expdate'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['dept_ref_id', 'name', 'lot', 'producer', 'expdate'];
public function getByDept($deptId)
{
diff --git a/app/Models/DictDeptModel.php b/app/Models/DictDeptModel.php
index 81e6bdb..d5bd067 100644
--- a/app/Models/DictDeptModel.php
+++ b/app/Models/DictDeptModel.php
@@ -2,15 +2,15 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class DictDeptModel extends Model
+class DictDeptModel extends BaseModel
{
protected $table = 'dict_depts';
protected $primaryKey = 'dept_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['dept_id', 'name'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['name'];
}
diff --git a/app/Models/DictTestModel.php b/app/Models/DictTestModel.php
index 81fe35e..70bf1e5 100644
--- a/app/Models/DictTestModel.php
+++ b/app/Models/DictTestModel.php
@@ -2,17 +2,17 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class DictTestModel extends Model
+class DictTestModel extends BaseModel
{
protected $table = 'dict_tests';
protected $primaryKey = 'test_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['test_id', 'dept_ref_id', 'name', 'unit', 'method', 'cva', 'ba', 'tea'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['dept_ref_id', 'name', 'unit', 'method', 'cva', 'ba', 'tea'];
public function getByDept($deptId)
{
diff --git a/app/Models/MonthlyCommentModel.php b/app/Models/MonthlyCommentModel.php
index bac3da6..cf8c2b1 100644
--- a/app/Models/MonthlyCommentModel.php
+++ b/app/Models/MonthlyCommentModel.php
@@ -2,35 +2,35 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class MonthlyCommentModel extends Model
+class MonthlyCommentModel extends BaseModel
{
protected $table = 'monthly_comment';
- protected $primaryKey = 'id';
+ protected $primaryKey = 'monthly_comment_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['controlid', 'testid', 'commonth', 'comtext'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['control_ref_id', 'test_ref_id', 'commonth', 'comtext'];
- public function getByControlTestMonth($controlid, $testid, $yearMonth)
+ public function getByControlTestMonth($controlId, $testId, $yearMonth)
{
- return $this->where('controlid', $controlid)
- ->where('testid', $testid)
+ return $this->where('control_ref_id', $controlId)
+ ->where('test_ref_id', $testId)
->where('commonth', $yearMonth)
->first();
}
public function saveComment($data)
{
- $existing = $this->where('controlid', $data['controlid'])
- ->where('testid', $data['testid'])
+ $existing = $this->where('control_ref_id', $data['control_ref_id'])
+ ->where('test_ref_id', $data['test_ref_id'])
->where('commonth', $data['commonth'])
->first();
if ($existing) {
- return $this->update($existing['id'], $data);
+ return $this->update($existing['monthly_comment_id'], $data);
} else {
return $this->insert($data);
}
diff --git a/app/Models/ResultCommentModel.php b/app/Models/ResultCommentModel.php
index adf1012..d4086be 100644
--- a/app/Models/ResultCommentModel.php
+++ b/app/Models/ResultCommentModel.php
@@ -2,17 +2,17 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class ResultCommentModel extends Model
+class ResultCommentModel extends BaseModel
{
protected $table = 'result_comments';
protected $primaryKey = 'result_comment_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['result_comment_id', 'control_ref_id', 'test_ref_id', 'commonth', 'comtext'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['control_ref_id', 'test_ref_id', 'commonth', 'comtext'];
public function getByControlTestMonth($controlId, $testId, $yearMonth)
{
diff --git a/app/Models/ResultModel.php b/app/Models/ResultModel.php
index cad6bf2..9cc6a7e 100644
--- a/app/Models/ResultModel.php
+++ b/app/Models/ResultModel.php
@@ -2,17 +2,17 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class ResultModel extends Model
+class ResultModel extends BaseModel
{
protected $table = 'results';
protected $primaryKey = 'result_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['result_id', 'control_ref_id', 'test_ref_id', 'resdate', 'resvalue', 'rescomment'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['control_ref_id', 'test_ref_id', 'resdate', 'resvalue', 'rescomment'];
public function getByMonth($controlId, $testId, $yearMonth)
{
diff --git a/app/Models/TestModel.php b/app/Models/TestModel.php
index 1497a2d..2897ab7 100644
--- a/app/Models/TestModel.php
+++ b/app/Models/TestModel.php
@@ -2,28 +2,28 @@
namespace App\Models;
-use CodeIgniter\Model;
+use App\Models\BaseModel;
-class TestModel extends Model
+class TestModel extends BaseModel
{
- protected $table = 'dict_test';
- protected $primaryKey = 'id';
+ protected $table = 'dict_tests';
+ protected $primaryKey = 'test_id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
- protected $useSoftDeletes = false;
- protected $allowedFields = ['deptid', 'name', 'unit', 'method', 'cva', 'ba', 'tea'];
- protected $useTimestamps = false;
+ protected $useSoftDeletes = true;
+ protected $useTimestamps = true;
+ protected $allowedFields = ['dept_ref_id', 'name', 'unit', 'method', 'cva', 'ba', 'tea'];
- public function getByDept($deptid)
+ public function getByDept($deptId)
{
- return $this->where('deptid', $deptid)->findAll();
+ return $this->where('dept_ref_id', $deptId)->findAll();
}
public function getWithDept()
{
- $builder = $this->db->table('dict_test t');
+ $builder = $this->db->table('dict_tests t');
$builder->select('t.*, d.name as dept_name');
- $builder->join('dict_dept d', 'd.id = t.deptid', 'left');
+ $builder->join('dict_depts d', 'd.dept_id = t.dept_ref_id', 'left');
return $builder->get()->getResultArray();
}
}
diff --git a/app/Views/control/index.php b/app/Views/control/index.php
index 6c39ad5..4ccc8f4 100644
--- a/app/Views/control/index.php
+++ b/app/Views/control/index.php
@@ -197,7 +197,7 @@ document.addEventListener('alpine:init', () => {
: `${window.BASEURL}/api/control`;
const res = await fetch(url, {
- method: this.form.control_id ? 'PUT' : 'POST',
+ method: this.form.control_id ? 'PATCH' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
});
diff --git a/app/Views/dept/index.php b/app/Views/dept/index.php
index 7628405..54c9f27 100644
--- a/app/Views/dept/index.php
+++ b/app/Views/dept/index.php
@@ -142,7 +142,7 @@ document.addEventListener('alpine:init', () => {
: `${window.BASEURL}/api/dept`;
const res = await fetch(url, {
- method: this.form.dept_id ? 'PUT' : 'POST',
+ method: this.form.dept_id ? 'PATCH' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: this.form.name })
});
diff --git a/app/Views/layout/main_layout.php b/app/Views/layout/main_layout.php
index c428ef0..4f5a0eb 100644
--- a/app/Views/layout/main_layout.php
+++ b/app/Views/layout/main_layout.php
@@ -94,7 +94,7 @@
-
+