# Phase 2: Admin Portal Foundation — 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:** Build the admin portal skeleton at `admin/` — folder structure, database schema, config layer, dark-gold theme, shared includes, login/logout, session auth, and a basic dashboard. This is the foundation every later phase builds on.

**Architecture:** Plain PHP, no framework. Each page is a `.php` file. Session-based auth checked at the top of every protected page via `require_once config/auth.php`. PDO for all DB queries. Shared HTML (nav, sidebar, head tags) in `includes/header.php` and `includes/footer.php`. The `admin/` folder lives inside the main repo and maps to `admin.lushawill.com` on the server via cPanel subdomain config.

**Tech Stack:** PHP 7.4+, MySQL 5.7+ / MariaDB 10+, PDO, bcrypt, HTML/CSS/JS.

**Local dev URL:** `http://localhost/lushawillupgrade/admin/`

---

## Chunk 1: Folder Structure + Database Schema

### Task 1: Create the admin folder structure

**Files:**
- Create: `admin/` (directory tree below)

- [ ] **Step 1: Create the full folder structure**

```bash
cd C:/xampp/htdocs/lushawillupgrade

mkdir -p admin/config
mkdir -p admin/includes
mkdir -p admin/assets/css
mkdir -p admin/assets/js
mkdir -p admin/assets/img
mkdir -p admin/modules/clients
mkdir -p admin/modules/inbox
mkdir -p admin/modules/templates
mkdir -p admin/modules/invoices
mkdir -p admin/modules/settings
mkdir -p admin/modules/whatsapp
mkdir -p admin/api
mkdir -p admin/cron
mkdir -p admin/uploads/invoices
mkdir -p admin/uploads/attachments
mkdir -p admin/db
```

- [ ] **Step 2: Create `.gitkeep` files in empty upload dirs so they are tracked by git**

```bash
echo "" > admin/uploads/invoices/.gitkeep
echo "" > admin/uploads/attachments/.gitkeep
```

- [ ] **Step 3: Add `uploads/` to `.gitignore` to avoid committing client files**

Append to the root `.gitignore`:
```
# Admin portal uploads (client files, generated PDFs)
admin/uploads/invoices/*
admin/uploads/attachments/*
!admin/uploads/invoices/.gitkeep
!admin/uploads/attachments/.gitkeep
```

- [ ] **Step 4: Commit skeleton**

```bash
git add admin/ .gitignore
git commit -m "feat: create admin portal folder structure"
```

---

### Task 2: Write the database schema

**Files:**
- Create: `admin/db/schema.sql`

- [ ] **Step 1: Create `admin/db/schema.sql` with the full schema**

```sql
-- LushaWill Couture Admin Portal — Database Schema
-- Run once on the MySQL database created via cPanel

SET FOREIGN_KEY_CHECKS = 0;

-- --------------------------------------------------------
-- users
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS users (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- clients
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS clients (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- orders
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS orders (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- status_history
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS status_history (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- templates
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS templates (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- auto_responses
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS auto_responses (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- invoices
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS invoices (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- invoice_sends
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS invoice_sends (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- messages
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS messages (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- payments (placeholder — activated when gateway chosen)
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS payments (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- --------------------------------------------------------
-- attachments
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS attachments (
  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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

SET FOREIGN_KEY_CHECKS = 1;
```

- [ ] **Step 2: Create the database in XAMPP**

1. Open `http://localhost/phpmyadmin`
2. Click "New" → Database name: `lushawill_admin` → Collation: `utf8mb4_unicode_ci` → Create
3. Select the new database → click "Import" tab
4. Choose file: `admin/db/schema.sql` → click "Go"
5. Verify all 11 tables appear: `users`, `clients`, `orders`, `status_history`, `templates`, `auto_responses`, `invoices`, `invoice_sends`, `messages`, `payments`, `attachments`

- [ ] **Step 3: Insert the two default user accounts**

In phpMyAdmin, run this SQL (SQL tab):

```sql
-- WARNING: These hashes are INVALID PLACEHOLDERS — login will fail until you run
-- admin/db/gen_passwords.php and update these rows (Task 6, Step 3).
-- Do NOT use these placeholders as real passwords.
INSERT INTO users (username, password_hash, role, email) VALUES
('admin', '$2y$12$PLACEHOLDER_CHANGE_ME_superadmin', 'superadmin', 'admin@lushawill.com'),
('Lusha', '$2y$12$PLACEHOLDER_CHANGE_ME_admin',      'admin',      'info@lushawill.com');
```

- [ ] **Step 4: Commit**

```bash
git add admin/db/schema.sql
git commit -m "feat: add full database schema for admin portal"
```

---

## Chunk 2: Config Layer + Security

### Task 3: Create config files

