# LushaWill Couture — Admin Portal Design Spec
**Date:** 2026-03-28
**Status:** Approved by client
**Stack:** PHP + MySQL (shared cPanel hosting)
**Portal URL:** admin.lushawill.com

---

## 1. Overview

A private admin portal for LushaWill Couture to manage clients, communications, invoices, and orders — all in one place. The portal sits on a subdomain (`admin.lushawill.com`) separate from the main website (`lushawill.com`).

The main website also receives two floating contact icons (WhatsApp + email) on every page.

---

## 2. Tech Stack

| Layer | Technology |
|---|---|
| Backend | PHP (plain, no framework) |
| Database | MySQL |
| Email send/receive | PHPMailer + Gmail IMAP/SMTP |
| WhatsApp | Meta WhatsApp Cloud API (free tier — 1,000 conversations/month) |
| Instagram | Manual log (no API — direct link to Instagram DMs) |
| PDF generation | TCPDF (not FPDF — required for UTF-8 support, multi-currency symbols, international client names) |
| Frontend | HTML/CSS/JS — dark theme with LushaWill gold (#C57642) accents |
| Auth | PHP sessions with session_regenerate_id(true) on login |
| Hosting | Shared cPanel hosting |

---

## 3. Folder Structure

```
admin.lushawill.com/
├── index.php                  — login page
├── dashboard.php              — home after login
├── .htaccess                  — security & routing rules
├── config/
│   ├── db.php                 — MySQL connection
│   ├── auth.php               — session & role helpers
│   └── mailer.php             — Gmail IMAP/SMTP config
├── modules/
│   ├── clients/               — client profiles & order tracking
│   ├── inbox/                 — unified messages (email + WhatsApp + Instagram log)
│   ├── templates/             — saved reply templates
│   ├── invoices/              — invoice builder & PDF sender
│   ├── whatsapp/              — WhatsApp Cloud API handler
│   └── settings/              — user management, credentials, auto-responses
├── assets/
│   ├── css/                   — dark gold theme
│   ├── js/
│   └── img/
├── api/                       — internal AJAX endpoints
│   ├── send-email.php
│   ├── send-whatsapp.php
│   └── webhook.php            — receives incoming WhatsApp messages (must remain publicly accessible to Meta)
├── cron/
│   └── fetch-email.php        — IMAP polling script (run via cPanel cron every 5 minutes)
└── uploads/
    ├── invoices/              — generated PDFs
    └── attachments/           — client photos & files (randomised filenames, no execution)
```

---

## 4. User Roles

| Role | Username | ENUM value | Access |
|---|---|---|---|
| Admin (day-to-day) | `Lusha` | `admin` | Clients, inbox, invoices, templates, auto-responses |
| Super Admin | `admin` | `superadmin` | Everything above + settings, user management, credentials |

- Both login at `admin.lushawill.com`
- Super admin can reset Lusha's password and create additional admin accounts
- Settings module is `superadmin` only
- Login locks after 5 failed attempts; lock auto-expires after 30 minutes
- Super admin can also manually unlock accounts from Settings

**Note on ENUM values:** `role = 'admin'` = the "Lusha" day-to-day admin. `role = 'superadmin'` = the "admin" super admin. These are storage values only — the UI always displays the friendly role name.

---

## 5. Database Schema

### users
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('admin', 'superadmin') NOT NULL,
email VARCHAR(255),
failed_attempts INT DEFAULT 0,
locked_until DATETIME NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME NULL
```

### clients
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
full_name VARCHAR(255) NOT NULL,
email VARCHAR(255),
phone VARCHAR(50),
instagram_handle VARCHAR(100),
country VARCHAR(100),
measurements JSON,
notes TEXT,
preferred_channel ENUM('email', 'whatsapp', 'instagram'),
status ENUM('active', 'inactive') DEFAULT 'active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
```

### orders
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
client_id INT NOT NULL,
dress_name VARCHAR(255),
description TEXT,
price DECIMAL(10,2),
currency ENUM('GBP', 'USD', 'EUR', 'NGN'),
status ENUM('enquiry','responded','in_progress','fitting_scheduled',
            'fitting_done','final_adjustments','ready','completed','paid')
       DEFAULT 'enquiry',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT
```

### status_history
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
old_status ENUM('enquiry','responded','in_progress','fitting_scheduled',
                'fitting_done','final_adjustments','ready','completed','paid') NULL,
new_status ENUM('enquiry','responded','in_progress','fitting_scheduled',
                'fitting_done','final_adjustments','ready','completed','paid') NOT NULL,
changed_by INT NOT NULL,
changed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (changed_by) REFERENCES users(id) ON DELETE RESTRICT
```
*(ENUM mirrors `orders.status` exactly — schema-level enforcement, not just application-layer validation.)*

### messages
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
client_id INT NULL,
order_id INT NULL,
invoice_id INT NULL,
channel ENUM('email', 'whatsapp', 'instagram') NOT NULL,
direction ENUM('inbound', 'outbound') NOT NULL,
subject VARCHAR(255),
body TEXT,
read_status BOOLEAN DEFAULT FALSE,
whatsapp_message_id VARCHAR(255) NULL,
sent_at DATETIME NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE SET NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL,
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE SET NULL
```
*(`client_id` is nullable — an inbound WhatsApp message from an unknown number is a valid state. The Inbox surfaces unlinked messages with an "Assign to Client" action. Once assigned, `client_id` is updated.)*

### templates
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
subject VARCHAR(255),
body TEXT NOT NULL,
channel ENUM('email', 'whatsapp', 'both') NOT NULL,
created_by INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
```

### auto_responses
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
trigger_event ENUM('new_message', 'status_change', 'invoice_sent') NOT NULL,
trigger_value VARCHAR(100) NULL,
channel ENUM('email', 'whatsapp', 'both') NOT NULL,
template_id INT NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (template_id) REFERENCES templates(id) ON DELETE RESTRICT
```

### invoices
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
client_id INT NOT NULL,
order_id INT NULL,
invoice_number VARCHAR(50) UNIQUE NOT NULL,
line_items JSON NOT NULL,
subtotal DECIMAL(10,2) NOT NULL,
discount DECIMAL(10,2) DEFAULT 0,
total DECIMAL(10,2) NOT NULL,
currency ENUM('GBP', 'USD', 'EUR', 'NGN') NOT NULL,
status ENUM('draft', 'sent', 'paid') DEFAULT 'draft',
pdf_path VARCHAR(255),
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL
```

### invoice_sends
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
invoice_id INT NOT NULL,
channel ENUM('email', 'whatsapp', 'instagram') NOT NULL,
sent_by INT NULL,
sent_at DATETIME DEFAULT CURRENT_TIMESTAMP,
status ENUM('sent', 'failed') DEFAULT 'sent',
notes TEXT,
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE,
FOREIGN KEY (sent_by) REFERENCES users(id) ON DELETE SET NULL
```
*(Replaces a single `sent_via` column — allows recording multiple send events per invoice across different channels. `notes` is TEXT to accommodate verbose API error responses.)*

### payments
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
invoice_id INT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
currency ENUM('GBP', 'USD', 'EUR', 'NGN') NOT NULL,
method VARCHAR(100),
gateway VARCHAR(100),
status ENUM('pending', 'completed', 'failed') DEFAULT 'pending',
transaction_ref VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE RESTRICT
```
*(`currency` must match the parent `invoice.currency` — enforced at application layer before insert.)*

### attachments
```sql
id INT AUTO_INCREMENT PRIMARY KEY,
client_id INT NOT NULL,
order_id INT NULL,
file_path VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
uploaded_by INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
```

---

## 6. Client Order Status Flow

```
Enquiry → Responded → In Progress → Fitting Scheduled
→ Fitting Done → Final Adjustments → Ready → Completed → Paid
```

- Each status is a clickable button on the client profile
- Advancing status can trigger an auto-response message to the client
- Every status change is recorded in the `status_history` table with timestamp and user
- Status history is visible on the client profile as a timeline

---

## 7. Portal Modules

### 7.1 Login Page
- Centered form with LushaWill logo
- Dark gold theme
- PHP session authentication with `session_regenerate_id(true)` called immediately after successful login
- Failed attempt counter stored in `users.failed_attempts`; account locked for 30 minutes after 5 failures (`locked_until` set to `NOW() + 30 minutes`)
- Super admin can manually unlock accounts in Settings

### 7.2 Dashboard
- Unread message counts by channel (Email / WhatsApp / Instagram)
- Active orders grouped by status
- Recent activity feed (messages, status changes, invoices sent)
- Quick action buttons: New Client, New Invoice, Compose Message

### 7.3 Clients Module
- Searchable, filterable client list (by status, country, channel)
- Individual client profile:
  - Contact details + measurements
  - Order history with status timeline (from `status_history`)
  - Full message thread (all channels combined, ordered by date)
  - Status progress bar with click-to-advance buttons
  - Internal notes (not visible to client)
  - Photo/file attachments (listed from `attachments` table, linked to client/order)

### 7.4 Unified Inbox
- Three tabs: **Email | WhatsApp | Instagram**
- **Email tab:** Reads from Gmail via IMAP — new emails fetched by cron job every 5 minutes via `cron/fetch-email.php`; stored in `messages` table; reply sent via SMTP from portal
- **WhatsApp tab:** Incoming messages arrive automatically via Meta webhook (`api/webhook.php`) — stored in `messages` table; auto-response triggered if matching rule exists; reply sent via Meta API
- **Instagram tab:** Manual log — admin pastes message content; portal records it in `messages` table; a button links directly to Instagram DMs
- Any message can be linked to a client profile
- One-click template insertion when composing replies

### 7.5 Templates Module
- Create, edit, delete reply templates
- Tag by channel: Email / WhatsApp / Both
- Placeholder variables: `{client_name}`, `{dress_name}`, `{price}`, `{fitting_date}`, `{invoice_number}`
- Templates selectable from inbox when composing

### 7.6 Auto-Responses Module
- Rule-based triggers:
  - New inbound message (any channel) → send template
  - Order status change to specific status → send template
  - Invoice sent → send template
- Each rule: toggle on/off, select channel, select template
- **Channel resolution:** `both` means email + WhatsApp only — Instagram auto-responses are not possible (no outbound API). If a rule is set to `both` and the client's only known channel is Instagram, the rule is skipped and a log entry is written. If client has a `preferred_channel` of email or WhatsApp, that channel is sent first; the second channel is also sent unless the admin has disabled multi-channel sends in Settings.
- Preview mode before activating

### 7.7 Invoice Module
- Build invoice: client name, dress details, line items, discount, total, currency (GBP / USD / EUR / NGN)
- Preview before generating
- Generate PDF using TCPDF (UTF-8 support for all currencies and international characters); stored in `uploads/invoices/` with randomised filename
- Send options:
  - **Email** — sent directly from portal via SMTP; logged in `invoice_sends`
  - **WhatsApp** — PDF sent via WhatsApp Cloud API; logged in `invoice_sends`
  - **Instagram** — download link generated for manual sharing; logged in `invoice_sends`
- Invoice can be sent via multiple channels; each send is recorded separately in `invoice_sends`
- Invoice status: Draft → Sent → Paid

### 7.8 Payment Gateway (Placeholder)
- Dedicated settings panel with configuration slots for: Stripe, PayPal, Flutterwave, Paystack
- All slots inactive until gateway is chosen and configured
- When active: invoices include a "Pay Now" link for clients
- `payments` table is present and ready — no schema changes needed when gateway is activated

### 7.9 Settings (Super Admin `superadmin` role only)
- **User management:** create, edit, delete admin accounts; reset passwords; manually unlock locked accounts
- **Gmail credentials:** IMAP host, SMTP host, email address, app password (not main Gmail password)
- **WhatsApp Business:** API token, Phone Number ID, webhook verification token, app secret (for HMAC signature verification)
- **Business WhatsApp number:** used in floating icons on main site
- **Auto-response:** global on/off toggle; per-rule management
- **Multi-channel send:** toggle whether `both` rules send to all channels or preferred channel only
- **System log:** recent activity log (logins, errors, webhook calls)

---

## 8. Main Website Changes

### Floating Contact Icons
- Added to all 18 HTML pages
- **Placement:** Icons are inserted OUTSIDE the `#orb-dynamic-content` div and outside the `orb-wrapper` div, directly inside `<body>` — this ensures they persist across Swup page transitions without re-rendering or flashing
- WhatsApp icon — bottom-left, fixed position, 42px
- Email icon — bottom-right, fixed position, 42px
- WhatsApp opens `https://wa.me/[number]?text=Hi%20LushaWill...`
- Email opens `contact-2.html` or `mailto:info@lushawill.com`
- Styled with LushaWill gold (#C57642) on hover
- `data-no-swup` attribute added to both icon links to prevent Swup from intercepting them

---

## 9. WhatsApp Cloud API Integration

- **Incoming:** Meta sends POST to `api/webhook.php`
  - Webhook endpoint must remain publicly accessible (Meta's servers POST to it)
  - Every incoming request must be verified using HMAC-SHA256 signature: compare `X-Hub-Signature-256` header against `hash_hmac('sha256', $rawBody, $appSecret)`; reject any request that fails verification
  - Verified message stored in `messages` table
  - Auto-response rule checked and triggered if applicable
- **Outgoing:** Portal calls Meta Graph API to send text or PDF document to client
- **Webhook verification (GET):** Meta sends a one-time GET request with `hub.verify_token`; webhook responds with `hub.challenge` to confirm ownership
- **`.htaccess` note:** `api/` directory is NOT blocked from external access — `webhook.php` must be reachable by Meta. All other files in `api/` are AJAX endpoints called only by the portal's own JS (no sensitive data exposed). `config/` and `uploads/` remain blocked.
- **Free tier:** 1,000 conversations/month (covers 500–1,000 client conversations)
- **Setup required:** Meta Business account, WhatsApp Business number, verified webhook URL (HTTPS required)

---

## 10. Email IMAP Polling (Cron Job)

- `cron/fetch-email.php` connects to Gmail via IMAP, fetches new emails, stores them in `messages` table
- Must be configured as a cPanel cron job: runs every 5 minutes
- Cron command: `php /home/[cpanel_user]/admin.lushawill.com/cron/fetch-email.php`
- Script marks fetched emails as read in Gmail to avoid re-importing
- New emails trigger auto-response rules if applicable

---

## 11. Security

- All portal pages check `$_SESSION['user_id']` on load — redirect to login if not authenticated
- Role-gated pages additionally check `$_SESSION['role'] === 'superadmin'`
- `session_regenerate_id(true)` called immediately after successful login (session fixation protection)
- Passwords stored as bcrypt hashes (`password_hash($pass, PASSWORD_BCRYPT)`)
- Login lockout: 5 failed attempts → `locked_until = NOW() + 30 min`; check `locked_until` before processing login
- CSRF tokens on all forms AND all AJAX mutation endpoints (`api/send-email.php`, `api/send-whatsapp.php`, etc.) — token is per-session, stored in `$_SESSION['csrf_token']`, verified on every POST and AJAX request that mutates data
- All user input sanitised using `htmlspecialchars()` and PDO prepared statements — `FILTER_SANITIZE_STRING` must NOT be used (deprecated PHP 8.1, removed PHP 8.2)
- Gmail app password used (not main Gmail password; 2FA must be enabled on Gmail account)
- HTTPS required — SSL certificate via cPanel Let's Encrypt (required for WhatsApp webhook)
- **Session timeout:** idle sessions expire after 30 minutes — enforced by storing `$_SESSION['last_activity']` and comparing on each page load; expired sessions are destroyed and user is redirected to login
- **`.htaccess` blocking:** `config/` and `uploads/` blocked from direct browser access. `api/` directory is open but individual scripts validate input strictly
- **File uploads:** server-side MIME type validation (using `finfo_file()`, not just file extension); maximum upload size 10MB; uploaded files stored with non-guessable randomised filename using `bin2hex(random_bytes(16))` (cryptographically random, not `uniqid()`); `uploads/attachments/` has `.htaccess` rule preventing PHP execution (`php_flag engine off`)
- WhatsApp webhook: HMAC-SHA256 signature verified on every POST request before processing

---

## 12. What's Out of Scope (for now)

- Instagram API integration (manual log only)
- Payment gateway activation (placeholder only)
- Mobile app
- Client-facing portal (clients cannot login — portal is internal only)
- Multi-language support

---

## 13. Future Additions (noted by client)

- Additional modules TBD — portal architecture supports new modules in `modules/` directory
- Appointment/fitting scheduler
- Catalogue management (add/edit dresses from portal without touching code)
- WhatsApp broadcast messaging
- Analytics dashboard
- Full payment gateway activation
