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

9.7 KiB

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

# 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:
    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:

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:

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:

<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:
    $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:
    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:

<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:
    <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:

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
$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):

<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