# Phase 3: Client Management + Settings — Implementation Plan

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Implement the full Clients module (list, create, view profile, edit, orders, status progression) and the Settings module (superadmin-only user management and credentials storage) for the LushaWill Couture admin portal.
**Architecture:** Two independent modules under `admin/modules/` — `clients/` and `settings/` — each composed of list/form/handler PHP files, protected by role-based auth. Settings stores configuration in a new `settings` key-value DB table.
**Tech Stack:** PHP, MySQL, PDO, HTML/CSS

---

## Chunk 1: Database + Settings Infrastructure

### Task 1: Add settings table to database

- [ ] Append the following DDL to `admin/db/schema.sql`:

```sql
CREATE TABLE IF NOT EXISTS settings (
  `key`      VARCHAR(100) PRIMARY KEY,
  `value`    TEXT,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

- [ ] Run the DDL against the live `lushawill_admin` database to create the table.

- [ ] Add the following two helper functions to `admin/config/db.php`:

```php
function get_setting(string $key, string $default = ''): string {
    static $cache = [];
    if (!array_key_exists($key, $cache)) {
        $stmt = get_db()->prepare('SELECT `value` FROM settings WHERE `key` = ?');
        $stmt->execute([$key]);
        $cache[$key] = $stmt->fetchColumn() ?: $default;
    }
    return $cache[$key];
}

