diff --git a/.agent/workflows/php-alpinejs-pattern.md b/.agent/workflows/php-alpinejs-pattern.md new file mode 100644 index 0000000..ff08c34 --- /dev/null +++ b/.agent/workflows/php-alpinejs-pattern.md @@ -0,0 +1,749 @@ +--- +description: PHP + Alpine.js SPA-like Application Pattern (CodeIgniter 4 + DaisyUI) +--- + +# PHP + Alpine.js Application Pattern + +This workflow describes how to build web applications using **PHP (CodeIgniter 4)** for backend with **Alpine.js + DaisyUI + TailwindCSS** for frontend, creating an SPA-like experience with server-rendered views. + +## Philosophy + +**"No-nonsense"** - Keep it simple, avoid over-engineering. This pattern gives you: +- Fast development with PHP backend +- Reactive UI with Alpine.js (no heavy framework overhead) +- Beautiful UI with DaisyUI/TailwindCSS +- JWT-based authentication + +--- + +## Technology Stack + +| Layer | Technology | +|-------|------------| +| Backend | CodeIgniter 4 (PHP 8.1+) | +| Frontend | Alpine.js + DaisyUI 5 + TailwindCSS | +| Database | MySQL/MariaDB | +| Auth | JWT (stored in HTTP-only cookies) | +| Icons | FontAwesome 7 | + +--- + +## Project Structure + +``` +project/ +├── app/ +│ ├── Config/ +│ │ └── Routes.php # All routes (pages + API) +│ ├── Controllers/ +│ │ ├── BaseController.php # Base controller +│ │ ├── PagesController.php # Page routes (returns views) +│ │ └── [Resource]Controller.php # API controllers +│ ├── Models/ +│ │ └── [Resource]Model.php # Database models +│ ├── Filters/ +│ │ └── JwtAuthFilter.php # JWT authentication filter +│ └── Views/ +│ ├── layout/ +│ │ └── main_layout.php # Base layout with sidebar +│ ├── [module]/ +│ │ ├── [module]_index.php # Main page with x-data +│ │ ├── dialog_[name].php # Modal dialogs (included) +│ │ └── drawer_[name].php # Drawer components +│ └── login.php +├── public/ +│ ├── index.php +│ └── assets/ +│ ├── css/output.css # Compiled TailwindCSS +│ └── js/app.js # Alpine.js setup +└── .env # Environment config +``` + +--- + +## 1. Backend Patterns + +### 1.1 Routes Structure (`app/Config/Routes.php`) + +Routes are split into: +1. **Public routes** - Login, logout, auth check +2. **Protected page routes** - Views (with `jwt-auth` filter) +3. **API routes** - RESTful JSON endpoints + +```php +get('/login', 'PagesController::login'); +$routes->post('/login', 'AuthController::login'); +$routes->get('/logout', 'AuthController::logout'); + +// Protected page routes (returns views) +$routes->group('', ['filter' => 'jwt-auth'], function ($routes) { + $routes->get('/', 'PagesController::dashboard'); + $routes->get('/patients', 'PagesController::patients'); + $routes->get('/requests', 'PagesController::requests'); + // Master data pages + $routes->get('/master/doctors', 'PagesController::masterDoctors'); +}); + +// API routes (returns JSON) +$routes->group('api', function ($routes) { + // Resource: patients + $routes->get('patients', 'PatientsController::index'); + $routes->get('patients/(:num)', 'PatientsController::show/$1'); + $routes->post('patients', 'PatientsController::create'); + $routes->patch('patients/(:num)', 'PatientsController::update/$1'); + + // Resource: [resourceName] + // Follow same pattern: index, show, create, update +}); +``` + +### 1.2 Pages Controller (`app/Controllers/PagesController.php`) + +This controller ONLY returns views. No business logic. + +```php + 'Dashboard', + 'activePage' => 'dashboard' + ]); + } + + public function patients() { + return view('patients/patients_index', [ + 'pageTitle' => 'Patients', + 'activePage' => 'patients' + ]); + } + + public function requests() { + return view('requests/requests_index', [ + 'pageTitle' => 'Lab Requests', + 'activePage' => 'requests' + ]); + } +} +``` + +### 1.3 API Controller Pattern (`app/Controllers/[Resource]Controller.php`) + +API controllers handle CRUD operations and return JSON. + +```php +model = new PatientsModel(); + $this->rules = [ + 'firstName' => 'required|min_length[2]', + 'lastName' => 'required|min_length[2]', + ]; + } + + /** + * GET /api/patients + * List all with optional search + */ + public function index() { + $keyword = $this->request->getGet('keyword'); + + try { + $rows = $this->model->search($keyword); + return $this->respond([ + 'status' => 'success', + 'message' => 'fetch success', + 'data' => $rows + ], 200); + } catch (\Exception $e) { + return $this->failServerError('Exception: ' . $e->getMessage()); + } + } + + /** + * GET /api/patients/:id + */ + public function show($id = null) { + try { + $row = $this->model->find($id); + if (empty($row)) { + return $this->respond(['status' => 'success', 'message' => 'not found'], 200); + } + return $this->respond(['status' => 'success', 'data' => $row], 200); + } catch (\Exception $e) { + return $this->failServerError('Error: ' . $e->getMessage()); + } + } + + /** + * POST /api/patients + */ + public function create() { + $input = $this->request->getJSON(true); + + if (!$this->validateData($input, $this->rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + + $input = camel_to_snake_array($input); // Convert keys to snake_case + + try { + $id = $this->model->insert($input); + return $this->respondCreated([ + 'status' => 'success', + 'message' => 'Created successfully', + 'data' => ['id' => $id] + ]); + } catch (\Exception $e) { + return $this->failServerError('Error: ' . $e->getMessage()); + } + } + + /** + * PATCH /api/patients/:id + */ + public function update($id = null) { + $input = $this->request->getJSON(true); + $input = camel_to_snake_array($input); + + try { + $this->model->update($id, $input); + return $this->respond(['status' => 'success', 'message' => 'updated']); + } catch (\Exception $e) { + return $this->failServerError('Error: ' . $e->getMessage()); + } + } +} +``` + +### 1.4 Standard API Response Format + +Always return this structure: + +```json +{ + "status": "success|error", + "message": "Human readable message", + "data": {} // or [] for lists +} +``` + +--- + +## 2. Frontend Patterns + +### 2.1 Base Layout (`app/Views/layout/main_layout.php`) + +The layout provides: +- Sidebar navigation with Alpine.js state +- Top navbar with user info +- Content section for page-specific content +- Script section for page-specific JavaScript + +```php + + + + + + AppName + + + + + + + +
+ + + + + renderSection('content') ?> +
+ + + renderSection('script') ?> + + +``` + +### 2.2 Page View Pattern (`app/Views/[module]/[module]_index.php`) + +Each page extends the layout and defines its Alpine.js component. + +```php +extend("layout/main_layout"); ?> + +section("content") ?> +
+ +
+
+

