# Rubis KTO Management System — AGENTS.md

## System Overview

Enterprise management system for **Rubis Kithyoko Service Station**. Upgraded from a legacy procedural PHP/Bootstrap codebase at `B:\Ampps\www\Rubis station` to a modern OOP MVC architecture at `B:\Ampps\www\Rubis station OOP`.

**Stack:** PHP 8.2 / MySQL (`rubis_kto`) / Tailwind CSS / Alpine.js / jQuery / Chart.js / DataTables / Flatpickr / Lucide Icons

---

## Architecture

### Directory Structure

```
Rubis station OOP/
├── app/
│   ├── Auth/              # Authentication service
│   ├── Controllers/
│   │   ├── Api/           # JSON API controllers (backend logic)
│   │   ├── Beverage/      # Beverage web controllers
│   │   ├── Fluid/         # Engine fluids web controllers
│   │   ├── Fuel/          # Fuel web controllers (dashboard, pump, purchase, supplier)
│   │   ├── Gas/           # Rubis gas web controllers
│   │   ├── Ledger/        # Credit/ledger web controllers
│   │   ├── Payroll/       # Payroll web controllers
│   │   └── *.php          # Auth, Dashboard, Home, Expense, Report, Chart
│   ├── Core/              # Framework: Application, Router, View, Database, Controller, Cache, Validator
│   ├── Helpers/           # base_path(), Url helpers, ImageHelper
│   ├── Middleware/        # CSRF, Auth, RateLimit, Permission
│   └── Models/            # 28+ Eloquent-style models
├── bootstrap/             # app.php entry point
├── config/                # app.php, database.php, mail.php, cache.php, permissions.php
├── public/                # Document root: index.php, .htaccess, login.php, assets/
├── resources/
│   └── views/
│       ├── components/    # Reusable UI: sidebar, topbar, footer, stat-card, data-table, badge, alert, empty-state, loading-state, product-search-modal
│       ├── emails/        # Email templates (OTP)
│       ├── errors/        # 403, 404, 500
│       ├── layouts/       # admin.php (main layout)
│       └── pages/         # All page views organized by module
├── routes/
│   ├── web.php            # Web routes (page rendering)
│   └── api.php            # API routes (JSON backend)
├── storage/               # cache, logs, uploads
└── migrate_compress_images.php  # One-time image compression migration
```

### Request Lifecycle

```
Browser → public/.htaccess (rewrite to index.php)
→ public/index.php (error handler → bootstrap)
→ Router::dispatch() (match route → middleware → controller)
→ Controller method (fetch data → View::layout())
→ View::render() (extract shared data → require view file)
→ View echoes HTML + injects page scripts
```

---

## Modules Implementation Status

### Fuel
**Status:** Fully implemented ✅
- Pump dashboard, 3 pump reading pages (open/close/dip)
- Purchase list, create, edit view pages
- Supplier list, create, edit
- Price updates
- Dashboard fuel stats + charts

### Beverages
**Status:** Fully implemented ✅
- `products.php` — Product CRUD with base64 image upload, DataTable, edit/delete modals
- `inventory.php` — Inventory overview with stock value
- `purchases.php` — Paginated purchase list + New Purchase modal with dynamic item rows
- `purchases/{id}` → `purchase_items.php` — Detail page: view items, add/remove with 25-day lock
- `fridge.php` — 3-card layout: Opening, Closing, Sales sessions
- `fridge/opening` → `fridge_opening.php` — Add/remove opening items, product search, 25-day lock
- `fridge/closing` → `fridge_closing.php` — Add/remove closing items, validated vs opening, stock/empties updates
- `kalumu.php` — 4-card layout: Opening, Closing, Empties, Crates sessions
- `kalumu/opening` → `kalumu_opening.php` — Crate/pet qty entry with unit conversion
- `kalumu/closing` → `kalumu_closing.php` — Closing with opening comparison
- `kalumu/empties` → `kalumu_empties.php` — Returned + Pending tabs, validated against closing inventory
- `kalumu/crates` → `kalumu_crates.php` — Sold crate management with empty qty updates
- `empties.php` — Current empties + Coca-Cola returns panels
- `empties/crates` → `empties_crates.php` — Cocacola return detail with truck/date, add/remove items
- Reports: sales, purchases, empties (3 pages with date range + export)
- Charts page
- All sessions have 25-day edit restrictions matching old system

### Engine Fluids
**Status:** Fully implemented ✅
- `sales.php` — Date-filtered sales with New Sale modal, product select auto-fills
- `inventory.php` — Product list with Add/Edit/Delete modals, camera capture + file upload, WebP compression, lazy-load image viewer via API
- `purchases.php` — Paginated purchase list with New Purchase modal
- Reports: sales, purchases, inventory
- Image handling: GD-based WebP compression (max 800px, quality 75), ETag-cached serving endpoint

