# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Club/sports facility ERP system built on a **custom PHP 8.1+ framework** (NOT Laravel, NOT Symfony). Arabic-first, RTL, multi-branch. All views are in Arabic. The framework is hand-built with plain PDO, custom autoloader, custom template engine, and modular MVC architecture.

## Commands

```bash
php cli.php migrate              # Run pending database migrations
php cli.php migrate:rollback     # Undo last batch
php cli.php migrate:status       # Show migration status
php cli.php seed                 # Run pending seeds
php cli.php seed:run <Name>      # Run specific seed
php cli.php cron                 # Run cron jobs
```

No composer, no npm, no build step. The app uses a custom PSR-4-style autoloader (`App\Core\Autoloader`). No test framework is configured.

## Architecture

### Entry Points
- **Web**: `public/index.php` — boots App singleton, dispatches router, sends response
- **CLI**: `cli.php` — minimal bootstrap (no sessions/routing), handles migrate/seed/cron

### App Boot Sequence
`App::getInstance()->boot()`: timezone (Africa/Cairo) -> .env -> config files -> DB connect -> session start -> load `system_config` table overrides -> execute all `app/Modules/*/bootstrap.php` (sorted) -> init router -> load all `app/Modules/*/Routes.php`

### Module Structure
Each module lives in `app/Modules/{Name}/` with:
- `bootstrap.php` — registers permissions via `PermissionRegistry`, sidebar menu via `MenuRegistry`, event listeners via `EventBus::listen()`
- `Routes.php` — returns array of `[METHOD, PATH, HANDLER, MIDDLEWARE_ARRAY, PERMISSION]`
- `Controllers/`, `Models/`, `Services/`, `Views/`

### Routing
Route format: `['GET', '/members/{id:\d+}', 'Members\Controllers\MemberController@show', ['auth'], 'member.view']`

Handler string resolves to `App\Modules\{handler}`. Controller method receives `(Request $request, ...$routeParams)` and returns `Response`.

Middleware names map to classes: `auth` -> AuthMiddleware, `csrf` -> CSRFMiddleware, `permission` -> PermissionMiddleware.

### Database Layer
`App\Core\Database` wraps PDO directly. Key methods:
- `select($sql, $params)` / `selectOne($sql, $params)` — prepared statements
- `insert($table, $data)` / `update($table, $data, $where, $params)` / `delete($table, $where, $params)`
- `query($sql, $params)` returns PDOStatement; `raw($sql)` executes without params

**No ORM relations.** Relationships are loaded via manual SQL joins or separate queries.

**IMPORTANT**: MySQL `SHOW TABLES LIKE ?` does NOT work with prepared statement placeholders. Use `information_schema.tables` query or hardcoded string instead.

### Model Layer
`App\Core\Model` provides: `$table`, `$fillable`, `$timestamps`, `$softDelete` (uses `is_archived` column). Static methods: `find()`, `findOrFail()`, `create()`, `query()` (returns QueryBuilder). Instance methods: `save()`, `update()`, `archive()`. Models emit events via EventBus: `{singular}.created`, `{singular}.updated`, `{singular}.archived`.

### QueryBuilder
`Model::query()->where('status','=','active')->orderBy('name','ASC')->get()`. Supports `where`, `whereIn`, `whereNull`, `join`, `leftJoin`, `groupBy`, `having`, `limit`, `offset`, `paginate`. Soft delete filtering is automatic (`withoutArchived()` by default).

### Template Engine
Plain PHP templates with layout/section support. View path uses dot notation: `'Members.Views.show'` -> `app/Modules/Members/Views/show.php`. Layout files: `'Layout.main'` -> `app/Shared/Layout/main.php`.

In views: `$__template->layout('Layout.main')`, `$__template->section('content')` / `$__template->endSection()`, `$__template->yield('content')`, `$__template->include('path', $data)`.

### EventBus
`EventBus::listen('event.name', $callback, $priority)` / `EventBus::dispatch('event.name', $data)`. Used heavily for cross-module integration (e.g., Accounting listens to payment/sale/fine events to auto-post journal entries).

### Flash Messages
`$session->flash('_alerts', [['type' => 'success', 'message' => '...']])`. Response helpers: `->withSuccess()`, `->withError()`, `->withWarning()`.

### Authorization
Routes specify a permission string. `Controller::authorize($permission)` throws RuntimeException(403) if denied. Employee permissions come from `role_permissions` + `employee_permissions` tables. Super admin check: query `employee_roles` joined with `roles` where `role_code = 'super_admin'`.

## Key Conventions

### Migration Naming
`database/migrations/Phase_NN_NNN_description.php` — returns `['up' => $sql, 'down' => $sql]` or a closure receiving `Database`. Multiple SQL statements in `up` are split by `;\n` (semicolon + newline).

### Seed Naming
`database/seeds/Phase_NN_NNN_description.php` — returns closure receiving `Database`. Tracked in `seeds` table; won't re-run unless forced.

### Permission Keys
Format: `module.action` (e.g., `member.view`, `accounting.journal.create`). Registered in each module's `bootstrap.php` via `PermissionRegistry::register()`.

### Global Helpers
`e()` for HTML escaping, `money()` for currency formatting, `csrf_field()` for form tokens, `old()` for repopulating forms after validation failure, `arabic_date()` for localized dates, `now()` / `today()` for current datetime/date.

### Controller Patterns
- `$this->view('Module.Views.path', $data)` to render
- `$this->json($data)` for API responses
- `$this->redirect('/path')` for redirects
- `$this->validate($_POST, $rules)` validates and flashes errors on failure
- `$this->authorize('permission.key')` checks permission

### Response Chaining
`return $this->redirect('/path')->withSuccess('message')` — flash + redirect in one call.

### Database Config
Connection params from `config/database.php` which reads from `.env`: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS`. Constructor: `new Database(string $host, int $port, string $dbName, string $user, string $pass, string $charset)`.

## Common Pitfalls

- **CLI context**: `App::getInstance()->db()` is null in CLI. Seeds receive `Database` as parameter; use `App::getInstance()->setDb($db)` if services need the App singleton.
- **Table existence checks**: Never use `SHOW TABLES LIKE ?` with params. Use `SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?`.
- **Column names vary by table**: Not all tables have `is_archived` — check the migration before assuming. `fines` has no `is_archived` or `imposed_date` (use `created_at`). `hr_payroll_runs` uses `paid_at` not `payment_date`. `installment_plans` has no `is_archived`.
- **`declare(strict_types=1)`**: Used in all PHP files. Type mismatches (e.g., passing array to string param) cause fatal errors.
- **ExceptionHandler**: Respects exception code for HTTP status (401, 403, 404 return proper pages; everything else is 500).