**Files:**
- Create: `admin/config/app.php`
- Create: `admin/config/db.php`
- Create: `admin/config/auth.php`
- Create: `admin/.htaccess`
- Create: `admin/config/.htaccess`
- Create: `admin/cron/.htaccess`
- Create: `admin/db/.htaccess`
- Create: `admin/uploads/attachments/.htaccess`

- [ ] **Step 1: Create `admin/config/app.php`**

```php
<?php
// Application constants
// Update BASE_URL for production: 'https://admin.lushawill.com'
define('APP_NAME',    'LushaWill Admin');
define('BASE_URL',    'http://localhost/lushawillupgrade/admin');
define('UPLOAD_MAX',  10 * 1024 * 1024); // 10 MB
define('SESSION_TTL', 1800); // 30 minutes idle timeout

// Allowed MIME types for client file uploads
define('ALLOWED_MIME_TYPES', [
    'image/jpeg',
    'image/png',
    'image/webp',
    'application/pdf',
]);
```

- [ ] **Step 2: Create `admin/config/db.php`**

```php
<?php
// Database connection — returns a singleton PDO instance
// Update credentials before deploying to production

define('DB_HOST', 'localhost');
define('DB_NAME', 'lushawill_admin');
define('DB_USER', 'root');       // Change for production
define('DB_PASS', '');           // Change for production
define('DB_CHARSET', 'utf8mb4');

function get_db(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
        $options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ];
        $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
    }
    return $pdo;
}
```

- [ ] **Step 3: Create `admin/config/auth.php`**

```php
<?php
// Auth helpers — include at top of every protected page
require_once __DIR__ . '/app.php';

if (session_status() === PHP_SESSION_NONE) {
    session_set_cookie_params([
        'lifetime' => 0,
        'path'     => '/',
        'secure'   => isset($_SERVER['HTTPS']),
        'httponly' => true,
        'samesite' => 'Strict',
    ]);
    session_start();
}

/**
 * Redirect to login if not authenticated.
 * Also enforces idle session timeout.
 */
function require_login(): void {
    if (empty($_SESSION['user_id'])) {
        header('Location: ' . BASE_URL . '/index.php');
        exit;
    }
    // Initialise last_activity defensively if not set
    if (!isset($_SESSION['last_activity'])) {
        $_SESSION['last_activity'] = time();
    }
    // Idle timeout check
    if ((time() - $_SESSION['last_activity']) > SESSION_TTL) {
        session_unset();
        session_destroy();
        header('Location: ' . BASE_URL . '/index.php?timeout=1');
        exit;
    }
    $_SESSION['last_activity'] = time();
}

/**
 * Redirect non-superadmins away from restricted pages.
 */
function require_superadmin(): void {
    require_login();
    if (($_SESSION['role'] ?? '') !== 'superadmin') {
        header('Location: ' . BASE_URL . '/dashboard.php?error=access_denied');
        exit;
    }
}

/**
 * Generate (or retrieve) the CSRF token for this session.
 */
function csrf_token(): string {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

/**
 * Verify CSRF token on POST and AJAX mutation requests.
 * Call at the top of any handler that mutates data.
 */
function verify_csrf(): void {
    $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
        http_response_code(403);
        exit('Invalid CSRF token.');
    }
}

/**
 * Escape output safely.
 */
function e(string $str): string {
    return htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
```

- [ ] **Step 4: Create `admin/.htaccess`**

```apache
# LushaWill Admin Portal — Security Rules

# Force HTTPS (uncomment on production)
# RewriteEngine On
# RewriteCond %{HTTPS} off
# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

# Block direct access to config directory
<FilesMatch "^(db|config|cron)">
  Require all denied
</FilesMatch>

# Block direct access to config/ directory
<If "%{REQUEST_URI} =~ m|/config/|">
  Require all denied
</If>

# Prevent directory listing
Options -Indexes

# Default charset
AddDefaultCharset UTF-8
```

- [ ] **Step 5: Create `.htaccess` files blocking direct access to `config/`, `cron/`, and `db/`**

Create `admin/config/.htaccess`:
```apache
Require all denied
```

Create `admin/cron/.htaccess`:
```apache
Require all denied
```

Create `admin/db/.htaccess`:
```apache
Require all denied
```

- [ ] **Step 6: Create `admin/uploads/attachments/.htaccess`** (disable PHP execution in uploads)

```apache
# Prevent PHP execution in uploads directory
php_flag engine off
Options -ExecCGI
RemoveHandler .php .php3 .php4 .php5 .phtml .cgi
```

- [ ] **Step 7: Write and run a quick connection test**

Create `admin/db/test_connection.php` temporarily:

```php
<?php
require_once __DIR__ . '/../config/db.php';
try {
    $pdo = get_db();
    $stmt = $pdo->query("SHOW TABLES");
    $tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
    echo "Connection OK. Tables found: " . implode(', ', $tables) . PHP_EOL;
} catch (PDOException $e) {
    echo "Connection FAILED: " . $e->getMessage() . PHP_EOL;
}
```

