tinyqc/AGENTS.md
mahdahar ff90e0eb29 Initial commit: Add CodeIgniter 4 QC application with full MVC structure
- CodeIgniter 4 framework setup with SQL Server database config
- Models: Control, Test, Dept, Result, Daily/ Monthly entry models
- Controllers: Dashboard, Control, Test, Dept, Entry, Report, API endpoints
- Views: CRUD pages with modal dialogs, dashboard, reports
- Database: Migrations for control test and daily/monthly result tables
- Legacy v1 PHP application preserved in /v1 directory
- Documentation: AGENTS.md, VIEWS_RULES.md for development guidelines
2026-01-14 16:49:27 +07:00

341 lines
9.7 KiB
Markdown

# AGENTS.md - QC Application Development Guide
This document provides guidelines for agentic coding agents working on this PHP QC (Quality Control) application built with CodeIgniter 4.
## Project Overview
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.
## Build/Lint/Test Commands
```bash
# PHP syntax check single file
php -l app/Controllers/Dashboard.php
# PHP syntax check all files recursively
find . -name "*.php" -exec php -l {} \; 2>&1 | grep -v "No syntax errors"
# 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
```
## Code Style Guidelines
### General Principles
- Follow CodeIgniter 4 MVC patterns
- Maintain consistency with surrounding code
- Keep files focused (<200 lines preferred)
- Use clear, descriptive names
### PHP Style
- Use `<?php` opening tag (not `<?` short tags)
- Enable strict types: `declare(strict_types=1);` at top of PHP files
- Use strict comparison (`===`, `!==`) over loose comparison
- Use `elseif` (one word) not `else if`
- Use parentheses with control structures for single statements
- Use 4 spaces for indentation (not tabs)
- Return types and typed properties required for new code:
```php
public function index(): string
{
return view('layout', [...]);
}
```
### Naming Conventions
- 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`)
### CodeIgniter 4 Patterns
**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'
]);
}
}
```
**Models** extend `CodeIgniter\Model`:
```php
namespace App\Models;
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
<div class="space-y-6">
<h1 class="text-2xl font-bold"><?= $title ?></h1>
<?php foreach ($tests as $test): ?>
<div><?= $test['name'] ?></div>
<?php endforeach; ?>
</div>
```
### 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
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
```
**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
<div x-data="{
property1: '',
property2: [],
init() {
// Initialization code
},
async loadData() {
// Fetch and handle data
App.showToast('Loaded successfully');
}
}">
<!-- Component markup -->
</div>
```
### 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
<?php
$isEdit = isset($record);
$action = $isEdit ? '/controller/update/' . $record['id'] : '/controller/save';
$title = $isEdit ? 'Edit Title' : 'Add Title';
?>
<div x-data="{ open: false }">
<?php if (!$isEdit): ?>
<button @click="open = true" class="...">Add</button>
<?php endif; ?>
<div x-show="open" class="fixed inset-0 z-50 ..." style="display: none;">
<h2 class="text-xl font-bold mb-4"><?= $title ?></h2>
<form action="<?= $action ?>" method="post">
<!-- Form fields with values from $record if edit mode -->
<input type="text" name="name" value="<?= $record['name'] ?? '' ?>">
<button type="submit"><?= $isEdit ? 'Update' : 'Save' ?></button>
</form>
</div>
</div>
```
**Index View Pattern (`index.php`):**
```php
<div class="space-y-6">
<!-- Table with data attributes for edit/delete -->
<table>
<tbody>
<?php foreach ($records as $record): ?>
<tr>
<td><?= $record['name'] ?></td>
<td>
<button data-edit-id="<?= $record['id'] ?>"
data-name="<?= $record['name'] ?>"
class="...">Edit</button>
<button data-delete-id="<?= $record['id'] ?>"
class="...">Delete</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Dialog for add (no data passed) -->
<?= view('module/dialog') ?>
<script>
// All modal-related JS in index.php
document.querySelectorAll('[data-edit-id]').forEach(btn => {
btn.addEventListener('click', function() {
document.getElementById('editId').value = this.dataset.editId;
document.getElementById('editName').value = this.dataset.name;
document.getElementById('editModal').classList.remove('hidden');
});
});
document.querySelectorAll('[data-delete-id]').forEach(btn => {
btn.addEventListener('click', function() {
if (confirm('Delete this record?')) {
window.location.href = '/controller/delete/' + this.dataset.deleteId;
}
});
});
</script>
```
**Searchable Multi-Select:**
- Use Select2 for searchable dropdowns (already included in layout.php)
- Initialize with: `$('#selectId').select2({ placeholder: 'Search...', allowClear: true })`
### Security Considerations
- Use CodeIgniter's built-in CSRF protection (`$this->validate('csrf')`)
- Escape all output in views with `<?= ?>` (auto-escaped)
- Use `$this->request->getPost()` instead of `$_POST`
- Never commit `.env` files with credentials
## Common Operations
**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]/`
**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