function set_setting(string $key, string $value): void {
    get_db()->prepare('INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)')
        ->execute([$key, $value]);
}
```

- [ ] Insert default settings rows into the live DB:

```sql
INSERT IGNORE INTO settings (`key`, `value`) VALUES
('gmail_imap_host',         'imap.gmail.com'),
('gmail_smtp_host',         'smtp.gmail.com'),
('gmail_email',             ''),
('gmail_app_password',      ''),
('whatsapp_api_token',      ''),
('whatsapp_phone_number_id',''),
('whatsapp_verify_token',   ''),
('whatsapp_app_secret',     ''),
('autoresponse_enabled',    '1'),
('multichannel_send',       '1');
```

- [ ] Commit: `feat: add settings key-value table and helper functions`

---

### Task 2: Settings — User Management

Replace `admin/modules/settings/index.php` placeholder with a real page. All files require `require_superadmin()`.

- [ ] **Modify** `admin/modules/settings/index.php` — user list table with columns: id, username, role, email, last_login, failed_attempts, locked_until. Action buttons per row: Edit, Reset Password, Unlock (only shown when locked_until is in the future), Delete (disabled for self). "New User" button at top right. Show `?success=1` flash message when present.

- [ ] **Create** `admin/modules/settings/user-create.php` — form with fields: username (text, required), role (ENUM select matching DB values), email (email input), password (password, required, min 8), confirm password (password, required). Posts to `user-save.php`.

- [ ] **Create** `admin/modules/settings/user-edit.php` — same form as create, pre-filled from DB row identified by GET `id`. No password field — password is changed via a separate reset flow. Posts to `user-save.php?id=X`.

- [ ] **Create** `admin/modules/settings/user-save.php` — POST handler for both create and edit:
  - Call `verify_csrf()` first.
  - On create: validate username is unique, password min 8 chars, passwords match; bcrypt hash at cost 12 (`PASSWORD_BCRYPT`, `['cost' => 12]`); INSERT into `users`.
  - On edit (GET `id` present): validate username unique excluding self; UPDATE `users` (no password change here).
  - On success: redirect to `index.php?success=1`.
  - On validation failure: redirect back with `?error=<message>`.

- [ ] **Create** `admin/modules/settings/user-reset-password.php` — POST handler:
  - Call `verify_csrf()` first.
  - Accept `id`, `password`, `confirm_password`.
  - Validate min 8 chars and match.
  - Bcrypt hash at cost 12.
  - UPDATE `users` SET `password_hash` = ?, `failed_attempts` = 0, `locked_until` = NULL WHERE `id` = ?.
  - Redirect to `index.php?success=1`.

- [ ] **Create** `admin/modules/settings/user-delete.php` — POST handler:
  - Call `verify_csrf()` first.
  - Prevent deleting self (compare id to `$_SESSION['user_id']`).
  - Prevent deleting last superadmin: COUNT(*) of superadmins must be > 1 before proceeding.
  - DELETE FROM `users` WHERE `id` = ?.
  - Redirect to `index.php?success=1`.

- [ ] **Create** `admin/modules/settings/user-unlock.php` — POST handler:
  - Call `verify_csrf()` first.
  - UPDATE `users` SET `failed_attempts` = 0, `locked_until` = NULL WHERE `id` = ?.
  - Redirect to `index.php?success=1`.

- [ ] Commit: `feat: add user management to settings module`

---

### Task 3: Settings — Credentials

- [ ] **Create** `admin/modules/settings/credentials.php` — page protected by `require_superadmin()`:
  - Loads current values for all settings keys via `get_setting()`.
  - Displays a form with labelled text inputs for: `gmail_imap_host`, `gmail_smtp_host`, `gmail_email`, `gmail_app_password`, `whatsapp_api_token`, `whatsapp_phone_number_id`, `whatsapp_verify_token`, `whatsapp_app_secret`.
  - Displays checkboxes (or toggle selects) for: `autoresponse_enabled`, `multichannel_send`.
  - Password/token fields display current value as `****` with a "Show" button that toggles the input type between `password` and `text` via inline JS.
  - CSRF token hidden input included in the form.
  - Shows `?success=1` flash message when present.
  - Posts to `credentials-save.php`.

- [ ] **Create** `admin/modules/settings/credentials-save.php` — POST handler:
  - Call `verify_csrf()` first. Requires `require_superadmin()`.
  - Define an allowlist of accepted keys: `['gmail_imap_host', 'gmail_smtp_host', 'gmail_email', 'gmail_app_password', 'whatsapp_api_token', 'whatsapp_phone_number_id', 'whatsapp_verify_token', 'whatsapp_app_secret', 'autoresponse_enabled', 'multichannel_send']`.
  - Loop over the allowlist; for each key: if `$_POST[$key]` is set and non-empty, call `set_setting($key, $_POST[$key])`. Skip blank submissions to preserve existing values.
  - Redirect to `credentials.php?success=1`.

- [ ] Commit: `feat: add credentials management to settings module`

---

## Chunk 2: Clients Module

### Task 4: Client list page

Replace `admin/modules/clients/index.php` placeholder with real implementation.

- [ ] **Modify** `admin/modules/clients/index.php`:
  - Protect with `require_login()`.
  - Read GET params: `q` (search string), `status` (all/active/inactive, default all), `channel` (all/email/whatsapp/instagram, default all), `page` (int, default 1).
  - Build WHERE clause dynamically:

```php
$where = ['1=1'];
$params = [];
if ($q) {
    $where[] = '(full_name LIKE ? OR email LIKE ? OR phone LIKE ? OR instagram_handle LIKE ?)';
    $params = array_merge($params, ["%$q%", "%$q%", "%$q%", "%$q%"]);
}
if ($status && $status !== 'all') {
    $where[] = 'status = ?';
    $params[] = $status;
}
if ($channel && $channel !== 'all') {
    $where[] = 'preferred_channel = ?';
    $params[] = $channel;
}
```

  - Paginate: 20 per page. COUNT total rows for pagination links.
  - JOIN to count orders per client: `LEFT JOIN orders o ON o.client_id = c.id GROUP BY c.id`.
  - Render table with columns: Name (link to view), Email, Phone, Channel badge, Status badge, Orders (count), Actions (View, Edit buttons).
  - Search box and filter selects (status, channel) above the table; all filters submit as GET.
  - "New Client" button top right linking to `create.php`.
  - Pagination links below table.

- [ ] Commit: `feat: add searchable client list page`

---

### Task 5: Create + Edit client

- [ ] **Replace** `admin/modules/clients/create.php` with real form — protected by `require_login()`:
  - Fields: Full Name (text, required), Email (email), Phone (text), Instagram Handle (text), Country (text), Preferred Channel (select: email/whatsapp/instagram), Status (select: active/inactive, default active), Notes (textarea).
  - Measurements section with text inputs: Bust, Waist, Hips, Height, Other.
  - CSRF hidden input. Posts to `save.php`.
  - Show `?error=X` flash message when present.

- [ ] **Create** `admin/modules/clients/edit.php` — same form as create, pre-filled from DB using GET `id`. Posts to `save.php?id=X`. Protected by `require_login()`.

- [ ] **Create** `admin/modules/clients/save.php` — POST handler for both create and edit:
  - Call `verify_csrf()` first. Requires `require_login()`.
  - Validate: `full_name` is non-empty; redirect back with `?error=full_name_required` otherwise.
  - Encode measurements as JSON: `json_encode(['bust' => $_POST['bust'], 'waist' => $_POST['waist'], 'hips' => $_POST['hips'], 'height' => $_POST['height'], 'other' => $_POST['other']])`.
  - On create (no `id`): INSERT into `clients`; get last insert id.
  - On edit (GET `id`): UPDATE `clients` WHERE `id` = ?.
  - Redirect to `view.php?id=<client_id>` on success.

- [ ] Commit: `feat: add create and edit client forms`

---

### Task 6: Client profile — view page

- [ ] **Create** `admin/modules/clients/view.php` — protected by `require_login()`:
  - Read GET `id`; fetch client row or 404.
  - **Client header card** — display full_name, status badge (active = green, inactive = grey), preferred_channel badge (gold), "Edit Client" quick link.
  - **Contact details card** — email, phone, instagram_handle, country, notes.
  - **Measurements card** — decode `measurements` JSON column; display as a simple 2-column grid: Bust / Waist / Hips / Height / Other. Show "—" for empty values.
  - **Orders section** — SELECT orders WHERE `client_id` = ?; display as a table: Dress Name, status badge (colour-coded by status), Price + Currency, Created date, "View" button linking to `order-view.php?id=X`. "New Order" button linking to `order-create.php?client_id=X`.
  - **Recent messages section** — SELECT last 5 messages WHERE `client_id` = ? ORDER BY created_at DESC; display: channel badge, direction (inbound/outbound), message preview (truncated to 80 chars with `e()`), date. Link "View all messages →" to inbox filtered by client_id.
  - All output escaped with `e()`.

- [ ] Commit: `feat: add client profile view page`

---

### Task 7: Order management on client profile

- [ ] **Create** `admin/modules/clients/order-create.php` — protected by `require_login()`:
  - Accept GET `client_id`; fetch client name for display.
  - Form fields: Dress Name (text, required), Description (textarea), Price (number, step 0.01), Currency (select: USD/GBP/NGN/EUR), Status (select of all 9 order statuses, default `enquiry`).
  - CSRF hidden input. Posts to `order-save.php`.

- [ ] **Create** `admin/modules/clients/order-save.php` — POST handler:
  - Call `verify_csrf()` first. Requires `require_login()`.
  - On create (no `order_id`): INSERT into `orders`; then INSERT into `status_history` (`order_id`, `old_status` = NULL, `new_status` = value, `changed_by` = session user id, `changed_at` = NOW()).
  - On edit (POST `order_id`): UPDATE `orders` fields (no status change here — status is changed via `order-status-update.php`).
  - Redirect to `order-view.php?id=<order_id>`.

- [ ] **Create** `admin/modules/clients/order-view.php` — protected by `require_login()`:
  - Read GET `id`; fetch order row (JOIN clients to get client name).
  - **Order info card** — Dress Name, Description, Price, Currency, client name (link to `view.php?id=client_id`).
  - **Status progression bar** — render 9 status buttons in defined order (e.g. enquiry → consultation → measurement → design → production → fitting → final_fitting → ready → delivered). Highlight current status in gold. Each non-current status renders as a POST form button pointing to `order-status-update.php`. Only the immediately next status should be primary; others are secondary/dimmed.
  - **Status history timeline** — SELECT from `status_history` WHERE `order_id` = ? ORDER BY `changed_at` ASC; display as a table: old_status → new_status, changed_by (username via JOIN), changed_at.
  - **Edit order form** (inline collapsible) — same fields as create (dress_name, description, price, currency). Posts to `order-save.php` with `order_id` hidden.
  - All output escaped with `e()`.

- [ ] **Create** `admin/modules/clients/order-status-update.php` — POST handler:
  - Call `verify_csrf()` first. Requires `require_login()`.
  - Accept POST: `order_id`, `new_status`.
  - Fetch current `status` from `orders` as `old_status`.
  - INSERT into `status_history` (`order_id`, `old_status`, `new_status`, `changed_by`, `changed_at`).
  - UPDATE `orders` SET `status` = ?, `updated_at` = NOW() WHERE `id` = ?.
  - Redirect to `order-view.php?id=<order_id>`.

- [ ] Commit: `feat: add order creation and status progression`

---

## General coding rules (apply to all tasks)

- Every protected page begins with:
  ```php
  require_once __DIR__ . '/../../config/app.php';   // adjust depth as needed
  require_once __DIR__ . '/../../config/auth.php';
  require_login();   // or require_superadmin() for settings pages
  ```
- Every POST handler begins with `verify_csrf()` before any DB writes.
- All user-supplied values output to HTML are escaped via `e()`.
- All DB queries use PDO prepared statements obtained via `get_db()`.
- Flash messages: pass `?success=1` or `?error=<slug>` on redirect; display a dismissible notice at the top of the receiving page.
- No inline `style=""` attributes; use existing component classes from `admin/assets/css/admin.css`.
- BASE_URL for link construction: `http://localhost/lushawillupgrade/admin` (defined in `admin/config/app.php`).