Run via CLI:
```bash
php admin/db/test_connection.php
```

Expected output:
```
Connection OK. Tables found: attachments, auto_responses, clients, invoice_sends, invoices, messages, orders, payments, status_history, templates, users
```

- [ ] **Step 8: Delete the test file**

```bash
del admin/db/test_connection.php
```

- [ ] **Step 9: Commit**

```bash
git add admin/config/ admin/cron/ admin/db/.htaccess admin/.htaccess admin/uploads/
git commit -m "feat: add config layer, auth helpers, and security rules"
```

---

## Chunk 3: Theme + Shared Includes

### Task 4: Create the admin CSS theme

**Files:**
- Create: `admin/assets/css/admin.css`

- [ ] **Step 1: Create `admin/assets/css/admin.css`**

```css
/* =====================================================
   LushaWill Admin — Dark Gold Theme
   ===================================================== */

:root {
  --gold:        #C57642;
  --gold-light:  #d9935e;
  --gold-dim:    rgba(197, 118, 66, 0.18);
  --bg:          #0f0d0b;
  --bg-card:     #181410;
  --bg-sidebar:  #120f0d;
  --bg-input:    #1e1813;
  --border:      rgba(197, 118, 66, 0.18);
  --text:        #e8ddd4;
  --text-muted:  #8a7b6e;
  --danger:      #c0392b;
  --success:     #27ae60;
  --warning:     #e6a817;
  --sidebar-w:   240px;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
  line-height: 1.6;
  min-height: 100vh;
}

a { color: var(--gold); text-decoration: none; }
a:hover { color: var(--gold-light); }

/* ---- Layout ---- */
.orb-admin-wrapper {
  display: flex;
  min-height: 100vh;
}

/* ---- Sidebar ---- */
.orb-sidebar {
  width: var(--sidebar-w);
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0; left: 0; bottom: 0;
  overflow-y: auto;
  z-index: 100;
}

.orb-sidebar-logo {
  padding: 24px 20px 20px;
  border-bottom: 1px solid var(--border);
}

.orb-sidebar-logo img {
  max-width: 120px;
  display: block;
}

.orb-sidebar-logo .orb-site-name {
  font-size: 11px;
  color: var(--text-muted);
  letter-spacing: 2px;
  text-transform: uppercase;
  margin-top: 6px;
}

.orb-nav { padding: 16px 0; flex: 1; }

.orb-nav-section {
  padding: 8px 20px 4px;
  font-size: 10px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--text-muted);
}

.orb-nav a {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 20px;
  color: var(--text-muted);
  font-size: 13px;
  transition: color 0.2s, background 0.2s;
  border-left: 3px solid transparent;
}

.orb-nav a:hover,
.orb-nav a.active {
  color: var(--text);
  background: var(--gold-dim);
  border-left-color: var(--gold);
}

.orb-nav a svg {
  width: 16px; height: 16px;
  fill: currentColor;
  flex-shrink: 0;
}

.orb-sidebar-footer {
  padding: 16px 20px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-muted);
}

.orb-sidebar-footer a {
  color: var(--text-muted);
}

.orb-sidebar-footer a:hover { color: var(--danger); }

/* ---- Main Content ---- */
.orb-main {
  margin-left: var(--sidebar-w);
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* ---- Top Bar ---- */
.orb-topbar {
  padding: 16px 28px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--bg);
  position: sticky;
  top: 0;
  z-index: 50;
}

.orb-topbar h1 {
  font-size: 16px;
  font-weight: 400;
  letter-spacing: 1px;
  color: var(--text);
}

.orb-topbar-right {
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: 13px;
  color: var(--text-muted);
}

/* ---- Page Content ---- */
.orb-content {
  padding: 28px;
  flex: 1;
}

/* ---- Cards ---- */
.orb-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 20px 24px;
  margin-bottom: 20px;
}

.orb-card-title {
  font-size: 11px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 12px;
}

/* ---- Stat Blocks ---- */
.orb-stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}

.orb-stat {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 18px 20px;
}

.orb-stat-label {
  font-size: 10px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 8px;
}

.orb-stat-value {
  font-size: 28px;
  color: var(--gold);
  font-weight: 300;
}

/* ---- Buttons ---- */
.orb-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 18px;
  border-radius: 4px;
  font-size: 13px;
  cursor: pointer;
  border: none;
  transition: opacity 0.2s, background 0.2s;
  text-decoration: none;
  font-family: inherit;
}

.orb-btn-primary {
  background: var(--gold);
  color: #fff;
}

.orb-btn-primary:hover { background: var(--gold-light); color: #fff; }

.orb-btn-outline {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
}

.orb-btn-outline:hover {
  border-color: var(--gold);
  color: var(--gold);
}

.orb-btn-danger {
  background: var(--danger);
  color: #fff;
}

/* ---- Forms ---- */
.orb-form-group { margin-bottom: 16px; }

.orb-form-group label {
  display: block;
  font-size: 11px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 6px;
}

.orb-form-group input,
.orb-form-group select,
.orb-form-group textarea {
  width: 100%;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  padding: 9px 12px;
  font-size: 13px;
  font-family: inherit;
  transition: border-color 0.2s;
  outline: none;
}

.orb-form-group input:focus,
.orb-form-group select:focus,
.orb-form-group textarea:focus {
  border-color: var(--gold);
}

.orb-form-group textarea { resize: vertical; min-height: 90px; }

/* ---- Tables ---- */
.orb-table-wrap { overflow-x: auto; }

table.orb-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.orb-table th {
  text-align: left;
  padding: 10px 14px;
  font-size: 10px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.orb-table td {
  padding: 12px 14px;
  border-bottom: 1px solid rgba(197,118,66,0.08);
  vertical-align: middle;
}

.orb-table tr:hover td { background: var(--gold-dim); }

/* ---- Badges ---- */
.orb-badge {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 20px;
  font-size: 11px;
  letter-spacing: 0.5px;
}

.orb-badge-gold    { background: var(--gold-dim); color: var(--gold); }
.orb-badge-success { background: rgba(39,174,96,0.15); color: var(--success); }
.orb-badge-danger  { background: rgba(192,57,43,0.15); color: var(--danger); }
.orb-badge-muted   { background: rgba(138,123,110,0.15); color: var(--text-muted); }

/* ---- Alerts ---- */
.orb-alert {
  padding: 10px 16px;
  border-radius: 4px;
  font-size: 13px;
  margin-bottom: 16px;
}

.orb-alert-error   { background: rgba(192,57,43,0.15); border: 1px solid rgba(192,57,43,0.3); color: #e87060; }
.orb-alert-success { background: rgba(39,174,96,0.12); border: 1px solid rgba(39,174,96,0.3); color: var(--success); }
.orb-alert-info    { background: var(--gold-dim); border: 1px solid var(--border); color: var(--gold-light); }

/* ---- Login Page ---- */
.orb-login-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
}

.orb-login-box {
  width: 100%;
  max-width: 360px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 40px 36px;
}

.orb-login-logo {
  text-align: center;
  margin-bottom: 8px;
}

.orb-login-logo img { max-width: 100px; }

.orb-login-subtitle {
  text-align: center;
  font-size: 11px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 28px;
}

/* ---- Responsive ---- */
@media (max-width: 768px) {
  .orb-sidebar { transform: translateX(-100%); transition: transform 0.3s; }
  .orb-sidebar.open { transform: translateX(0); }
  .orb-main { margin-left: 0; }
}
```

