diff --git a/.gitignore b/.gitignore index 1c59c9e..d952fae 100644 --- a/.gitignore +++ b/.gitignore @@ -126,5 +126,5 @@ _modules/* /phpunit*.xml .claude/ -_bmad/ -_bmad-output/ \ No newline at end of file +.serena/ +CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 6ccfb87..f6190a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,597 +1,169 @@ -# AGENTS.md - AI Agent Guidelines for [PROJECT NAME] +# AGENTS.md - AI Agent Guidelines for TinyQC -## AI Agent Guidelines +## Important: Always Use Serena Tools -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. +**When editing this codebase, prefer Serena tools over regular file operations:** +- Use `find_symbol` to locate code rather than grep/glob when possible +- Use `replace_symbol_body` for entire method/class changes +- Use `replace_content` with regex for targeted edits +- Use `get_symbols_overview` before reading full files +- Serena tools provide structured, semantically-aware code editing + +## Build/Lint/Test Commands + +```bash + +never start dev server just use the base_url from .env file + +# Run all tests +./vendor/bin/phpunit +composer test + +# Run single test file +./vendor/bin/phpunit tests/unit/HealthTest.php + +# Run with coverage +./vendor/bin/phpunit --coverage-html coverage/ + +# Run specific test method +./vendor/bin/phpunit --filter testMethodName + +# Run migrations +php spark migrate +php spark migrate:rollback + +# Clear cache +php spark cache:clear +``` ## Technology Stack | Layer | Technology | |-------|------------| | Backend | CodeIgniter 4 (PHP 8.1+) | -| Frontend | Alpine.js + TailwindCSS | -| Database | MySQL/MariaDB | +| Frontend | Alpine.js + TailwindCSS + DaisyUI | +| Database | SQL Server | -## Key Files & Locations +## Code Style Guidelines -### Backend - -``` -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 -``` - -### Frontend - -``` -app/Views/ # PHP views with Alpine.js -app/Views/layout/ # Base templates -public/ # Static assets (css, js) -``` - -## Coding Conventions - -### PHP / CodeIgniter 4 - -1. **Controllers** extend `BaseController` and use `ResponseTrait` -2. **Models** extend `App\Models\BaseModel` (custom base with auto camel/snake conversion) -3. **Soft deletes** are enabled on all tables (`deleted_at`) -4. **Timestamps** are automatic (`created_at`, `updated_at`) -5. **Validation** happens in controllers, not models -6. **JSON API responses** follow this structure: - ```php - return $this->respond([ - 'status' => 'success', - 'message' => 'fetch success', - 'data' => $rows - ], 200); - ``` - -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()` - -### Database - -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 - -```php -// 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'); -``` - -### Controller Template +### PHP +**Imports** - Order: CodeIgniter, App namespace, use statements ```php model = new ItemsModel(); - $this->rules = [ - 'itemCode' => 'required|min_length[1]', - 'itemName' => 'required', - ]; - } +**Comments** +- Minimal/none - write self-documenting code - public function index() { - $keyword = $this->request->getGet('keyword'); - try { - $rows = $this->model->getItems($keyword); - return $this->respond([ - 'status' => 'success', - 'message' => 'fetch success', - 'data' => $rows - ], 200); - } catch (\Exception $e) { - return $this->failServerError('Exception: ' . $e->getMessage()); - } - } - - 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()); - } - } +**Error Handling** +```php +try { + $rows = $this->model->findAll(); + return $this->respond(['status' => 'success', 'message' => 'fetch success', 'data' => $rows], 200); +} catch (\Exception $e) { + return $this->failServerError($e->getMessage()); } ``` -### Model Template +### Controllers +**Structure** +- Extend `BaseController`, use `ResponseTrait` +- Define `$model` and `$rules` in `__construct()` +- CRUD pattern: `index()`, `show($id)`, `create()`, `update($id)`, `delete($id)` + +**Input Handling** ```php -request->getJSON(true); +$input = camel_to_snake_array($input); // Convert before DB +``` -use App\Models\BaseModel; +**Response Format** +```php +return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => $rows +], 200); +``` -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; - - public function getItems($keyword = null) { - if ($keyword) { - return $this->groupStart() - ->like('item_code', $keyword) - ->orLike('item_name', $keyword) - ->groupEnd() - ->findAll(); - } - return $this->findAll(); - } +**Validation** +```php +if (!$this->validate($this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); } ``` -### Migration Template +### Models +**Structure** +- Extend `App\Models\BaseModel` (auto camel/snake conversion) +- Always enable: `$useSoftDeletes = true`, `$useTimestamps = true` +- Define: `$table`, `$primaryKey`, `$allowedFields` + +**Example** ```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); - } -} +protected $table = 'results'; +protected $primaryKey = 'result_id'; +protected $allowedFields = ['control_id', 'test_id', 'res_date', 'res_value']; ``` -### Routes Template +### Database Conventions -```php -post('/login', 'AuthController::login'); -$routes->get('/logout', 'AuthController::logout'); +**JavaScript** +- No jQuery - use Fetch API +- camelCase in JavaScript, snake_case in PHP/DB +- Use Alpine.js: `x-data`, `x-model`, `x-show`, `x-for`, `@click` -$routes->group('', ['filter' => 'jwt-auth'], function ($routes) { - $routes->get('/', 'PagesController::dashboard'); - $routes->get('/module', 'PagesController::module'); - $routes->get('/master/items', 'PagesController::masterItems'); -}); +**DaisyUI** +- Use DaisyUI components (btn, input, modal, dropdown, etc.) +- TailwindCSS for utilities -$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'); -}); +**Base URL** +```javascript +const BASEURL = ''; // or window.BASEURL ``` -### View Template (Index with Alpine.js) +## File Naming -```php -extend("layout/main_layout"); ?> - -section("content"); ?> -
-
-
-

Items

-

Manage your items

-
- -
- -
-
-
- - -
- -
-
- -
- -
- - include('module/items/dialog_items_form'); ?> -
-endSection(); ?> - -section("script"); ?> - -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 -``` +| Component | Pattern | Example | +|-----------|---------|---------| +| Controller | `PascalCase + Controller` | `ResultsController` | +| Model | `PascalCase + Model` | `ResultsModel` | +| Migration | `YYYY-MM-DD-XXXXXX_Description.php` | `2026-01-15-000001_Results.php` | +| View | `module/action.php` | `entry/daily.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? +- jQuery, over-engineering, skipping soft deletes, hardcoding +- Don't mix concerns (Controllers handle HTTP, Models handle data) +- Don't forget camel/snake conversion at boundaries +- Don't add unnecessary comments ## 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. +After changes: run `./vendor/bin/phpunit`, update README.md for new features diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 2fb2260..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,178 +0,0 @@ -# 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 7 - -### 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\*` - Consolidated entry API controllers (DashboardApi, EntryApi, ReportApi) -- `Master\*` - CRUD for master data (MasterDepts, MasterTests, MasterControls) -- `Qc\*` - QC domain controllers (ControlTests, Results, ResultComments) - -**Views** (`app/Views/`): -- 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()` -- The `stringcase` helper is auto-loaded in `BaseController` - -### Database Schema - -Tables use soft deletes (`deleted_at`) and timestamps (`created_at`, `updated_at`): -- `dict_depts`, `dict_tests`, `dict_controls` - Master data -- `control_tests` - Control-test associations with QC parameters (mean, sd) -- `results` - Daily test results -- `result_comments` - Comments per result - -## Conventions - -### Case Convention -- **Frontend/JS/API**: camelCase -- **Backend PHP variables**: camelCase -- **Database**: snake_case -- Models handle automatic conversion; use helpers for manual conversions - -### API Response Format -```php -return $this->respond([ - 'status' => 'success', - 'message' => 'fetch success', - 'data' => $rows -], 200); -``` - -### Controller Pattern -```php -namespace App\Controllers\Master; - -use CodeIgniter\API\ResponseTrait; -use App\Controllers\BaseController; - -class DeptsController extends BaseController { - use ResponseTrait; - protected $model; - protected $rules; - - public function __construct() { - $this->model = new MasterDeptsModel(); - $this->rules = ['name' => 'required|min_length[1]']; - } - - public function index() { - $keyword = $this->request->getGet('keyword'); - $rows = $this->model->search($keyword); - return $this->respond([...], 200); - } - - public function create() { - $input = camel_to_snake_array($this->request->getJSON(true)); - if (!$this->validate($this->rules)) { - return $this->failValidationErrors($this->validator->getErrors()); - } - $id = $this->model->insert($input, true); - return $this->respondCreated(['status' => 'success', 'message' => $id]); - } -} -``` - -### Model Pattern -```php -namespace App\Models\Master; - -use App\Models\BaseModel; - -class MasterDeptsModel extends BaseModel { - protected $table = 'dict_depts'; - protected $primaryKey = 'dept_id'; - protected $allowedFields = ['dept_name', 'deleted_at']; - protected $useTimestamps = true; - protected $useSoftDeletes = true; - - public function search(?string $keyword) { - if ($keyword) { - $this->like('dept_name', $keyword); - } - return $this->findAll(); - } -} -``` - -### Routes Pattern -- Page routes: `$routes->get('/path', 'PageController::method');` -- API routes: `$routes->group('api', function($routes) { ... });` -- API sub-groups: `api/master`, `api/qc` - -## Frontend Patterns - -- Alpine.js `x-data` for component state (inline or in ` -endSection(); ?> -``` - -## Things to Avoid - -1. Don't skip soft deletes (`deleted_at`) -2. Don't mix concerns - controllers handle HTTP, models handle data -3. Don't forget case conversion - use helpers or BaseModel - -## Response Style - -- Use emojis in responses where appropriate to add visual appeal 😊 -- Keep responses concise and helpful diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 3ced2aa..92b55f8 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -18,7 +18,7 @@ $routes->get('/entry', 'PageController::entry'); $routes->get('/entry/daily', 'PageController::entryDaily'); $routes->get('/entry/monthly', 'PageController::entryMonthly'); $routes->get('/report', 'PageController::report'); -$routes->get('/report/view', 'PageController::reportView'); +$routes->get('/report/merged', 'PageController::reportMerged'); $routes->group('api', function ($routes) { $routes->get('dashboard/recent', 'Api\DashboardApiController::getRecent'); diff --git a/app/Controllers/Master/MasterControlsController.php b/app/Controllers/Master/MasterControlsController.php index 8fcbe05..07d03a3 100644 --- a/app/Controllers/Master/MasterControlsController.php +++ b/app/Controllers/Master/MasterControlsController.php @@ -24,7 +24,7 @@ class MasterControlsController extends BaseController { 'status' => 'success', 'message' => 'fetch success', 'data' => $rows - ], 200); + ], 200)->setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); } catch (\Exception $e) { return $this->failServerError($e->getMessage()); } diff --git a/app/Controllers/Master/MasterTestsController.php b/app/Controllers/Master/MasterTestsController.php index 14b1167..cab8723 100644 --- a/app/Controllers/Master/MasterTestsController.php +++ b/app/Controllers/Master/MasterTestsController.php @@ -23,7 +23,7 @@ class MasterTestsController extends BaseController { 'status' => 'success', 'message' => 'fetch success', 'data' => $rows - ], 200); + ], 200)->setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); } catch (\Exception $e) { return $this->failServerError($e->getMessage()); } diff --git a/app/Controllers/PageController.php b/app/Controllers/PageController.php index 934cbb4..e6a9058 100644 --- a/app/Controllers/PageController.php +++ b/app/Controllers/PageController.php @@ -46,4 +46,8 @@ class PageController extends BaseController { public function reportView() { return view('report/view'); } + + public function reportMerged() { + return view('report/merged'); + } } diff --git a/app/Views/entry/daily.php b/app/Views/entry/daily.php index 20749e2..c4384f5 100644 --- a/app/Views/entry/daily.php +++ b/app/Views/entry/daily.php @@ -43,39 +43,17 @@ -