# AGENTS.md — SalonPro X 2.0

## Final Summary

### Goal
Enterprise Level Salon ERP + CRM + POS + Appointment Management System built from scratch with Core PHP 8.3+, MySQL 8.0+, Bootstrap 5, vanilla JS.

### Architecture
- No frameworks (Laravel, CodeIgniter, etc.)
- MVC with Core PHP, PSR coding style, prepared statements, CSRF, XSS, RBAC
- Gold (#C8A44E) & Dark (#0D0D0D) luxury glassmorphism theme
- Local vendor assets only (Bootstrap, jQuery, Font Awesome, Chart.js)
- Cart in PHP session for POS

### Progress
| Phase | Status |
|-------|--------|
| 1 — Core Infrastructure (Router, DB, Auth, helpers, etc.) | ✅ Complete |
| 1 — Database schema + seed (62 tables, foreign keys, triggers, views, procs) | ✅ Imported, running |
| 2 — Templates (layouts, partials, 8 components) | ✅ Complete |
| 2 — Assets (6 CSS, 4 JS, SVG logos, favicon) | ✅ Complete |
| 3 — All 17 Modules (233 source files) | ✅ Built |
| Error triage & schema alignment | ✅ Complete |
| System verification (12 pages tested) | ✅ All 200 |

### Framework Fixes Applied
- `Router.php`: `ErrorsController`→`ErrorController` handler name
- `Database.php`: placeholder sanitization for functions in `where()`
- `Database.php`: `IS NULL`/`IS NOT NULL` support when `$value=null`
- `Database.php`: `\Closure` support for grouped WHEREs
- `Database.php`: `whereRaw()` method added
- `Database.php`: `paginate()` count query uses subquery with wrapping alias
- `Request.php`: `url()` strips base path from `REQUEST_URI`
- `index.php`: exception handler guards `http_response_code()` with `is_int()`

### Schema-to-Code Mismatches Fixed
- `appointments.start_datetime`→`start_time`, `end_datetime`→`end_time`
- `employees.status`→`is_active` (across 6+ controllers)
- `inventory_items`→`products` (POS, Inventory controllers)
- `appointment_items`→`appointment_services`
- `categories`→`product_categories`
- Virtual alias columns on `invoices` for `i.total`, `i.discount`, `i.status`
- Virtual alias columns on `invoice_items` for `total`, `price`
- `deleted_at` added to: branches, product_categories
- `cash_book.date`→`transaction_date` in CashBookController
- Dashboard queries simplified (removed invalid joins)

### Verified Pages (all HTTP 200)
`/dashboard`, `/customers`, `/services`, `/employees`, `/appointments`, `/inventory`, `/pos`, `/reports`, `/settings`, `/marketing`, `/notifications`, `/loyalty`, `/roles`, `/audit-logs`, `/accounting/cash-book`

### Key Credentials
- URL: `http://localhost/SalonPro_X_2.0/`
- DB: `salonprox_db`, MariaDB 10.4, InnoDB, utf8mb4
- PHP: 8.2.12 (XAMPP), bcrypt cost 12
- Login: `admin@salonprox.com` / `admin123`

### Session 2 Fixes Applied
- **SalaryController**: `salaries` table → `employee_salary`; removed `deleted_at IS NULL` (column missing); fixed column names (`month`/`year`→`salary_date`, `status`→`payment_status`, `paid_at`→`payment_date`, `processed_by`→`paid_by`); commissions query fixed (`paid_status`→`status`, `paid_date`→`paid_at`, `month`/`year`→extracted from `salary_date`)
- **StockController**: removed `deleted_at IS NULL` on `stock_adjustments` (column missing)
- **PurchaseController** (3 occ): removed `deleted_at IS NULL` on `goods_received_notes` (column missing)
- **RefundController**: removed `deleted_at IS NULL` on `refunds` (column missing); also fixed `invoice_refunds`→`refunds`, `refund_amount`→`amount`, `refund_reason`→`reason`, `refunded_by`→`created_by`, removed `refund_items` insert (no table), removed `updated_at`
- **v_todays_appointments**: dropped (used renamed `start_datetime`/`end_datetime` columns)
- **POS checkout (POSController.php)**: Fixed `invoices` insert columns: `user_id`→`created_by`, removed `discount_type`/`discount_value`/`coupon_code`/`coupon_discount`/`tax_rate` (no columns), `tax`→`tax_total`, `balance`→`balance_amount`, `note`→`notes`, `discount`→`discount_total`; fixed `invoice_items` insert: `item_type`→`reference_type` (map to singular), `item_id`→`reference_id`, `price`/`total`→`unit_price`/`total_price`; fixed `invoice_payments` insert: `reference`→`reference_number`, removed `payment_date`; fixed inventory decrement: `inventory_items`→`products`, `stock_quantity`→`current_stock`
- **POS view (pos.php)**: Fixed JS `addToCart` to use `item.unit_price` (not `selling_price`); exposed all onclick-dependent functions to `window` so they're accessible from inline onclick handlers (layout wraps `$inlineScripts` in `$(function(){...})` closure, trapping function declarations)
- **Invoice Model**: Fixed `$fillable`/`$casts` to match actual columns; `getWithRelations()` join `i.user_id`→`i.created_by`
- **Payment Model**: Fixed `$fillable`/`$casts`; all queries changed `payment_date`→`created_at`; `reference`→`reference_number`
- **InvoiceController**: Fixed SQL joins `i.user_id`→`i.created_by`; URLs `pos/invoices`→`invoices`
- **PaymentController**: Fixed `payment_date`→`created_at`, `reference`→`reference_number`, `balance`→`balance_amount`; URLs `pos/payments`→`payments`
- **Invoice views (index, invoice, print_invoice)**: Fixed all column refs (`balance`→`balance_amount`, `tax`→`tax_total`, `note`→`notes`, `reference`→`reference_number`, `item_type`→`reference_type`, `discount_value`/`coupon_discount`→`discount_total`); fixed all URLs from `pos/invoices`/`pos/invoice` to `invoices`
- **Refund/Payments views**: Fixed URLs from `pos/invoices/show/{id}` to `invoices/{id}`
- All 37+ pages verified HTTP 200

### Remaining `deleted_at` Gaps (tables without column that code may query)
`expense_categories`, `income_categories`, `employee_salary`, `stock_adjustments`, `goods_received_notes`, `refunds` — all now handled (conditions removed from queries).

### Schema-to-Code Mismatches (comprehensive)
- `invoices` table: real columns are `discount_total`, `tax_total`, `grand_total`, `net_total`, `paid_amount`, `balance_amount`, `created_by`, `notes` — NOT `discount_value`, `coupon_discount`, `tax_rate`, `tax`, `balance`, `user_id`, `note`. `total`/`discount`/`status` are VIRTUAL GENERATED (read-only).
- `invoice_items` table: real columns are `reference_type` (enum `service`/`product`/`package`), `reference_id`, `unit_price`, `total_price` — NOT `item_type`, `item_id`, `price`, `total`. `price`/`total` are VIRTUAL GENERATED (read-only).
- `invoice_payments` table: uses `reference_number`, `created_at` — NOT `reference`, `payment_date`.
- `refunds` table: uses `amount`, `reason`, `created_by` — NOT `refund_amount`, `refund_reason`, `refunded_by`. No `refund_items` table exists.
- `refunds` table lacks `deleted_at` — condition removed from all queries.
- Cart stores `type` as `services`/`products` (plural), but DB enum expects `service`/`product` (singular).

### Session 3 Fixes Applied
- **FullCalendar** — Downloaded `fullcalendar.min.js` v6.1.15 to `assets/js/vendor/`. Fixed triple JS bug: (1) removed outer `DOMContentLoaded` (layout already wraps `$inlineScripts` in `$(function(){...})`), (2) removed `extraParams` callback (caused TDZ on `const calendar`), (3) changed `themeSystem: 'bootstrap5'`→`'standard'` (bundle lacks Bootstrap 5 plugin). Fixed tooltip: `$(el).tooltip()`→`new bootstrap.Tooltip(el, {...})`. Fixed CSS: `.card-body { overflow-x: auto }`→`overflow: visible`. Added FC dark‑theme CSS variable overrides in `components.css`.
- **Reception Board** — Fixed route URLs: `update-status/{id}`→`status/{id}`, `check-in/{id}`→`checkin/{id}` in `reception.php` and `show.php`.
- **Salary page (500 → 200)** — Fixed `whereRaw()`/`where()` PDO binding mismatch (mixed `?` positional with `:name` named placeholders). Changed to date‑range filter: compute `$startDate`/`$endDate` from month/year, use two `where()` calls with `salary_date` range.
- **Created missing assets**: `assets/images/logo-white.svg` (was 404), `assets/images/favicon.ico` (was 404).
- **Verified 41 pages** — All return HTTP 200, zero PHP errors in log:
  `/dashboard`, `/customers`, `/services`, `/employees`, `/appointments`, `/inventory`, `/pos`, `/reports`, `/settings`, `/marketing`, `/notifications`, `/loyalty`, `/roles`, `/audit-logs`, `/accounting/cash-book`, `/accounting/petty-cash`, `/expenses`, `/income`, `/salary`, `/membership`, `/gift-cards`, `/purchases`, `/reception`, `/invoices`, `/attendance`, `/commissions`, `/performance`, `/suppliers`, `/stock-transfers`, `/stock-adjustments`, `/grn`, `/branches`, `/packages`, `/profile`, `/payments`, `/refunds`, `/backup`, `/reports/*` (14 sub-pages)

### Session 4 Fixes Applied
- **PO store (was silently failing)** — Changed `$this->request->get('items')` → `$this->request->post('items')` so POST form data is read (`PurchaseController.php:113`).
- **Table name `purchase_items` → `purchase_order_items`** in 3 places (store insert, show query, receive query).
- **GRN insert** — removed ghost columns `supplier_id`, `status`, `updated_at` (`goods_received_notes` table hasn't those columns).
- **`grn_items` insert** — removed `purchase_item_id`, `total_cost`; added `quantity_accepted` / `quantity_rejected`.
- **`stock_movements` insert** — `type` → `movement_type`, `'purchase_receive'` → `'purchase'` (enum constraint), removed `grn_id`, `notes`; added `before_quantity` / `after_quantity`.
- **GRN list & view joins** — `grn.supplier_id` → `po.supplier_id`.
- **GRN route** — `/grn` → `/purchases/grn` in `App.php`.
- **Added route `/purchases/show/{id}`** — maps to `PurchaseController@show`.
- **Added `/purchases/approve/{id}` and `/purchases/receive/{id}` routes** — both POST.
- **Index query** — added `leftJoin('users u', …)` for `user_first_name` / `user_last_name`.
- **Grand Total JS** — `updateGrandTotal()` parses `Rs. 7500.00` correctly (strips `Rs.` prefix explicitly, avoiding `.7500.00` → `0.75` bug). `create.php:165`.
- **Exposed inline onclick functions to `window`** — `addRow`, `removeRow`, `updateRowTotal`, `updateGrandTotal`, `currency` at `create.php`.
- **Added `window.confirmAction`** — approve/receive buttons call it via inline `show.php` script.
- **GRN view status blank** — removed `as po_status` alias (view accesses `$order->status`).
- **POS `"Cart is empty"` checkout fix** — Cart was JS-only (`var cart = {}`) but server read `Session::get('pos_cart', [])` (always empty). Fixed: JS sends `cart_items: JSON.stringify(Object.values(cart))` via POST; server decodes from `$this->request->post('cart_items', '[]')`.
- **POS coupon validation** — `validateCoupon()` read cart from session (always 0 subtotal). Fixed to read `subtotal` from POST (sent by JS `applyCoupon()`).
- **POS `newSale()`** — removed `Session::remove('pos_cart')` (no longer exists).
- **POS `customer_id` FK violation (1048)** — Walk-in Customer (ID 3) default when none selected.
- **"Balance Due" when overpaid** — Changed `balance_amount` to `max(0, grand_total - paid)`; added `$changeDue = max(0, paid - grand_total)`. Views show **Change Due** (yellow) when overpaid, else **Balance Due** (red) when `balance_amount > 0`.
- **Product FK constraint error (1452)** — Category made required (`'required|integer'`); removed empty `<option value="">` from create/edit dropdowns.
- **404 viewing product (`/inventory/show/{id}`)** — Added route at `App.php:196`.
- **`show()` querying `purchase_items`** — Changed to `purchase_order_items`.
- **Undefined property `$movement->type`** — Column is `movement_type`, fixed in `show.php:149`.
- **Stock doubled on GRN receive** — DB trigger `trg_after_grn_item_insert` already updates `current_stock` and inserts `stock_movements`. PHP was duplicating. Removed manual update from `PurchaseController@receive`.
- **POS stock doubled on checkout** — DB trigger `trg_after_invoice_item_insert` already decrements stock. Removed manual decrement from `POSController@checkout`.
- **Cleaned up existing doubled stock data** — Deleted duplicate `stock_movements` and reset `current_stock` (Water bottle: 32→20, Test Item 1: 20→10, Test Item 2: 20→10).
- **Duplicate product validation** — `store()` and `update()` check for existing `name` and `sku` with `where('deleted_at', 'IS', null)`. Redirect with error and preserved input via `withInput()->withAlert()->redirect(url(...))`.
- **Flash messages not rendering after redirect** — `Controller::getSharedData()` called `Session::getAlerts()` eagerly, consuming flash before the `alerts.php` partial could read it. Fixed: removed `'alerts' => Session::getAlerts()` from `getSharedData()`. (`Controller.php:75`)

### Critical Notes
- **`whereRaw` + `where` both use `$this->bindings`** — `where()` uses named placeholders (`:col_N`), `whereRaw()` uses positional (`?`). PDO cannot mix. Either avoid `whereRaw` alongside `where()`, or rewrite `whereRaw` to generate named placeholders.
- Layout `main.php` wraps `$inlineScripts` in `$(function(){...})` — view JS must NOT have own `DOMContentLoaded` wrapper. Functions for inline `onclick` must be exposed via `window.functionName`.
- `Controller::getSharedData()` should NOT call `Session::getAlerts()` — it consumes flash before partials render. Partial `alerts.php` handles session reading natively.
- Use `$this->request->post()` for POST form data, NOT `$this->request->get()`.
- Stock updates are handled by DB triggers (`trg_after_grn_item_insert`, `trg_after_invoice_item_insert`) — PHP code must NOT duplicate them.
- Cart is client-side JS only; server receives it via POST `cart_items` JSON string.

### Next Steps
1. Test CRUD on all Inventory sub-modules (Suppliers, Stock Transfers, Stock Adjustments)
2. Test full business flow: Appointment → Check‑in → POS → Payment → Invoice → Refund
3. Add `deleted_at` columns to tables missing them if soft-delete is desired
4. Review Report model complex SQL for column mismatches
5. Fix `core/Model.php:33` `all()` — generically applies `deleted_at IS NULL`; risk for tables without that column