- [ ] **Step 2: Copy the LushaWill logo into admin assets**

```bash
cp "C:/xampp/htdocs/lushawillupgrade/img/newimg/PNG/Artboard 4mdpi.png" \
   C:/xampp/htdocs/lushawillupgrade/admin/assets/img/logo.png
```

- [ ] **Step 3: Commit**

```bash
git add admin/assets/
git commit -m "feat: add admin portal dark gold CSS theme"
```

---

### Task 5: Create shared includes (header + footer + sidebar)

**Files:**
- Create: `admin/includes/header.php`
- Create: `admin/includes/footer.php`
- Create: `admin/assets/js/admin.js`

- [ ] **Step 1: Create `admin/includes/header.php`**

```php
<?php
// Usage: include at the top of every protected page AFTER require_once + require_login()
// $page_title must be set before including this file.
// $active_nav must be set to the current nav item key (e.g. 'dashboard', 'clients').

if (!defined('BASE_URL')) {
    die('Direct access not permitted.');
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><?= e($page_title ?? 'Admin') ?> — LushaWill Admin</title>
  <link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/admin.css">
</head>
<body>
<div class="orb-admin-wrapper">

  <!-- Sidebar -->
  <aside class="orb-sidebar">
    <div class="orb-sidebar-logo">
      <a href="<?= BASE_URL ?>/dashboard.php">
        <img src="<?= BASE_URL ?>/assets/img/logo.png" alt="LushaWill">
      </a>
      <div class="orb-site-name">Admin Portal</div>
    </div>

    <nav class="orb-nav">
      <div class="orb-nav-section">Main</div>
      <a href="<?= BASE_URL ?>/dashboard.php"
         class="<?= ($active_nav ?? '') === 'dashboard' ? 'active' : '' ?>">
        <svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
        Dashboard
      </a>

      <div class="orb-nav-section">Clients</div>
      <a href="<?= BASE_URL ?>/modules/clients/index.php"
         class="<?= ($active_nav ?? '') === 'clients' ? 'active' : '' ?>">
        <svg viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
        Clients
      </a>

      <div class="orb-nav-section">Messages</div>
      <a href="<?= BASE_URL ?>/modules/inbox/index.php"
         class="<?= ($active_nav ?? '') === 'inbox' ? 'active' : '' ?>">
        <svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
        Inbox
      </a>
      <a href="<?= BASE_URL ?>/modules/templates/index.php"
         class="<?= ($active_nav ?? '') === 'templates' ? 'active' : '' ?>">
        <svg viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
        Templates
      </a>

      <div class="orb-nav-section">Billing</div>
      <a href="<?= BASE_URL ?>/modules/invoices/index.php"
         class="<?= ($active_nav ?? '') === 'invoices' ? 'active' : '' ?>">
        <svg viewBox="0 0 24 24"><path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/></svg>
        Invoices
      </a>

      <?php if (($_SESSION['role'] ?? '') === 'superadmin'): ?>
      <div class="orb-nav-section">System</div>
      <a href="<?= BASE_URL ?>/modules/settings/index.php"
         class="<?= ($active_nav ?? '') === 'settings' ? 'active' : '' ?>">
        <svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
        Settings
      </a>
      <?php endif; ?>
    </nav>

    <div class="orb-sidebar-footer">
      Logged in as <strong><?= e($_SESSION['username'] ?? '') ?></strong><br>
      <a href="<?= BASE_URL ?>/logout.php">Logout</a>
    </div>
  </aside>

  <!-- Main -->
  <main class="orb-main">
    <div class="orb-topbar">
      <h1><?= e($page_title ?? '') ?></h1>
      <div class="orb-topbar-right">
        <span><?= date('D, d M Y') ?></span>
      </div>
    </div>
    <div class="orb-content">
```