### Rubis Gas
**Status:** Fully implemented ✅
- `sales.php` — Date-filtered with sale type (Refill/Full), product search modal, empty/fsale tracking
- `inventory.php` — 5-price columns, Add/Edit/Delete, camera capture + upload, WebP compression
- `purchases.php` — Paginated with New Purchase modal
- `cylinders.php` — Full Sale Cylinders + Empty Cylinders tabs, return empties form with qty validation
- Reports: sales, purchases, inventory, empties

### Payroll
**Status:** Fully implemented ✅
- `employees.php` — Add/Edit employee modal with all fields (name, phone, email, ID, KRA, NSSF, NHIF, dept, position, date), toggle active
- `salary.php` — Employee picker + salary form (basic, 4 allowances, 3 deductions, effective_from), salary history, edit/delete
- `advances.php` — Employee picker + amount/date/reason CRUD with running balance
- `penalties.php` — Same pattern as advances
- `payslip.php` — Full payslip print view with signature pad (employee/HR/manager)
- Dashboard: Generate/Approve/Unapprove payroll with summary cards
- Auto-creates expense entry on payroll approval (category: "Salaries & Wages")
- Signature: save, check status, bulk sign (HR/manager)

### Credit & Ledger
**Status:** Fully implemented ✅
- `clients.php` — Client CRUD with pagination, outstanding balance, search
- `invoices.php` — New invoice with dynamic line items (fuel/beverage/gas/fluid/other types), filters, cancel
- `payments.php` — Payment form (client→invoice picker, amount, method, ref), recent payments table
- `outstanding.php` — Summary cards + client list with invoice count and outstanding amount

### Expenses
**Status:** Fully implemented ✅
- `index.php` — Summary cards (today, month, categories), paginated table with edit/delete, category filter, per-page selector
- `categories.php` — Inline add + edit modal + delete with confirmation
- `reports.php` — Date range, pie chart, daily line chart, DataTable with export

### User Profile
**Status:** Fully implemented ✅
- Profile edit, change password, manage users (admin), add user (admin)
- 2-tab edit user modal: User Details + Permissions (categorized by module with clear page labels)

---

## Image System

### ImageHelper (`app/Helpers/ImageHelper.php`)
GD-based utility for product images (fluids, gas):
- `compress($path, $maxWidth=800, $quality=75)` — Resize + convert to WebP
- `compressFileToWebpBase64($file)` — Upload → compressed WebP base64 for DB storage
- `serveImage($base64, $maxAge=86400)` — Binary output with ETag + Cache-Control (304 support)
- `servePlaceholder()` — Lightweight SVG placeholder

### Image Serving Endpoints
- `GET /api/fluids/image/{id}` — Serves cached, ETagged WebP images
- `GET /api/gas/image/{id}` — Same for gas products
- Images are served as binary WebP with 24-hour browser cache, not inline base64
- Server-side file-cache layer (1-hour TTL) avoids repeated DB reads

### Camera Capture
- Both fluids and gas inventory views have Camera button (native `getUserMedia`) + Upload button
- Captured photos auto-compressed to WebP before DB storage
- Image viewer opens in fullscreen modal on click

### Migration Script (`migrate_compress_images.php`)
One-time compression of all existing base64 images:
```
php migrate_compress_images.php                    # All tables (d_fluids, d_gas, d_bev)
php migrate_compress_images.php --table=d_fluids   # Single table
php migrate_compress_images.php --dry-run           # Preview only
php migrate_compress_images.php --quality=70        # Custom quality (default 75)
```
Reports before/after sizes per image and total MB saved. 8MB phone photos → ~50KB WebP.

---

## Cache System

File-based in `storage/cache/` (`App\Core\Cache`). Uses `md5($key).cache` filenames with serialized metadata.

### Invalidation Rules (Updated)

Every create/update/delete calls one of these standard helpers:

