From da4942c9f56a7238a228a4d532749515768aa4ed Mon Sep 17 00:00:00 2001
From: mahdahar <89adham@gmail.com>
Date: Wed, 24 Dec 2025 16:42:07 +0700
Subject: [PATCH] fix v2 patients
---
.agent/workflows/php-alpinejs-pattern.md | 749 +++++++++++++++++++++++
app/Controllers/Pages/V2Page.php | 22 +-
app/Views/layouts/v2.php | 19 +-
app/Views/v2/patients.php | 6 +-
4 files changed, 777 insertions(+), 19 deletions(-)
create mode 100644 .agent/workflows/php-alpinejs-pattern.md
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
+
+
+
+
+
+
+
+
+
+
+
+
+ = $this->renderSection('content') ?>
+
+
+
+ = $this->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
+= $this->extend("layout/main_layout"); ?>
+
+= $this->section("content") ?>
+
+
+
+
+
Patients
+
Manage patient records
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ include('[module]/dialog_form'); ?>
+
+= $this->endSection(); ?>
+
+= $this->section("script") ?>
+
+= $this->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 @@