- [ ] **Step 2: Create `admin/includes/footer.php`**

```php
    </div><!-- /.orb-content -->
  </main><!-- /.orb-main -->
</div><!-- /.orb-admin-wrapper -->

<script src="<?= BASE_URL ?>/assets/js/admin.js"></script>
</body>
</html>
```

- [ ] **Step 3: Create `admin/assets/js/admin.js`**

```javascript
// LushaWill Admin — General UI scripts

document.addEventListener('DOMContentLoaded', function () {

  // Auto-dismiss alerts after 4 seconds
  document.querySelectorAll('.orb-alert[data-auto-dismiss]').forEach(function (el) {
    setTimeout(function () {
      el.style.transition = 'opacity 0.4s';
      el.style.opacity = '0';
      setTimeout(function () { el.remove(); }, 400);
    }, 4000);
  });

  // Confirm dialogs for destructive actions
  document.querySelectorAll('[data-confirm]').forEach(function (el) {
    el.addEventListener('click', function (e) {
      if (!confirm(el.getAttribute('data-confirm'))) {
        e.preventDefault();
      }
    });
  });

});
```

- [ ] **Step 4: Commit**

```bash
git add admin/includes/ admin/assets/js/
git commit -m "feat: add shared header/footer includes and admin JS"
```

---

## Chunk 4: Login + Auth + Dashboard

### Task 6: Build the login page and authentication

**Files:**
- Create: `admin/index.php`
- Create: `admin/logout.php`

- [ ] **Step 1: Create `admin/index.php`**