| Module | Helper Method | Clears |
|--------|-------------|--------|
| **Fuel** | `invalidateFuelCache()` | `fuel_dashboard_data`, `dashboard_stats`, `dashboard_fuel_sales_*` |
| **Beverages** | `invalidateBevCache()` | `beverage_*`, `inventory_*`, `dashboard_*` |
| **Beverages Fridge/Kalumu** | above + `bev_empty_*`, `report_*` | All beverage patterns + report + empty |
| **Beverages Purchases** | above + `bev_purchase_*` | All beverage + purchase patterns |
| **Fluids** | `invalidateInventoryCache()` | `inventory_*`, `fuel_chart_*`, `report_*`, `dashboard_*` |
| **Gas** | `invalidateInventoryCache()` | Same as fluids |
| **Expenses** | `clearPattern('expense_')` + `report_*` + `dashboard_stats` | All expense + report |
| **Expense Categories** | `clearPattern('expense_')` + `report_*` + `dashboard_stats` | Same (includes report_) |
| **Payroll** | `invalidatePayrollCache()` | `payroll_*`, `report_*`, `dashboard_stats` |
| **Ledger** | `invalidateLedgerCache()` | `ledger_*`, `report_*`, `dashboard_stats` |
| **Users** | `invalidateUser()` | `user_*`, `dashboard_stats` |

### Image Cache
- `fluid_image_{id}` / `gas_image_{id}` — Deleted on product create/update, set on first image serve (3600s TTL)
- Image serve uses ETag + 304 Not Modified for browser cache

---

## Reusable Pagination System

Defined in `resources/views/layouts/admin.php`:

### renderPagination(containerId, currentPage, totalPages, totalItems, perPage)

Renders pagination UI with event delegation. Views using pagination:
- Beverages: purchases, products
- Fluids: purchases
- Gas: purchases
- Expenses: index
- Ledger: clients, invoices, payments

---

## UI Components (resources/views/components/)

| Component | Purpose |
|---|---|
| `sidebar.php` | Navigation with collapsible menus, search, user info |
| `topbar.php` | Mobile hamburger, clock, theme toggle, profile, logout, clear-cache |
| `footer.php` | Copyright bar |
| `stat-card.php` | Metric card with icon, value, label, optional progress |
| `data-table.php` | Table wrapper with search, export, DataTable init |
| `badge.php` | Status badge pill |
| `alert.php` | Alert box with icon |
| `empty-state.php` | Centered empty state with action button |
| `loading-state.php` | Spinning loader |
| `product-search-modal.php` | Shared product browser modal (gas/fridge/kalumu sales) |

---

## Global JS Functions (admin.php)

| Function | Purpose |
|---|---|
| `fmt(v, d)` | Number format with Kenya locale |
| `fmt3(v)` | 3 decimal places |
| `fmtDate(d)` | Y-m-d → d-m-Y |
| `initDataTable(tableId, opts)` | DataTable with Print/Excel/PDF export |
| `renderPagination(container, page, totalPages, total, perPage)` | Pagination UI |
| `showToast(message, type)` | Toast notification (success/error/warning/info) |
| `showConfirm(msg, onConfirm)` | Confirmation modal |

---

## Old vs New System — Key Differences

| Feature | Old System | New System |
|---------|-----------|-----------|
| Architecture | Procedural PHP, no MVC | MVC with Router, Controller, View, Model layers |
| AJAX pattern | `$_POST['request'] = N` switch statements | RESTful routes with HTTP methods |
| Front-end | Bootstrap 4 + jQuery plugins | Tailwind CSS + Alpine.js + jQuery DataTables |
| Admin template | Quixnav | Custom built with Tailwind |
| Sidebar | MetisMenu (jQuery) | Alpine.js x-data collapsible |
| Date picker | Bootstrap datepicker | Flatpickr |
| Icons | Material Design Icons (mdi) | Lucide Icons |
| Cache | File-based with CacheHelper | File-based with Cache class + pattern invalidation |
| Image storage | Raw base64 (8-10MB photos) | Compressed WebP via GD (30-80KB) |
| Image serving | Inline base64 in HTML | Server endpoint with ETag + 24h browser cache |
| Camera capture | Not available | Native `getUserMedia` camera capture |
| Modal system | Bootstrap modals | Custom Tailwind modals |
| Dark mode | Not available | Full dark mode with system preference detection |
| Middleware | None | CSRF, Auth, RateLimit |
| Form validation | Client-side only | Client-side + server-side Validator |
| Error handling | `die()` or inline echo | Structured JSON `{success: false, message}` |
| Transactions | `mysqli::begin_transaction()` in some | Used in purchases, payments, payroll generation |
| 25-day lock | Hardcoded in each file | `isEditable()` + `checkEditable()` helper |
| Orphan cleanup | Manual queries in request=1 | `createPurchase()` + `cocacolaReturn()` auto-cleanup |

---

## Key Implementation Details

### Beverages — Session Workflow
1. Create session for a date (e.g. fridge opening)
2. Add products to session (with qty/price validation)
3. Create closing session (must have opening for same date)
4. Closing: subtract sold qty from d_bev stock, add to d_bev_empty if bev_id ≤ 5
5. Sales = opening_qty - closing_qty per product
6. Profit = (closing_sprice × qty_sold) - (opening_bprice × qty_sold)
7. Kalumu: qty entered in crates/pet, multiplied by packaging qty for DB storage
8. Kalumu sales include empty crate sales from `d_bev_kalumu_scrates_item`