Patients

+

Manage patient records

+
+ +
+ + +
+ + +
+ + +
+ + +
+ + + include('[module]/dialog_form'); ?> +
+endSection(); ?> + +section("script") ?> + +endSection(); ?> +``` + +### 2.3 Dialog Component Pattern (`app/Views/[module]/dialog_form.php`) + +Dialogs are included in the main page and share x-data context. + +```php + + + + + +``` + +--- + +## 3. Naming Conventions + +### 3.1 Files & Directories + +| Type | Convention | Example | +|------|------------|---------| +| Views | `snake_case` | `patients_index.php` | +| Dialogs | `dialog_[name].php` | `dialog_form.php` | +| Drawers | `drawer_[name].php` | `drawer_filter.php` | +| Controllers | `PascalCase` | `PatientsController.php` | +| Models | `PascalCase` | `PatientsModel.php` | + +### 3.2 Variables & Keys + +| Context | Convention | Example | +|---------|------------|---------| +| PHP/Database | `snake_case` | `pat_id`, `first_name` | +| JavaScript | `camelCase` | `patId`, `firstName` | +| Alpine.js x-data | `camelCase` | `showModal`, `fetchList` | + +### 3.3 Primary Keys + +Use format: `{table_singular}_id` + +| Table | Primary Key | +|-------|-------------| +| `patients` | `pat_id` | +| `requests` | `request_id` | +| `master_tests` | `test_id` | + +--- + +## 4. Database Conventions + +### 4.1 Standard Columns + +Every table should have: + +```sql +`created_at` DATETIME, +`updated_at` DATETIME, +`deleted_at` DATETIME -- Soft deletes +``` + +### 4.2 Status Codes (Single Character) + +| Code | Meaning | +|------|---------| +| `P` | Pending | +| `I` | In Progress | +| `C` | Completed | +| `V` | Validated | +| `X` | Cancelled | + +--- + +## 5. UI/UX Guidelines + +### 5.1 Color Palette + +| Purpose | Color | TailwindCSS | +|---------|-------|-------------| +| Primary Action | Emerald | `bg-emerald-600` | +| Secondary | Slate | `bg-slate-800` | +| Danger | Red | `bg-red-500` | +| Info | Blue | `bg-blue-500` | +| Warning | Amber | `bg-amber-500` | + +### 5.2 Component Patterns + +- **Cards**: `bg-white rounded-xl border border-slate-100 shadow-sm` +- **Buttons**: Use DaisyUI `btn` with custom colors +- **Inputs**: Use DaisyUI `input input-bordered` +- **Modals**: Use DaisyUI `modal` with custom backdrop + +### 5.3 Icons + +Use FontAwesome 7 with consistent sizing: +- Navigation: `text-sm` +- Buttons: Default size +- Headers: `text-lg` to `text-2xl` + +--- + +## 6. Common Helpers + +### 6.1 camel_to_snake_array (PHP Helper) + +Convert JavaScript camelCase keys to PHP snake_case: + +```php +function camel_to_snake_array(array $data): array { + $result = []; + foreach ($data as $key => $value) { + $snakeKey = strtolower(preg_replace('/[A-Z]/', '_$0', lcfirst($key))); + $result[$snakeKey] = is_array($value) ? camel_to_snake_array($value) : $value; + } + return $result; +} +``` + +### 6.2 base_url() Usage + +Always use `base_url()` for asset and API paths: + +```php +// In PHP + + +// In JavaScript (via global variable) + +``` + +--- + +## 7. Quick Reference: Creating a New Module + +### Step 1: Create Route + +```php +// app/Config/Routes.php +$routes->get('/products', 'PagesController::products'); +$routes->get('api/products', 'ProductsController::index'); +$routes->get('api/products/(:num)', 'ProductsController::show/$1'); +$routes->post('api/products', 'ProductsController::create'); +$routes->patch('api/products/(:num)', 'ProductsController::update/$1'); +``` + +### Step 2: Create Model + +```php +// app/Models/ProductsModel.php +namespace App\Models; + +class ProductsModel extends BaseModel { + protected $table = 'products'; + protected $primaryKey = 'product_id'; + protected $allowedFields = ['name', 'sku', 'price']; +} +``` + +### Step 3: Create Controller + +Copy pattern from `PatientsController.php` or `RequestsController.php`. + +### Step 4: Create View + +Create `app/Views/products/products_index.php` following the page view pattern. + +### Step 5: Create Dialog + +Create `app/Views/products/dialog_form.php` for add/edit modal. + +### Step 6: Add to Navigation + +Update `app/Views/layout/main_layout.php` sidebar. + +--- + +## 8. Things to Avoid + +1. **Don't use jQuery** - Use Alpine.js or vanilla JS +2. **Don't over-engineer** - Keep it simple +3. **Don't skip soft deletes** - Always use `deleted_at` +4. **Don't hardcode URLs** - Use `base_url()` and `BASEURL` +5. **Don't mix concerns** - Controllers handle HTTP, Models handle data +6. **Don't create separate JS files for each page** - Keep JS inline in views for simplicity + +--- + +## 9. Checklist Before Deploying + +- [ ] All routes added to `Routes.php` +- [ ] API responses follow standard format +- [ ] Form validation in both frontend and backend +- [ ] Error handling with user-friendly messages +- [ ] Loading states for async operations +- [ ] Responsive design tested +- [ ] README.md updated diff --git a/app/Controllers/Pages/V2Page.php b/app/Controllers/Pages/V2Page.php index 6892f7d..210b919 100644 --- a/app/Controllers/Pages/V2Page.php +++ b/app/Controllers/Pages/V2Page.php @@ -89,7 +89,7 @@ class V2Page extends Controller return view('v2/dashboard', [ 'title' => 'V2 Dashboard', 'user' => $this->user, - 'currentPage' => 'dashboard' + 'activePage' => 'dashboard' ]); } @@ -103,7 +103,7 @@ class V2Page extends Controller return view('v2/api-tester', [ 'title' => 'API Tester', 'user' => $this->user, - 'currentPage' => 'api-tester' + 'activePage' => 'api-tester' ]); } @@ -117,7 +117,7 @@ class V2Page extends Controller return view('v2/db-browser', [ 'title' => 'DB Browser', 'user' => $this->user, - 'currentPage' => 'db-browser' + 'activePage' => 'db-browser' ]); } @@ -131,7 +131,7 @@ class V2Page extends Controller return view('v2/logs', [ 'title' => 'Logs', 'user' => $this->user, - 'currentPage' => 'logs' + 'activePage' => 'logs' ]); } @@ -152,7 +152,7 @@ class V2Page extends Controller return view('v2/organization', [ 'title' => 'Organization: ' . ucfirst($type) . 's', 'user' => $this->user, - 'currentPage' => 'organization', + // activePage set below for sub-pages 'activePage' => 'organization-' . $type, 'type' => $type ]); @@ -168,7 +168,7 @@ class V2Page extends Controller return view('v2/valuesets', [ 'title' => 'Value Sets', 'user' => $this->user, - 'currentPage' => 'valuesets' + 'activePage' => 'valuesets' ]); } @@ -182,7 +182,7 @@ class V2Page extends Controller return view('v2/patients', [ 'title' => 'Patients', 'user' => $this->user, - 'currentPage' => 'patients' + 'activePage' => 'patients' ]); } @@ -196,7 +196,7 @@ class V2Page extends Controller return view('v2/patient-form', [ 'title' => 'New Patient', 'user' => $this->user, - 'currentPage' => 'patients' + 'activePage' => 'patients' ]); } @@ -218,7 +218,7 @@ class V2Page extends Controller return view('v2/patient-form', [ 'title' => 'Edit Patient', 'user' => $this->user, - 'currentPage' => 'patients', + 'activePage' => 'patients', 'patient' => $patient ]); } @@ -233,7 +233,7 @@ class V2Page extends Controller return view('v2/patient-view', [ 'title' => 'Patient Details', 'user' => $this->user, - 'currentPage' => 'patients', + 'activePage' => 'patients', 'patientId' => $id ]); } @@ -263,7 +263,7 @@ class V2Page extends Controller return view('v2/jwt-decoder', [ 'title' => 'JWT Decoder', 'user' => $this->user, - 'currentPage' => 'jwt-decoder', + 'activePage' => 'jwt-decoder', 'token' => $token, 'decoded' => $decoded ]); diff --git a/app/Views/layouts/v2.php b/app/Views/layouts/v2.php index a76ced2..b662f67 100644 --- a/app/Views/layouts/v2.php +++ b/app/Views/layouts/v2.php @@ -251,7 +251,7 @@