```php
<?php
require_once __DIR__ . '/config/app.php';
require_once __DIR__ . '/config/db.php';

if (session_status() === PHP_SESSION_NONE) session_start();

// Already logged in — go to dashboard
if (!empty($_SESSION['user_id'])) {
    header('Location: ' . BASE_URL . '/dashboard.php');
    exit;
}

$error   = '';
$timeout = isset($_GET['timeout']);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Verify CSRF token first
    $submitted_token = $_POST['csrf_token'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'] ?? '', $submitted_token)) {
        $error = 'Invalid request. Please try again.';
    }

    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';

    if (empty($error) && ($username === '' || $password === '')) {
        $error = 'Please enter your username and password.';
    } else {
        $pdo  = get_db();
        $stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? LIMIT 1');
        $stmt->execute([$username]);
        $user = $stmt->fetch();

        // Check lockout
        if ($user && $user['locked_until'] && strtotime($user['locked_until']) > time()) {
            $mins = ceil((strtotime($user['locked_until']) - time()) / 60);
            $error = "Account locked. Try again in {$mins} minute(s).";
        } elseif ($user && password_verify($password, $user['password_hash'])) {
            // Successful login
            session_regenerate_id(true);
            $_SESSION['user_id']       = $user['id'];
            $_SESSION['username']      = $user['username'];
            $_SESSION['role']          = $user['role'];
            $_SESSION['last_activity'] = time();
            $_SESSION['csrf_token']    = bin2hex(random_bytes(32));

            // Reset failed attempts + update last_login
            $pdo->prepare('UPDATE users SET failed_attempts=0, locked_until=NULL, last_login=NOW() WHERE id=?')
                ->execute([$user['id']]);

            header('Location: ' . BASE_URL . '/dashboard.php');
            exit;
        } else {
            // Failed login
            if ($user) {
                $attempts = $user['failed_attempts'] + 1;
                if ($attempts >= 5) {
                    $pdo->prepare('UPDATE users SET failed_attempts=?, locked_until=DATE_ADD(NOW(), INTERVAL 30 MINUTE) WHERE id=?')
                        ->execute([$attempts, $user['id']]);
                    $error = 'Too many failed attempts. Account locked for 30 minutes.';
                } else {
                    $pdo->prepare('UPDATE users SET failed_attempts=? WHERE id=?')
                        ->execute([$attempts, $user['id']]);
                    $error = 'Invalid username or password.';
                }
            } else {
                // Dummy verify to prevent timing-based username enumeration
                password_verify($password, '$2y$12$invaliddummyhashfortimingprotectionXXXXXXXXXXXXXXXXX');
                $error = 'Invalid username or password.';
            }
        }
    }
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Login — LushaWill Admin</title>
  <link rel="stylesheet" href="<?= BASE_URL ?>/assets/css/admin.css">
</head>
<body>
<div class="orb-login-wrap">
  <div class="orb-login-box">
    <div class="orb-login-logo">
      <img src="<?= BASE_URL ?>/assets/img/logo.png" alt="LushaWill">
    </div>
    <p class="orb-login-subtitle">Admin Portal</p>

    <?php if ($timeout): ?>
      <div class="orb-alert orb-alert-info">Session expired. Please log in again.</div>
    <?php endif; ?>

    <?php if ($error): ?>
      <div class="orb-alert orb-alert-error"><?= e($error) ?></div>
    <?php endif; ?>

    <form method="POST" action="">
      <input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
      <div class="orb-form-group">
        <label for="username">Username</label>
        <input type="text" id="username" name="username"
               value="<?= e($_POST['username'] ?? '') ?>"
               autocomplete="username" required autofocus>
      </div>
      <div class="orb-form-group">
        <label for="password">Password</label>
        <input type="password" id="password" name="password"
               autocomplete="current-password" required>
      </div>
      <button type="submit" class="orb-btn orb-btn-primary" style="width:100%;justify-content:center;margin-top:8px;">
        Sign In
      </button>
    </form>
  </div>
</div>
</body>
</html>
```

- [ ] **Step 2: Create `admin/logout.php`**

```php
<?php
require_once __DIR__ . '/config/app.php';

if (session_status() === PHP_SESSION_NONE) session_start();
session_unset();
session_regenerate_id(true);
session_destroy();
header('Location: ' . BASE_URL . '/index.php');
exit;
```

- [ ] **Step 3: Set real passwords for both accounts**

Create a temporary password-hash generator `admin/db/gen_passwords.php`:

```php
<?php
// Run once via CLI to generate bcrypt hashes. Delete after use.
// Usage: php admin/db/gen_passwords.php

$passwords = [
    'Lusha'  => 'EnterLushaPasswordHere',
    'admin'  => 'EnterSuperAdminPasswordHere',
];

foreach ($passwords as $user => $pass) {
    echo $user . ': ' . password_hash($pass, PASSWORD_BCRYPT, ['cost' => 12]) . PHP_EOL;
}
```

Run it:
```bash
php admin/db/gen_passwords.php
```

Copy the output hashes and update the users in phpMyAdmin:
```sql
UPDATE users SET password_hash = '[hash_from_output]' WHERE username = 'Lusha';
UPDATE users SET password_hash = '[hash_from_output]' WHERE username = 'admin';
```

Delete the generator:
```bash
del admin/db/gen_passwords.php
```

- [ ] **Step 4: Test login in browser**

Open `http://localhost/lushawillupgrade/admin/`

Verify:
- Login form appears with LushaWill logo and dark gold theme
- Entering wrong password shows error "Invalid username or password."
- After 5 wrong attempts, account locks with "locked for 30 minutes" message
- Correct credentials for `Lusha` → redirects to `dashboard.php`
- Correct credentials for `admin` → redirects to `dashboard.php`
- Visiting `dashboard.php` without a session → redirects to login

- [ ] **Step 5: Commit**

```bash
git add admin/index.php admin/logout.php
git commit -m "feat: add login/logout with bcrypt auth and lockout protection"
```

---

### Task 7: Build the dashboard

**Files:**
- Create: `admin/dashboard.php`

- [ ] **Step 1: Create `admin/dashboard.php`**

