# AGENTS.md - AI Agent Guidelines for [PROJECT NAME]

## AI Agent Guidelines

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.

## Technology Stack

| Layer | Technology |
|-------|------------|
| Backend | CodeIgniter 4 (PHP 8.1+) |
| Frontend | Alpine.js + TailwindCSS |
| Database | MySQL/MariaDB |

## Key Files & Locations

### 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
<?php
namespace App\Controllers\Module;

use CodeIgniter\API\ResponseTrait;
use App\Controllers\BaseController;
use App\Models\Module\ItemModel;

class ItemsController extends BaseController {
    use ResponseTrait;

    protected $model;
    protected $rules;

    public function __construct() {
        $this->model = new ItemsModel();
        $this->rules = [
            'itemCode' => 'required|min_length[1]',
            'itemName' => 'required',
        ];
    }

    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());
        }
    }
}
```

### Model Template

```php
<?php
namespace App\Models\Module;

use App\Models\BaseModel;

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();
    }
}
```

### Migration Template

```php
<?php
namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class ModuleItems extends Migration {

    public function up() {
        $this->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
<?php

use CodeIgniter\Router\RouteCollection;

$routes = get('/login', 'PagesController::login');
$routes->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"); ?>
<main class="flex-1 p-6 overflow-auto bg-slate-50/50" x-data="items()">
    <div class="flex justify-between items-center mb-6">
        <div>
            <h1 class="text-2xl font-bold text-slate-800 tracking-tight">Items</h1>
            <p class="text-slate-500 text-sm mt-1">Manage your items</p>
        </div>
        <button
            class="btn btn-sm gap-2 shadow-sm border-0 bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 text-white transition-all duration-200"
            @click="showForm()"
        >
            <i class="fa-solid fa-plus"></i> New Item
        </button>
    </div>

    <div class="bg-white rounded-xl border border-slate-100 shadow-sm p-4 mb-6">
        <div class="flex items-center gap-3">
            <div class="relative flex-1 max-w-md">
                <i class="fa-solid fa-magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm"></i>
                <input
                    type="text"
                    placeholder="Search by name or code..."
                    class="w-full pl-10 pr-4 py-2.5 text-sm bg-slate-50 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
                    x-model="keyword"
                    @keyup.enter="fetchList()"
                />
            </div>
            <button
                class="px-4 py-2.5 text-sm font-medium bg-slate-800 text-white rounded-lg hover:bg-slate-700 transition-all duration-200 flex items-center gap-2"
                @click="fetchList()"
            >
                <i class="fa-solid fa-magnifying-glass text-xs"></i> Search
            </button>
        </div>
    </div>

    <div class="bg-white rounded-xl border border-slate-100 shadow-sm overflow-hidden">
        <template x-if="list">
            <div class="overflow-x-auto">
                <table class="w-full text-sm text-left">
                    <thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wider">
                        <tr>
                            <th class="py-3 px-5 font-semibold">Code</th>
                            <th class="py-3 px-5 font-semibold">Name</th>
                            <th class="py-3 px-5 font-semibold text-right">Action</th>
                        </tr>
                    </thead>
                    <tbody class="text-slate-600 divide-y divide-slate-100">
                        <template x-for="item in list" :key="item.itemId">
                            <tr class="hover:bg-slate-50/50 transition-colors">
                                <td class="py-3 px-5">
                                    <span class="font-mono text-xs bg-slate-100 text-slate-600 px-2 py-1 rounded" x-text="item.itemCode"></span>
                                </td>
                                <td class="py-3 px-5 font-medium text-slate-700" x-text="item.itemName"></td>
                                <td class="py-3 px-5 text-right">
                                    <button
                                        class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-amber-700 bg-amber-50 hover:bg-amber-100 rounded-lg transition-colors"
                                        @click="showForm(item.itemId)"
                                    >
                                        <i class="fa-solid fa-pencil"></i> Edit
                                    </button>
                                </td>
                            </tr>
                        </template>
                    </tbody>
                </table>
            </div>
        </template>
    </div>

    <?= $this->include('module/items/dialog_items_form'); ?>
</main>
<?= $this->endSection(); ?>

<?= $this->section("script"); ?>
<script type="module">
    import Alpine from '<?= base_url('/assets/js/app.js'); ?>';

    document.addEventListener('alpine:init', () => {
        Alpine.data("items", () => ({
            loading: false,
            showModal: false,
            errors: {},
            error: null,
            part: "items",
            keyword: "",
            list: null,
            item: null,
            form: {
                itemCode: "",
                itemName: "",
            },

            async fetchList() {
                this.loading = true;
                this.error = null;
                this.list = null;
                try {
                    const params = new URLSearchParams({ keyword: this.keyword });
                    const response = await fetch(`${window.BASEURL}api/module/${this.part}?${params}`, {
                        method: "GET",
                        headers: { "Content-Type": "application/json" }
                    });
                    if (!response.ok) throw new Error("Failed to load data");
                    const data = await response.json();
                    this.list = data.data;
                } catch (err) {
                    this.error = err.message;
                } finally {
                    this.loading = false;
                }
            },

            async loadData(id) {
                this.loading = true;
                try {
                    const response = await fetch(`${window.BASEURL}api/module/${this.part}/${id}`, {
                        method: "GET",
                        headers: { "Content-Type": "application/json" }
                    });
                    if (!response.ok) throw new Error("Failed to load item");
                    const data = await response.json();
                    this.form = data.data[0];
                } catch (err) {
                    this.error = err.message;
                    this.form = {};
                } finally {
                    this.loading = false;
                }
            },

            async showForm(id = null) {
                this.showModal = true;
                if (id) {
                    await this.loadData(id);
                } else {
                    this.form = {};
                }
            },

            closeModal() {
                this.showModal = false;
                this.form = {};
            },

            validate() {
                this.errors = {};
                if (!this.form.itemCode) this.errors.itemCode = "Code is required.";
                if (!this.form.itemName) this.errors.itemName = "Name is required.";
                return Object.keys(this.errors).length === 0;
            },

            async save() {
                if (!this.validate()) return;
                this.loading = true;
                let method = '';
                let url = '';
                if (this.form.itemId) {
                    method = 'patch';
                    url = `${BASEURL}api/module/${this.part}/${this.form.itemId}`;
                } else {
                    method = 'post';
                    url = `${BASEURL}api/module/${this.part}`;
                }
                try {
                    const res = await fetch(url, {
                        method: method,
                        headers: { "Content-Type": "application/json" },
                        body: JSON.stringify(this.form),
                    });
                    const data = await res.json();
                    if (data.status === 'success') {
                        alert("Data saved successfully!");
                        this.closeModal();
                        this.list = null;
                    } else {
                        alert(data.message || "Something went wrong.");
                    }
                } catch (err) {
                    console.error(err);
                    alert("Failed to save data.");
                } finally {
                    this.loading = false;
                }
            }
        }));
    });

    Alpine.start();
</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.