### Gas — Sale Type Logic
- `saletype=1` (Refill) → uses `d_gas_rbprice` and `d_gas_rsprice`
- `saletype=0` (Full) → uses `d_gas_bprice` and `d_gas_sprice`
- For product IDs 1-2 (cylinder types): refill increments `d_gas_empty`, full increments `d_gas_fsale`
- Empty cylinders returned via `/admin/gas/cylinders` decrements `d_gas_empty` and resets `d_gas_fsale`

### Payroll — Generation Logic
1. Get active employees
2. For each: get current salary (effective_from ≤ today ≤ effective_to)
3. Calculate: gross = basic + allowances, mandatory = nssf + nhif + paye
4. Running balance of advances/penalties = SUM(created) - SUM(previously deducted)
5. Deduct advances first, then penalties from available pay
6. On approve: creates expense entry in "Salaries & Wages" category

### Ledger — Payment Flow
1. Record payment (auto-generates PAY-YYYY-NNNN number)
2. If invoice_id provided: lock invoice row FOR UPDATE
3. Validate payment ≤ invoice balance
4. Update invoice: amount_paid += payment, balance -= payment
5. Status = 'paid' if balance ≤ 0, otherwise 'partial'

---

## Standard Implementation Patterns

### Views
- Use `View::layout('admin', 'pages/module/page', ['title' => '...', 'page' => '...'])`
- Pass `page` param matching sidebar expectation
- Page-specific JS via `View::share('_page_scripts', $script)` using nowdoc + str_replace

### FormData Uploads (images)
- Use `FormData` + `processData: false, contentType: false` in jQuery AJAX
- Check `$_FILES['image']` with `UPLOAD_ERR_OK` on server side
- Compress via `ImageHelper::compressFileToWebpBase64($_FILES['image'])`

### Modal Pattern
- Define modal `<div>` with `fixed inset-0 z-50 hidden`
- JS functions `showModal(id)` / `closeModal(id)` toggle `hidden`/`flex` classes
- Backdrop click to close: `onclick="if(event.target===this)closeModal('id')"`

### Flatpickr Date
- Default format: `d-m-Y` (display)
- Convert to `Y-m-d` for API: `parts = dateRaw.split('-'); date = parts[2] + '-' + parts[1] + '-' + parts[0];`

---

## File Inventory (All Page Views)

```
resources/views/pages/
├── beverages/
│   ├── products.php
│   ├── inventory.php
│   ├── purchases.php
│   ├── purchase_items.php          # Purchase detail (add/remove items)
│   ├── fridge.php
│   ├── fridge_opening.php          # Fridge opening items detail
│   ├── fridge_closing.php          # Fridge closing items detail
│   ├── kalumu.php
│   ├── kalumu_opening.php          # Kalumu opening items detail
│   ├── kalumu_closing.php          # Kalumu closing items detail
│   ├── kalumu_empties.php          # Kalumu returned/pending empties
│   ├── kalumu_crates.php           # Kalumu sold crates detail
│   ├── empties.php
│   └── empties_crates.php          # Cocacola return detail
├── fluids/
│   ├── sales.php
│   ├── inventory.php
│   └── purchases.php
├── gas/
│   ├── sales.php
│   ├── inventory.php
│   ├── purchases.php
│   └── cylinders.php               # Full sale + empty cylinders
├── payroll/
│   ├── dashboard.php
│   ├── employees.php
│   ├── salary.php
│   ├── advances.php
│   ├── penalties.php
│   └── payslip.php
├── expenses/
│   ├── index.php
│   ├── categories.php
│   └── reports.php
├── ledger/
│   ├── clients.php
│   ├── invoices.php
│   ├── payments.php
│   └── outstanding.php
├── charts/
│   ├── fuel.php
│   ├── beverages.php
│   ├── fluids.php
│   └── gas.php
├── reports/
│   ├── fuel-sales.php
│   ├── fuel-purchases.php
│   ├── beverage-sales.php
│   ├── beverage-purchases.php
│   ├── beverage-empties.php
│   ├── fluids-sales.php
│   ├── fluids-purchases.php
│   ├── fluids-inventory.php
│   ├── gas-sales.php
│   ├── gas-purchases.php
│   ├── gas-inventory.php
│   ├── gas-empties.php
│   ├── combined.php
│   └── profit-loss.php
├── fuel/
│   ├── dashboard.php
│   ├── pump.php
│   ├── purchases.php
│   ├── purchase-form.php
│   └── suppliers.php
└── profile.php
```