```php
<?php
require_once __DIR__ . '/config/app.php';
require_once __DIR__ . '/config/db.php';
require_once __DIR__ . '/config/auth.php';
require_login();

$page_title = 'Dashboard';
$active_nav = 'dashboard';
$pdo = get_db();

// Stats
$stats = [];

$stats['unread_email']     = $pdo->query("SELECT COUNT(*) FROM messages WHERE channel='email'     AND direction='inbound' AND read_status=0")->fetchColumn();
$stats['unread_whatsapp']  = $pdo->query("SELECT COUNT(*) FROM messages WHERE channel='whatsapp'  AND direction='inbound' AND read_status=0")->fetchColumn();
$stats['unread_instagram'] = $pdo->query("SELECT COUNT(*) FROM messages WHERE channel='instagram' AND direction='inbound' AND read_status=0")->fetchColumn();
$stats['active_orders']    = $pdo->query("SELECT COUNT(*) FROM orders WHERE status NOT IN ('completed','paid')")->fetchColumn();
$stats['total_clients']    = $pdo->query("SELECT COUNT(*) FROM clients WHERE status='active'")->fetchColumn();
$stats['unpaid_invoices']  = $pdo->query("SELECT COUNT(*) FROM invoices WHERE status='sent'")->fetchColumn();

// Recent activity (last 10 messages)
$recent_messages = $pdo->query(
    "SELECT m.*, c.full_name as client_name
     FROM messages m
     LEFT JOIN clients c ON m.client_id = c.id
     ORDER BY m.created_at DESC
     LIMIT 10"
)->fetchAll();

include __DIR__ . '/includes/header.php';
?>

<!-- Stats Grid -->
<div class="orb-stats-grid">
  <div class="orb-stat">
    <div class="orb-stat-label">Unread Email</div>
    <div class="orb-stat-value"><?= (int)$stats['unread_email'] ?></div>
  </div>
  <div class="orb-stat">
    <div class="orb-stat-label">Unread WhatsApp</div>
    <div class="orb-stat-value"><?= (int)$stats['unread_whatsapp'] ?></div>
  </div>
  <div class="orb-stat">
    <div class="orb-stat-label">Unread Instagram</div>
    <div class="orb-stat-value"><?= (int)$stats['unread_instagram'] ?></div>
  </div>
  <div class="orb-stat">
    <div class="orb-stat-label">Active Orders</div>
    <div class="orb-stat-value"><?= (int)$stats['active_orders'] ?></div>
  </div>
  <div class="orb-stat">
    <div class="orb-stat-label">Active Clients</div>
    <div class="orb-stat-value"><?= (int)$stats['total_clients'] ?></div>
  </div>
  <div class="orb-stat">
    <div class="orb-stat-label">Awaiting Payment</div>
    <div class="orb-stat-value"><?= (int)$stats['unpaid_invoices'] ?></div>
  </div>
</div>

<!-- Quick Actions -->
<div class="orb-card" style="margin-bottom:24px;">
  <div class="orb-card-title">Quick Actions</div>
  <div style="display:flex;gap:10px;flex-wrap:wrap;">
    <a href="<?= BASE_URL ?>/modules/clients/create.php" class="orb-btn orb-btn-primary">+ New Client</a>
    <a href="<?= BASE_URL ?>/modules/invoices/create.php" class="orb-btn orb-btn-outline">+ New Invoice</a>
    <a href="<?= BASE_URL ?>/modules/inbox/index.php" class="orb-btn orb-btn-outline">Open Inbox</a>
  </div>
</div>

<!-- Recent Activity -->
<div class="orb-card">
  <div class="orb-card-title">Recent Messages</div>
  <?php if (empty($recent_messages)): ?>
    <p style="color:var(--text-muted);font-size:13px;">No messages yet.</p>
  <?php else: ?>
    <div class="orb-table-wrap">
      <table class="orb-table">
        <thead>
          <tr>
            <th>Client</th>
            <th>Channel</th>
            <th>Direction</th>
            <th>Subject / Preview</th>
            <th>Time</th>
          </tr>
        </thead>
        <tbody>
          <?php foreach ($recent_messages as $msg): ?>
          <tr>
            <td><?= e($msg['client_name'] ?? '—') ?></td>
            <td><span class="orb-badge orb-badge-gold"><?= e($msg['channel']) ?></span></td>
            <td><?= e($msg['direction']) ?></td>
            <td><?= e(mb_substr($msg['subject'] ?: $msg['body'], 0, 60)) ?>…</td>
            <td style="color:var(--text-muted);white-space:nowrap;">
              <?= e(date('d M, H:i', strtotime($msg['created_at']))) ?>
            </td>
          </tr>
          <?php endforeach; ?>
        </tbody>
      </table>
    </div>
  <?php endif; ?>
</div>

<?php include __DIR__ . '/includes/footer.php'; ?>
```

- [ ] **Step 2: Create placeholder pages for modules not yet built** (prevents 404 errors when clicking nav links)

Create `admin/modules/clients/index.php`:
```php
<?php
require_once __DIR__ . '/../../config/app.php';
require_once __DIR__ . '/../../config/auth.php';
require_login();
$page_title = 'Clients'; $active_nav = 'clients';
include __DIR__ . '/../../includes/header.php';
echo '<div class="orb-alert orb-alert-info">Clients module — coming in Phase 3.</div>';
include __DIR__ . '/../../includes/footer.php';
```

Create `admin/modules/clients/create.php`:
```php
<?php
require_once __DIR__ . '/../../config/app.php';
require_once __DIR__ . '/../../config/auth.php';
require_login();
$page_title = 'New Client'; $active_nav = 'clients';
include __DIR__ . '/../../includes/header.php';
echo '<div class="orb-alert orb-alert-info">New Client form — coming in Phase 3.</div>';
include __DIR__ . '/../../includes/footer.php';
```

Create `admin/modules/inbox/index.php`:
```php
<?php
require_once __DIR__ . '/../../config/app.php';
require_once __DIR__ . '/../../config/auth.php';
require_login();
$page_title = 'Inbox'; $active_nav = 'inbox';
include __DIR__ . '/../../includes/header.php';
echo '<div class="orb-alert orb-alert-info">Inbox module — coming in Phase 4.</div>';
include __DIR__ . '/../../includes/footer.php';
```

Create `admin/modules/templates/index.php`:
```php
<?php
require_once __DIR__ . '/../../config/app.php';
require_once __DIR__ . '/../../config/auth.php';
require_login();
$page_title = 'Templates'; $active_nav = 'templates';
include __DIR__ . '/../../includes/header.php';
echo '<div class="orb-alert orb-alert-info">Templates module — coming in Phase 5.</div>';
include __DIR__ . '/../../includes/footer.php';
```

Create `admin/modules/invoices/index.php`:
```php
<?php
require_once __DIR__ . '/../../config/app.php';
require_once __DIR__ . '/../../config/auth.php';
require_login();
$page_title = 'Invoices'; $active_nav = 'invoices';
include __DIR__ . '/../../includes/header.php';
echo '<div class="orb-alert orb-alert-info">Invoices module — coming in Phase 6.</div>';
include __DIR__ . '/../../includes/footer.php';
```

Create `admin/modules/invoices/create.php`:
```php
<?php
require_once __DIR__ . '/../../config/app.php';
require_once __DIR__ . '/../../config/auth.php';
require_login();
$page_title = 'New Invoice'; $active_nav = 'invoices';
include __DIR__ . '/../../includes/header.php';
echo '<div class="orb-alert orb-alert-info">New Invoice form — coming in Phase 6.</div>';
include __DIR__ . '/../../includes/footer.php';
```

Create `admin/modules/settings/index.php`:
```php
<?php
require_once __DIR__ . '/../../config/app.php';
require_once __DIR__ . '/../../config/auth.php';
require_superadmin();
$page_title = 'Settings'; $active_nav = 'settings';
include __DIR__ . '/../../includes/header.php';
echo '<div class="orb-alert orb-alert-info">Settings module — coming in Phase 3.</div>';
include __DIR__ . '/../../includes/footer.php';
```

- [ ] **Step 3: Verify dashboard in browser**

Open `http://localhost/lushawillupgrade/admin/` → log in as `Lusha`:
- Dashboard loads with dark gold theme
- All 6 stat blocks show `0` (no data yet — correct)
- "Recent Messages" shows "No messages yet."
- Quick action buttons are visible
- Sidebar shows correct nav items for `admin` role (no Settings link)

Log out, log in as `admin`:
- Settings link appears in sidebar (superadmin only)
- Clicking Settings shows the "coming in Phase 3" placeholder

- [ ] **Step 4: Commit**

```bash
git add admin/dashboard.php admin/modules/
git commit -m "feat: add dashboard and placeholder module pages"
git push
```

---

## Post-Phase Checklist

- [ ] All 11 database tables created and verified in phpMyAdmin
- [ ] Both user accounts (`Lusha` and `admin`) have real bcrypt passwords
- [ ] Login lockout works after 5 failed attempts
- [ ] Session expires after 30 minutes idle
- [ ] Dashboard loads correctly for both roles
- [ ] Settings nav item only visible to `admin` (superadmin) role
- [ ] All placeholder module pages load without errors
- [ ] `admin/db/gen_passwords.php` deleted
- [ ] Changes committed and pushed to `staging`
- [ ] **Update this plan:** check off completed tasks
- [ ] **Write Phase 3 plan** once this phase is verified: `docs/superpowers/plans/2026-03-28-phase3-client-management.md`
