# ISP360 UI/UX Specifications

**Version:** 1.0  
**Date:** 2026-06-13  
**Prepared by:** Polakis Technologies  
**Pages specified:** 164

Companion documents: [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) | [DATABASE_DESIGN.md](DATABASE_DESIGN.md)

---

## Global UI Standards

### Technology
- **CSS:** Bootstrap 5.3
- **JS:** jQuery 3.7, AJAX for all form submissions
- **Tables:** DataTables 1.13+ with server-side processing on all list pages
- **Charts:** Chart.js 4.x on dashboard and report pages
- **Icons:** Bootstrap Icons

### Layout Types
| Layout | Used by | Files |
|--------|---------|-------|
| `main` | Admin panel | `layouts/header.php`, `sidebar.php`, `footer.php` |
| `auth` | Login/password | `layouts/auth_header.php`, `auth_footer.php` |
| `portal` | Customer portal | `layouts/portal_header.php`, `portal_sidebar.php` |

### Standard Feedback Patterns
| Scenario | Pattern |
|----------|---------|
| Form save success | Bootstrap toast (top-right), green, 4s auto-dismiss |
| Validation error | Inline `.is-invalid` on fields + `.invalid-feedback` |
| AJAX failure | Toast red + retry button for transient errors |
| Permission denied | Redirect `access_denied.php` with message |
| Session expired | Redirect login with `?expired=1` |
| Confirm destructive action | Bootstrap modal with explicit Confirm/Cancel |

### Permission Check Pattern
```php
require_once('includes/config.php');
require_once('includes/auth.php');
require_once('layouts/session.php');

if (!permission_check('module.action')) {
    header('Location: access_denied.php');
    exit;
}
```

---

## Module Specifications

### Auth

#### `change_password.php`

**Module:** Auth  
**Purpose:** Change password form  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `users`  
**AJAX:** `ajax/change_password.php`

**Permission gate:**
```php
requireLogin();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `forgot_password.php`

**Module:** Auth  
**Purpose:** Password recovery request  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `auth`  
**Tables:** `password_reset_tokens`  
**AJAX:** `ajax/send_password_reset.php`

**Permission gate:**
```php
// Public access
// File: forgot_password.php
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `index.php`

**Module:** Auth  
**Purpose:** Landing redirect to login or dashboard  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** —  
**AJAX:** —

**Permission gate:**
```php
// Public access
// File: index.php
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

_No UI buttons — handler/redirect page._


**Validation — Client:** N/A — server-side only

**Validation — Server:** Process and redirect; no HTML output

**Success workflow:** HTTP redirect

**Error workflow:** Redirect with flash error

**UI components:** None — backend handler

---

#### `login.php`

**Module:** Auth  
**Purpose:** Staff login form  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `auth`  
**Tables:** `users`, `login_history`  
**AJAX:** `ajax/login_attempt.php`

**Permission gate:**
```php
// Public page — redirect if already logged in
if (isLoggedIn()) { header('Location: dashboard.php'); exit; }
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `username` | text | Yes | — | 3-64 chars, alphanumeric + underscore |
| `password` | password | Yes | — | min 8 chars |
| `remember_me` | checkbox | No | 0 | Extend session 30 days |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Login | `primary` | POST login_process.php | Validate and authenticate |
| Forgot Password? | `link` | forgot_password.php | — |


**Validation — Client:** username required; password required; disable submit while posting

**Validation — Server:** Verify credentials; check user.status=active; log login_history; rate-limit 5 attempts/15min per IP

**Success workflow:** Redirect to dashboard.php or intended URL

**Error workflow:** Show inline alert: invalid credentials, account locked, or pending activation

**UI components:** Tenant logo from tenant_themes; optional MFA prompt modal

---

#### `login_process.php`

**Module:** Auth  
**Purpose:** Staff login handler  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `users`, `sessions`, `login_history`  
**AJAX:** —

**Permission gate:**
```php
// Public access
// File: login_process.php
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

_No UI buttons — handler/redirect page._


**Validation — Client:** N/A — server-side only

**Validation — Server:** Process and redirect; no HTML output

**Success workflow:** HTTP redirect

**Error workflow:** Redirect with flash error

**UI components:** None — backend handler

---

#### `logout.php`

**Module:** Auth  
**Purpose:** Session termination  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `sessions`  
**AJAX:** —

**Permission gate:**
```php
requireLogin();
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

_No UI buttons — handler/redirect page._


**Validation — Client:** N/A — server-side only

**Validation — Server:** Process and redirect; no HTML output

**Success workflow:** HTTP redirect

**Error workflow:** Redirect with flash error

**UI components:** None — backend handler

---

#### `profile.php`

**Module:** Auth  
**Purpose:** User profile view/edit  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `users`, `user_preferences`  
**AJAX:** `ajax/save_profile.php`, `ajax/change_password.php`

**Permission gate:**
```php
requireLogin();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `reset_password.php`

**Module:** Auth  
**Purpose:** Password reset handler  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `password_reset_tokens`, `users`  
**AJAX:** —

**Permission gate:**
```php
// Public access
// File: reset_password.php
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

_No UI buttons — handler/redirect page._


**Validation — Client:** N/A — server-side only

**Validation — Server:** Process and redirect; no HTML output

**Success workflow:** HTTP redirect

**Error workflow:** Redirect with flash error

**UI components:** None — backend handler

---

#### `reset_password_page.php`

**Module:** Auth  
**Purpose:** Password reset form  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `auth`  
**Tables:** `password_reset_tokens`  
**AJAX:** —

**Permission gate:**
```php
// Public access
// File: reset_password_page.php
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

### Dashboard

#### `dashboard.php`

**Module:** Dashboard  
**Purpose:** Main KPI dashboard  
**Roles:** All staff  
**Layout:** `main`  
**Tables:** `vw_tenant_dashboard_metrics`, `dashboard_metrics`  
**AJAX:** `ajax/dashboard_metrics.php`, `ajax/dashboard_charts.php`

**Permission gate:**
```php
requireLogin();
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Refresh | `secondary` | AJAX | Reload metrics |
| Customize Layout | `secondary` | modal | Save widget layout |


**Validation — Client:** N/A

**Validation — Server:** Tenant-scoped metrics from vw_tenant_dashboard_metrics

**Success workflow:** Charts render with cached data

**Error workflow:** Show stale data badge if cache > 15 min old

**UI components:** Chart.js: revenue trend (line), active sessions (bar), package sales (doughnut); Counter cards: customers, routers, revenue MTD, open tickets

---

### Tenants

#### `tenant_billing.php`

**Module:** Tenants  
**Purpose:** SaaS billing config  
**Roles:** Super Admin  
**Layout:** `main`  
**Tables:** `tenant_billing`, `tenant_subscriptions`  
**AJAX:** `ajax/save_tenant_billing.php`

**Permission gate:**
```php
permission_check('tenants.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `tenant_domains.php`

**Module:** Tenants  
**Purpose:** Domain management  
**Roles:** Super Admin  
**Layout:** `main`  
**Tables:** `tenant_domains`  
**AJAX:** `ajax/save_tenant_domain.php`, `ajax/verify_domain.php`

**Permission gate:**
```php
permission_check('tenants.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search tenant_domains |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | tenant_domain_edit.php | tenants.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `tenant_edit.php`

**Module:** Tenants  
**Purpose:** Create/edit tenant  
**Roles:** Super Admin  
**Layout:** `main`  
**Tables:** `tenants`, `tenant_billing`, `tenant_settings`  
**AJAX:** `ajax/save_tenant.php`, `ajax/provision_tenant.php`

**Permission gate:**
```php
role_check('super_admin');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `tenant_code` | text | Yes | — | Unique globally, lowercase |
| `company_name` | text | Yes | — | — |
| `contact_email` | email | Yes | — | — |
| `contact_phone` | tel | No | — | — |
| `country_id` | select | Yes | — | countries |
| `currency_id` | select | Yes | — | currencies |
| `timezone_id` | select | Yes | — | timezones |
| `status` | select | Yes | pending | — |
| `max_customers` | number | No | 100000 | — |
| `max_routers` | number | No | 500 | — |
| `admin_username` | text | Yes | — | Create flow only |
| `admin_password` | password | Yes | — | Create flow only |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_tenant.php | — |
| Provision | `success` | AJAX provision_tenant.php | Calls sp_provision_tenant |
| Cancel | `link` | tenants.php | — |


**Validation — Client:** tenant_code slug format; password strength on create

**Validation — Server:** sp_provision_tenant procedure; seed tenant_settings defaults

**Success workflow:** Redirect tenant_view.php?id={id}

**Error workflow:** Duplicate tenant_code

**UI components:** Wizard steps on create: Details → Admin User → Modules

---

#### `tenant_modules.php`

**Module:** Tenants  
**Purpose:** Module toggles  
**Roles:** Super Admin  
**Layout:** `main`  
**Tables:** `tenant_modules`, `system_modules`  
**AJAX:** `ajax/toggle_tenant_module.php`

**Permission gate:**
```php
permission_check('tenants.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search tenant_modules |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | tenant_module_edit.php | tenants.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `tenant_view.php`

**Module:** Tenants  
**Purpose:** Tenant detail  
**Roles:** Super Admin  
**Layout:** `main`  
**Tables:** `tenants`, `tenant_domains`, `tenant_modules`  
**AJAX:** `ajax/get_tenant_details.php`

**Permission gate:**
```php
role_check('super_admin');
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Edit | `primary` | tenant_edit.php | edit permission |
| Back | `link` | tenants.php | — |


**Validation — Client:** N/A

**Validation — Server:** Load record by ID + tenant_id; 404 if not found

**Success workflow:** Render detail panels

**Error workflow:** Redirect to list with flash message

**UI components:** Info cards; Related records tabs; Activity timeline

---

#### `tenants.php`

**Module:** Tenants  
**Purpose:** Tenant list  
**Roles:** Super Admin  
**Layout:** `main`  
**Tables:** `tenants`  
**AJAX:** `ajax/get_tenants.php`, `ajax/delete_tenant.php`

**Permission gate:**
```php
role_check('super_admin');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Company name, tenant_code |
| `status_filter` | select | No | all | trial|active|suspended|cancelled |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add Tenant | `primary` | tenant_edit.php | tenants.create |


**Validation — Client:** N/A

**Validation — Server:** Platform-level query — no tenant_id filter

**Success workflow:** DataTable

**Error workflow:** Toast

**UI components:** DataTables: tenant_code, company_name, status, customers count, routers count, created_at

---

### Users & RBAC

#### `api_tokens.php`

**Module:** Users & RBAC  
**Purpose:** API token management  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `api_tokens`  
**AJAX:** `ajax/create_api_token.php`, `ajax/revoke_api_token.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search api_tokens |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | api_token_edit.php | settings.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `audit_logs.php`

**Module:** Users & RBAC  
**Purpose:** Audit trail  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `audit_logs`  
**AJAX:** `ajax/get_audit_logs.php`

**Permission gate:**
```php
permission_check('settings.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search audit_logs |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | audit_log_edit.php | settings.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `login_history.php`

**Module:** Users & RBAC  
**Purpose:** Login history  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `login_history`  
**AJAX:** `ajax/get_login_history.php`

**Permission gate:**
```php
permission_check('users.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search login_history |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | login_history_edit.php | users.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `permissions.php`

**Module:** Users & RBAC  
**Purpose:** Permission matrix view  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `permissions`, `role_permissions`  
**AJAX:** `ajax/get_permissions.php`

**Permission gate:**
```php
permission_check('users.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search permissions |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | permission_edit.php | users.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `role_edit.php`

**Module:** Users & RBAC  
**Purpose:** Create/edit role  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `roles`, `role_permissions`, `permissions`  
**AJAX:** `ajax/save_role.php`, `ajax/save_role_permissions.php`

**Permission gate:**
```php
permission_check('users.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_role.php | — |
| Cancel | `link` | roles.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect role_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `roles.php`

**Module:** Users & RBAC  
**Purpose:** Role list  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `roles`  
**AJAX:** `ajax/get_roles.php`

**Permission gate:**
```php
permission_check('users.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search roles |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | role_edit.php | users.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `user_edit.php`

**Module:** Users & RBAC  
**Purpose:** Create/edit user  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `users`, `user_roles`, `roles`  
**AJAX:** `ajax/save_user.php`, `ajax/assign_roles.php`

**Permission gate:**
```php
// Any of:
if (!(permission_check('users.create') || permission_check('users.edit'))) { accessDenied(); }
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_user.php | — |
| Cancel | `link` | users.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect user_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `users.php`

**Module:** Users & RBAC  
**Purpose:** Staff user list  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `users`, `user_roles`  
**AJAX:** `ajax/get_users.php`, `ajax/delete_user.php`

**Permission gate:**
```php
permission_check('users.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search users |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | user_edit.php | users.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### Customers

#### `customer_devices.php`

**Module:** Customers  
**Purpose:** Device registry  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customer_devices`  
**AJAX:** `ajax/save_customer_device.php`

**Permission gate:**
```php
permission_check('customers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search customer_devices |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | customer_device_edit.php | customers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `customer_documents.php`

**Module:** Customers  
**Purpose:** Document management  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customer_documents`  
**AJAX:** `ajax/upload_customer_document.php`, `ajax/delete_customer_document.php`

**Permission gate:**
```php
permission_check('customers.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search customer_documents |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | customer_document_edit.php | customers.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `customer_edit.php`

**Module:** Customers  
**Purpose:** Create/edit customer  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customers`, `customer_addresses`, `customer_contacts`  
**AJAX:** `ajax/save_customer.php`, `ajax/check_customer_phone.php`, `ajax/check_customer_email.php`

**Permission gate:**
```php
permission_check($id ? 'customers.edit' : 'customers.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_id` | hidden | No | — | Present on edit only |
| `customer_number` | text | No | auto | Auto-generated on create; read-only on edit |
| `full_name` | text | Yes | — | max 255 |
| `customer_type` | select | Yes | individual | individual|business|government|education|hotel |
| `company_name` | text | No | — | Required if business |
| `phone` | tel | Yes | — | Unique per tenant; E.164 format |
| `phone_alt` | tel | No | — | — |
| `email` | email | No | — | Unique per tenant if provided |
| `national_id` | text | No | — | — |
| `group_id` | select | No | — | customer_groups |
| `status` | select | Yes | pending | pending|active|inactive|suspended |
| `credit_limit` | number | No | 0 | min 0 |
| `billing_cycle_day` | number | No | — | 1-28 |
| `auto_renew` | checkbox | No | 1 | — |
| `address_line1` | text | No | — | Service address |
| `city` | text | No | — | — |
| `notes` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_customer.php | — |
| Save & New | `secondary` | AJAX + redirect customer_edit.php | create only |
| Cancel | `link` | customers.php | — |


**Validation — Client:** phone required; email format; company_name if business; debounce phone/email uniqueness via AJAX

**Validation — Server:** UNIQUE tenant+phone; UNIQUE tenant+email; sanitize inputs; audit_log on save

**Success workflow:** Redirect customer_view.php?id={id} or customers.php

**Error workflow:** Inline field errors; duplicate phone/email message

**UI components:** Bootstrap tabs: General, Address, Contacts; Select2 for group

---

#### `customer_groups.php`

**Module:** Customers  
**Purpose:** Customer groups  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customer_groups`  
**AJAX:** `ajax/get_customer_groups.php`, `ajax/save_customer_group.php`

**Permission gate:**
```php
permission_check('customers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search customer_groups |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | customer_group_edit.php | customers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `customer_import.php`

**Module:** Customers  
**Purpose:** Bulk import  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customer_import_batches`, `customer_import_rows`  
**AJAX:** `ajax/upload_customer_import.php`, `ajax/process_customer_import.php`

**Permission gate:**
```php
permission_check('customers.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_customer_import.php | — |
| Cancel | `link` | customer_imports.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect customer_import_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `customer_kyc.php`

**Module:** Customers  
**Purpose:** KYC verification  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customer_kyc`  
**AJAX:** `ajax/verify_customer_kyc.php`

**Permission gate:**
```php
permission_check('customers.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `customer_notes.php`

**Module:** Customers  
**Purpose:** Customer notes  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customer_notes`  
**AJAX:** `ajax/save_customer_note.php`

**Permission gate:**
```php
permission_check('customers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search customer_notes |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | customer_note_edit.php | customers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `customer_subscriptions.php`

**Module:** Customers  
**Purpose:** Subscription management  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customer_subscriptions`, `packages`  
**AJAX:** `ajax/save_customer_subscription.php`, `ajax/suspend_subscription.php`

**Permission gate:**
```php
permission_check('customers.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search customer_subscriptions |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | customer_subscription_edit.php | customers.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `customer_view.php`

**Module:** Customers  
**Purpose:** Customer 360 view  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customers`, `customer_subscriptions`, `invoices`, `payments`  
**AJAX:** `ajax/get_customer_details.php`, `ajax/get_customer_subscriptions.php`

**Permission gate:**
```php
permission_check('customers.view');
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Edit | `primary` | customer_edit.php | edit permission |
| Back | `link` | customers.php | — |


**Validation — Client:** N/A

**Validation — Server:** Load record by ID + tenant_id; 404 if not found

**Success workflow:** Render detail panels

**Error workflow:** Redirect to list with flash message

**UI components:** Info cards; Related records tabs; Activity timeline

---

#### `customers.php`

**Module:** Customers  
**Purpose:** Customer list  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `customers`  
**AJAX:** `ajax/get_customers.php`, `ajax/search_customers.php`, `ajax/export_customers.php`

**Permission gate:**
```php
permission_check('customers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search name, phone, email, customer_number |
| `status_filter` | select | No | all | all|active|inactive|suspended|pending |
| `group_filter` | select | No | — | customer_groups dropdown |
| `date_from` | date | No | — | Registration date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add Customer | `primary` | customer_edit.php | permission_check('customers.create') |
| Import | `secondary` | customer_import.php | permission_check('customers.create') |
| Export | `secondary` | AJAX export | permission_check('customers.view') |
| Bulk Suspend | `danger` | AJAX modal | permission_check('customers.suspend') |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; paginate max 100/page

**Success workflow:** DataTable reload on filter change

**Error workflow:** Toast notification on AJAX failure

**UI components:** DataTables: customer_number, full_name, phone, email, status, balance, group, created_at; Column search; Server-side pagination

---

### Packages

#### `package_edit.php`

**Module:** Packages  
**Purpose:** Create/edit package  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `packages`, `package_limits`, `package_pricing`, `package_fup_rules`  
**AJAX:** `ajax/save_package.php`, `ajax/save_package_limits.php`, `ajax/save_package_pricing.php`

**Permission gate:**
```php
permission_check('packages.create|packages.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `package_code` | text | Yes | — | Unique per tenant |
| `package_name` | text | Yes | — | — |
| `service_type` | select | Yes | pppoe | — |
| `package_group_id` | select | No | — | — |
| `base_price` | number | Yes | 0 | — |
| `billing_type` | select | Yes | prepaid | prepaid|postpaid|usage |
| `billing_cycle` | select | Yes | monthly | — |
| `download_speed_kbps` | number | No | — | package_limits tab |
| `upload_speed_kbps` | number | No | — | — |
| `data_cap_mb` | number | No | 0 | 0=unlimited |
| `validity_days` | number | No | — | For prepaid |
| `is_public` | checkbox | No | 1 | Show in portal |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX | — |
| Cancel | `link` | packages.php | — |


**Validation — Client:** Positive prices; speeds required for network packages

**Validation — Server:** Save packages + package_limits in transaction; sync radius if PPPoE/hotspot

**Success workflow:** Redirect packages.php

**Error workflow:** Duplicate package_code

**UI components:** Tabs: General, Speed/Limits, FUP Rules, Pricing, Features

---

#### `package_groups.php`

**Module:** Packages  
**Purpose:** Package groups  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `package_groups`  
**AJAX:** `ajax/save_package_group.php`

**Permission gate:**
```php
permission_check('packages.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search package_groups |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | package_group_edit.php | packages.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `package_promotions.php`

**Module:** Packages  
**Purpose:** Promotions  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `package_promotions`  
**AJAX:** `ajax/save_promotion.php`

**Permission gate:**
```php
permission_check('packages.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search package_promotions |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | package_promotion_edit.php | packages.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `package_view.php`

**Module:** Packages  
**Purpose:** Package detail  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `packages`, `package_features`  
**AJAX:** `ajax/get_package_details.php`

**Permission gate:**
```php
permission_check('packages.view');
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Edit | `primary` | package_edit.php | edit permission |
| Back | `link` | packages.php | — |


**Validation — Client:** N/A

**Validation — Server:** Load record by ID + tenant_id; 404 if not found

**Success workflow:** Render detail panels

**Error workflow:** Redirect to list with flash message

**UI components:** Info cards; Related records tabs; Activity timeline

---

#### `packages.php`

**Module:** Packages  
**Purpose:** Package list  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `packages`  
**AJAX:** `ajax/get_packages.php`, `ajax/delete_package.php`

**Permission gate:**
```php
permission_check('packages.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | — |
| `service_type` | select | No | all | pppoe|hotspot|vpn|fibre|voucher |
| `status` | select | No | active | active|inactive|all |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add Package | `primary` | package_edit.php | packages.create |


**Validation — Client:** N/A

**Validation — Server:** Tenant-scoped

**Success workflow:** DataTable

**Error workflow:** Toast

**UI components:** DataTables: package_code, name, service_type, price, billing_cycle, status, sort_order

---

### Billing

#### `billing.php`

**Module:** Billing  
**Purpose:** Billing overview  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `invoices`, `payments`  
**AJAX:** `ajax/billing_summary.php`

**Permission gate:**
```php
permission_check('billing.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `billing_cycles.php`

**Module:** Billing  
**Purpose:** Billing cycles  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `billing_cycles`  
**AJAX:** `ajax/save_billing_cycle.php`

**Permission gate:**
```php
permission_check('billing.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search billing_cycles |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | billing_cycle_edit.php | billing.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `credit_notes.php`

**Module:** Billing  
**Purpose:** Credit notes  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `credit_notes`  
**AJAX:** `ajax/save_credit_note.php`

**Permission gate:**
```php
permission_check('billing.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search credit_notes |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | credit_note_edit.php | billing.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `debit_notes.php`

**Module:** Billing  
**Purpose:** Debit notes  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `debit_notes`  
**AJAX:** `ajax/save_debit_note.php`

**Permission gate:**
```php
permission_check('billing.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search debit_notes |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | debit_note_edit.php | billing.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `dunning.php`

**Module:** Billing  
**Purpose:** Dunning rules  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `dunning_rules`, `dunning_log`  
**AJAX:** `ajax/save_dunning_rule.php`

**Permission gate:**
```php
permission_check('billing.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `invoice_edit.php`

**Module:** Billing  
**Purpose:** Create/edit invoice  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `invoices`, `invoice_items`  
**AJAX:** `ajax/save_invoice.php`, `ajax/add_invoice_item.php`, `ajax/remove_invoice_item.php`

**Permission gate:**
```php
permission_check('billing.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_id` | select | Yes | — | Searchable customer dropdown |
| `issue_date` | date | Yes | today | — |
| `due_date` | date | Yes | +30 days | Must be >= issue_date |
| `invoice_type` | select | Yes | standard | standard|recurring|proforma |
| `notes` | textarea | No | — | — |
| `items[]` | repeater | Yes | — | description, quantity, unit_price, package_id |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Draft | `secondary` | AJAX | — |
| Issue Invoice | `primary` | AJAX | Sets status=issued |
| Add Line Item | `secondary` | inline | — |
| Cancel | `link` | invoices.php | — |


**Validation — Client:** At least 1 line item; due_date >= issue_date; positive amounts

**Validation — Server:** Calculate totals server-side; fn_generate_invoice_number(); transaction wrap

**Success workflow:** Redirect invoice_view.php?id={id}

**Error workflow:** Cannot issue with zero total

**UI components:** Dynamic line item table; Running totals panel; Tax auto-calc from tax_rates

---

#### `invoice_view.php`

**Module:** Billing  
**Purpose:** Invoice detail/PDF  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `invoices`, `invoice_items`  
**AJAX:** `ajax/generate_invoice_pdf.php`

**Permission gate:**
```php
permission_check('billing.view');
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Edit | `primary` | invoice_edit.php | edit permission |
| Back | `link` | invoices.php | — |


**Validation — Client:** N/A

**Validation — Server:** Load record by ID + tenant_id; 404 if not found

**Success workflow:** Render detail panels

**Error workflow:** Redirect to list with flash message

**UI components:** Info cards; Related records tabs; Activity timeline

---

#### `invoices.php`

**Module:** Billing  
**Purpose:** Invoice list  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `invoices`  
**AJAX:** `ajax/get_invoices.php`, `ajax/send_invoice.php`

**Permission gate:**
```php
permission_check('billing.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Invoice number, customer |
| `status_filter` | select | No | all | draft|issued|paid|partial|overdue|cancelled |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Create Invoice | `primary` | invoice_edit.php | billing.create |
| Export | `secondary` | AJAX | reports.export |


**Validation — Client:** Date range valid

**Validation — Server:** Tenant-scoped; role Finance sees all, others per assignment

**Success workflow:** DataTable reload

**Error workflow:** Toast

**UI components:** DataTables: invoice_number, customer, issue_date, due_date, total, balance, status; Color-coded overdue rows

---

#### `payment_allocate.php`

**Module:** Billing  
**Purpose:** Allocate payment  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `payments`, `payment_allocations`, `invoices`  
**AJAX:** `ajax/allocate_payment.php`

**Permission gate:**
```php
permission_check('payments.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_payment_allocate.php | — |
| Cancel | `link` | payment_allocates.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect payment_allocate_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `payment_plans.php`

**Module:** Billing  
**Purpose:** Payment plans  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `payment_plans`, `payment_plan_installments`  
**AJAX:** `ajax/save_payment_plan.php`

**Permission gate:**
```php
permission_check('payments.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search payment_plans |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | payment_plan_edit.php | payments.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `payment_view.php`

**Module:** Billing  
**Purpose:** Payment detail  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `payments`, `payment_allocations`  
**AJAX:** `ajax/get_payment_details.php`

**Permission gate:**
```php
permission_check('payments.view');
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Edit | `primary` | payment_edit.php | edit permission |
| Back | `link` | payments.php | — |


**Validation — Client:** N/A

**Validation — Server:** Load record by ID + tenant_id; 404 if not found

**Success workflow:** Render detail panels

**Error workflow:** Redirect to list with flash message

**UI components:** Info cards; Related records tabs; Activity timeline

---

#### `payments.php`

**Module:** Billing  
**Purpose:** Payment list  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `payments`  
**AJAX:** `ajax/get_payments.php`, `ajax/record_payment.php`

**Permission gate:**
```php
permission_check('payments.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search payments |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | payment_edit.php | payments.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### Mobile Money

#### `airtel_transactions.php`

**Module:** Mobile Money  
**Purpose:** Airtel Money log  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `airtel_transactions`  
**AJAX:** `ajax/get_airtel_transactions.php`

**Permission gate:**
```php
permission_check('payments.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search airtel_transactions |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | airtel_transaction_edit.php | payments.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `momo_transactions.php`

**Module:** Mobile Money  
**Purpose:** MTN MoMo log  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `momo_transactions`  
**AJAX:** `ajax/get_momo_transactions.php`

**Permission gate:**
```php
permission_check('payments.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search momo_transactions |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | momo_transaction_edit.php | payments.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `mpesa_transactions.php`

**Module:** Mobile Money  
**Purpose:** M-Pesa log  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `mpesa_transactions`  
**AJAX:** `ajax/get_mpesa_transactions.php`

**Permission gate:**
```php
permission_check('payments.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search mpesa_transactions |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | mpesa_transaction_edit.php | payments.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `payment_callbacks.php`

**Module:** Mobile Money  
**Purpose:** Callback log  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `payment_callbacks`  
**AJAX:** `ajax/get_payment_callbacks.php`

**Permission gate:**
```php
permission_check('payments.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search payment_callbacks |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | payment_callback_edit.php | payments.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `payment_disbursements.php`

**Module:** Mobile Money  
**Purpose:** Disbursements  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `payment_disbursements`  
**AJAX:** `ajax/initiate_disbursement.php`

**Permission gate:**
```php
permission_check('payments.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search payment_disbursements |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | payment_disbursement_edit.php | payments.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `payment_reconciliation.php`

**Module:** Mobile Money  
**Purpose:** Reconciliation  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `payment_reconciliation`, `payment_reconciliation_items`  
**AJAX:** `ajax/run_reconciliation.php`, `ajax/match_payment.php`

**Permission gate:**
```php
permission_check('payments.reconcile');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `payment_refunds.php`

**Module:** Mobile Money  
**Purpose:** Refunds  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `payment_refunds`  
**AJAX:** `ajax/process_refund.php`

**Permission gate:**
```php
permission_check('payments.refund');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search payment_refunds |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | payment_refund_edit.php | payments.refund |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### Routers

#### `bandwidth_profiles.php`

**Module:** Routers  
**Purpose:** Bandwidth profiles  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `bandwidth_profiles`  
**AJAX:** `ajax/save_bandwidth_profile.php`

**Permission gate:**
```php
permission_check('routers.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search bandwidth_profiles |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | bandwidth_profile_edit.php | routers.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `router_alerts.php`

**Module:** Routers  
**Purpose:** Alerts  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_alerts`  
**AJAX:** `ajax/acknowledge_router_alert.php`

**Permission gate:**
```php
permission_check('routers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search router_alerts |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | router_alert_edit.php | routers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `router_backup.php`

**Module:** Routers  
**Purpose:** Backup management  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_backups`  
**AJAX:** `ajax/create_router_backup.php`, `ajax/restore_router_backup.php`, `ajax/download_router_backup.php`

**Permission gate:**
```php
permission_check('routers.backup');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `router_commands.php`

**Module:** Routers  
**Purpose:** Command queue  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_commands`  
**AJAX:** `ajax/queue_router_command.php`, `ajax/get_router_commands.php`

**Permission gate:**
```php
permission_check('routers.command');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search router_commands |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | router_command_edit.php | routers.command |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `router_discovery.php`

**Module:** Routers  
**Purpose:** Network discovery  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_discovery`  
**AJAX:** `ajax/run_router_discovery.php`

**Permission gate:**
```php
permission_check('routers.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `router_edit.php`

**Module:** Routers  
**Purpose:** Add/edit router  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `routers`, `router_credentials`, `nas`  
**AJAX:** `ajax/save_router.php`, `ajax/test_router_connection.php`, `ajax/discover_router.php`

**Permission gate:**
```php
permission_check($id ? 'routers.edit' : 'routers.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `router_id` | hidden | No | — | — |
| `router_code` | text | Yes | — | Unique per tenant |
| `router_name` | text | Yes | — | — |
| `ip_address` | text | Yes | — | Valid IPv4/hostname |
| `api_port` | number | Yes | 8729 | 8728 or 8729 |
| `api_username` | text | Yes | — | — |
| `api_password` | password | Yes | — | Encrypted at rest |
| `use_ssl` | checkbox | No | 1 | API-SSL |
| `location_id` | select | No | — | service_locations |
| `group_id` | select | No | — | router_groups |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_router.php | — |
| Test Connection | `secondary` | AJAX test_router_connection.php | — |
| Cancel | `link` | routers.php | — |


**Validation — Client:** IP format; port 1-65535; test connection before save recommended

**Validation — Server:** Encrypt api_password; register NAS; queue initial sync; UNIQUE tenant+router_code

**Success workflow:** Redirect router_view.php?id={id}

**Error workflow:** Connection test failure shows RouterOS error message

**UI components:** Connection test result panel; Map picker for location (optional)

---

#### `router_firewall.php`

**Module:** Routers  
**Purpose:** Firewall rules  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_firewall_rules`  
**AJAX:** `ajax/save_firewall_rule.php`

**Permission gate:**
```php
permission_check('routers.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `router_groups.php`

**Module:** Routers  
**Purpose:** Router groups  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_groups`, `router_group_members`  
**AJAX:** `ajax/save_router_group.php`

**Permission gate:**
```php
permission_check('routers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search router_groups |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | router_group_edit.php | routers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `router_metrics.php`

**Module:** Routers  
**Purpose:** Metrics charts  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_metrics`, `network_metrics_hourly`  
**AJAX:** `ajax/get_router_metrics.php`

**Permission gate:**
```php
permission_check('routers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search router_metrics |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | router_metric_edit.php | routers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `router_queues.php`

**Module:** Routers  
**Purpose:** Queue trees  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_queues`, `queue_tree_templates`  
**AJAX:** `ajax/save_router_queue.php`, `ajax/apply_queue_template.php`

**Permission gate:**
```php
permission_check('routers.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search router_queues |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | router_queue_edit.php | routers.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `router_schedules.php`

**Module:** Routers  
**Purpose:** Schedules  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_schedules`  
**AJAX:** `ajax/save_router_schedule.php`

**Permission gate:**
```php
permission_check('routers.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search router_schedules |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | router_schedule_edit.php | routers.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `router_scripts.php`

**Module:** Routers  
**Purpose:** Scripts  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `router_scripts`  
**AJAX:** `ajax/save_router_script.php`, `ajax/run_router_script.php`

**Permission gate:**
```php
permission_check('routers.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search router_scripts |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | router_script_edit.php | routers.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `router_view.php`

**Module:** Routers  
**Purpose:** Router detail  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `routers`, `router_status`, `router_interfaces`  
**AJAX:** `ajax/get_router_details.php`, `ajax/get_router_status.php`

**Permission gate:**
```php
permission_check('routers.view');
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Edit | `primary` | router_edit.php | edit permission |
| Back | `link` | routers.php | — |


**Validation — Client:** N/A

**Validation — Server:** Load record by ID + tenant_id; 404 if not found

**Success workflow:** Render detail panels

**Error workflow:** Redirect to list with flash message

**UI components:** Info cards; Related records tabs; Activity timeline

---

#### `routers.php`

**Module:** Routers  
**Purpose:** Router list  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `routers`  
**AJAX:** `ajax/get_routers.php`, `ajax/delete_router.php`

**Permission gate:**
```php
permission_check('routers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Name, IP, code |
| `status_filter` | select | No | all | all|online|offline|degraded|maintenance |
| `group_filter` | select | No | — | router_groups |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add Router | `primary` | router_edit.php | routers.create |
| Discover | `secondary` | router_discovery.php | routers.create |
| Refresh All | `secondary` | AJAX | routers.view |


**Validation — Client:** N/A

**Validation — Server:** Tenant-scoped; include router_status join

**Success workflow:** DataTable with live status badges

**Error workflow:** Toast on connection test batch failure

**UI components:** DataTables: router_code, router_name, ip_address, status badge, cpu%, sessions, last_check; Row actions: View, Edit, Backup, Commands

---

#### `service_locations.php`

**Module:** Routers  
**Purpose:** POP/locations  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `service_locations`  
**AJAX:** `ajax/save_service_location.php`

**Permission gate:**
```php
permission_check('routers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search service_locations |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | service_location_edit.php | routers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### VPN

#### `vpn_logs.php`

**Module:** VPN  
**Purpose:** VPN logs  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `vpn_logs`  
**AJAX:** `ajax/get_vpn_logs.php`

**Permission gate:**
```php
permission_check('vpn.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search vpn_logs |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | vpn_log_edit.php | vpn.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `vpn_peer_edit.php`

**Module:** VPN  
**Purpose:** Edit peer  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `vpn_peers`, `vpn_keys`  
**AJAX:** `ajax/generate_vpn_keys.php`, `ajax/push_vpn_config.php`

**Permission gate:**
```php
permission_check('vpn.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_vpn_peer.php | — |
| Cancel | `link` | vpn_peers.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect vpn_peer_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `vpn_peers.php`

**Module:** VPN  
**Purpose:** VPN peers  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `vpn_peers`  
**AJAX:** `ajax/get_vpn_peers.php`, `ajax/save_vpn_peer.php`

**Permission gate:**
```php
permission_check('vpn.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search vpn_peers |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | vpn_peer_edit.php | vpn.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `vpn_server_edit.php`

**Module:** VPN  
**Purpose:** Edit VPN server  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `vpn_servers`, `vpn_server_interfaces`  
**AJAX:** `ajax/save_vpn_server.php`

**Permission gate:**
```php
// Any of:
if (!(permission_check('vpn.create') || permission_check('vpn.edit'))) { accessDenied(); }
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_vpn_server.php | — |
| Cancel | `link` | vpn_servers.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect vpn_server_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `vpn_servers.php`

**Module:** VPN  
**Purpose:** VPN servers  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `vpn_servers`  
**AJAX:** `ajax/get_vpn_servers.php`, `ajax/save_vpn_server.php`

**Permission gate:**
```php
permission_check('vpn.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search vpn_servers |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | vpn_server_edit.php | vpn.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `vpn_tunnels.php`

**Module:** VPN  
**Purpose:** Tunnel monitor  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `vpn_tunnels`, `vpn_peer_status`  
**AJAX:** `ajax/get_vpn_tunnels.php`

**Permission gate:**
```php
permission_check('vpn.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search vpn_tunnels |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | vpn_tunnel_edit.php | vpn.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### FreeRADIUS

#### `nas_devices.php`

**Module:** FreeRADIUS  
**Purpose:** NAS devices  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `nas`  
**AJAX:** `ajax/save_nas.php`

**Permission gate:**
```php
permission_check('routers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search nas_devices |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | nas_device_edit.php | routers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `radius_accounting.php`

**Module:** FreeRADIUS  
**Purpose:** Accounting viewer  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `radacct`, `radius_accounting_summary`  
**AJAX:** `ajax/get_radacct.php`

**Permission gate:**
```php
permission_check('pppoe.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `radius_groups.php`

**Module:** FreeRADIUS  
**Purpose:** RADIUS groups  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `radgroupcheck`, `radgroupreply`  
**AJAX:** `ajax/save_radius_group.php`

**Permission gate:**
```php
permission_check('pppoe.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search radius_groups |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | radius_group_edit.php | pppoe.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `radius_profiles.php`

**Module:** FreeRADIUS  
**Purpose:** RADIUS profiles  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `radius_profiles`, `radius_profile_attributes`  
**AJAX:** `ajax/get_radius_profiles.php`, `ajax/save_radius_profile.php`

**Permission gate:**
```php
permission_check('pppoe.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search radius_profiles |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | radius_profile_edit.php | pppoe.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `radius_sync.php`

**Module:** FreeRADIUS  
**Purpose:** Sync status  
**Roles:** ISP Admin, Technician  
**Layout:** `main`  
**Tables:** `radius_sync_log`, `radcheck`  
**AJAX:** `ajax/sync_radius.php`, `ajax/get_radius_sync_log.php`

**Permission gate:**
```php
permission_check('routers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

### Hotspot

#### `hotspot_edit.php`

**Module:** Hotspot  
**Purpose:** Edit hotspot  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `hotspots`, `hotspot_profiles`  
**AJAX:** `ajax/save_hotspot.php`

**Permission gate:**
```php
// Any of:
if (!(permission_check('hotspot.create') || permission_check('hotspot.edit'))) { accessDenied(); }
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_hotspot.php | — |
| Cancel | `link` | hotspots.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect hotspot_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `hotspot_mac_auth.php`

**Module:** Hotspot  
**Purpose:** MAC auth  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `hotspot_mac_auth`, `mac_auth_devices`  
**AJAX:** `ajax/save_hotspot_mac.php`

**Permission gate:**
```php
permission_check('hotspot.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `hotspot_portals.php`

**Module:** Hotspot  
**Purpose:** Portal config  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `hotspot_portals`, `hotspot_portal_templates`  
**AJAX:** `ajax/save_hotspot_portal.php`

**Permission gate:**
```php
permission_check('hotspot.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search hotspot_portals |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | hotspot_portal_edit.php | hotspot.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `hotspot_profiles.php`

**Module:** Hotspot  
**Purpose:** Profiles  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `hotspot_profiles`, `hotspot_profile_packages`  
**AJAX:** `ajax/save_hotspot_profile.php`

**Permission gate:**
```php
permission_check('hotspot.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search hotspot_profiles |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | hotspot_profile_edit.php | hotspot.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `hotspot_sessions.php`

**Module:** Hotspot  
**Purpose:** Active sessions  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `hotspot_sessions`  
**AJAX:** `ajax/get_hotspot_sessions.php`, `ajax/disconnect_hotspot_session.php`

**Permission gate:**
```php
permission_check('hotspot.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search hotspot_sessions |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | hotspot_session_edit.php | hotspot.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `hotspot_usage.php`

**Module:** Hotspot  
**Purpose:** Usage reports  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `hotspot_usage`  
**AJAX:** `ajax/get_hotspot_usage.php`

**Permission gate:**
```php
permission_check('hotspot.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `hotspot_users.php`

**Module:** Hotspot  
**Purpose:** Hotspot users  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `hotspot_users`  
**AJAX:** `ajax/save_hotspot_user.php`, `ajax/suspend_hotspot_user.php`

**Permission gate:**
```php
permission_check('hotspot.users');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search hotspot_users |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | hotspot_user_edit.php | hotspot.users |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `hotspot_walled_garden.php`

**Module:** Hotspot  
**Purpose:** Walled garden  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `hotspot_walled_garden`  
**AJAX:** `ajax/save_walled_garden.php`

**Permission gate:**
```php
permission_check('hotspot.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `hotspots.php`

**Module:** Hotspot  
**Purpose:** Hotspot list  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `hotspots`  
**AJAX:** `ajax/get_hotspots.php`

**Permission gate:**
```php
permission_check('hotspot.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search hotspots |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | hotspot_edit.php | hotspot.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### PPPoE

#### `fibre_connections.php`

**Module:** PPPoE  
**Purpose:** Fibre connections  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `fibre_connections`  
**AJAX:** `ajax/save_fibre_connection.php`

**Permission gate:**
```php
permission_check('pppoe.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search fibre_connections |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | fibre_connection_edit.php | pppoe.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `pppoe_account_edit.php`

**Module:** PPPoE  
**Purpose:** Edit account  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `pppoe_accounts`, `radcheck`  
**AJAX:** `ajax/save_pppoe_account.php`, `ajax/suspend_pppoe_account.php`

**Permission gate:**
```php
// Any of:
if (!(permission_check('pppoe.create') || permission_check('pppoe.edit'))) { accessDenied(); }
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_pppoe_account.php | — |
| Cancel | `link` | pppoe_accounts.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect pppoe_account_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `pppoe_accounts.php`

**Module:** PPPoE  
**Purpose:** PPPoE accounts  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `pppoe_accounts`  
**AJAX:** `ajax/get_pppoe_accounts.php`, `ajax/save_pppoe_account.php`

**Permission gate:**
```php
permission_check('pppoe.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search pppoe_accounts |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | pppoe_account_edit.php | pppoe.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `pppoe_ip_pools.php`

**Module:** PPPoE  
**Purpose:** IP pools  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `pppoe_ip_pools`, `pppoe_pool_assignments`  
**AJAX:** `ajax/save_pppoe_pool.php`

**Permission gate:**
```php
permission_check('pppoe.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search pppoe_ip_pools |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | pppoe_ip_pool_edit.php | pppoe.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `pppoe_profiles.php`

**Module:** PPPoE  
**Purpose:** PPPoE profiles  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `pppoe_profiles`, `pppoe_profile_packages`  
**AJAX:** `ajax/save_pppoe_profile.php`

**Permission gate:**
```php
permission_check('pppoe.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search pppoe_profiles |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | pppoe_profile_edit.php | pppoe.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `pppoe_sessions.php`

**Module:** PPPoE  
**Purpose:** Active sessions  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `pppoe_sessions`  
**AJAX:** `ajax/get_pppoe_sessions.php`, `ajax/disconnect_pppoe_session.php`

**Permission gate:**
```php
permission_check('pppoe.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search pppoe_sessions |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | pppoe_session_edit.php | pppoe.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `pppoe_static_ips.php`

**Module:** PPPoE  
**Purpose:** Static IPs  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `pppoe_static_ips`, `ip_address_reservations`  
**AJAX:** `ajax/assign_static_ip.php`

**Permission gate:**
```php
permission_check('pppoe.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search pppoe_static_ips |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | pppoe_static_ip_edit.php | pppoe.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### Vouchers

#### `voucher_batch_edit.php`

**Module:** Vouchers  
**Purpose:** Create batch  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `voucher_batches`, `vouchers`  
**AJAX:** `ajax/generate_voucher_batch.php`

**Permission gate:**
```php
permission_check('vouchers.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `batch_code` | text | Yes | — | Auto or manual |
| `batch_name` | text | Yes | — | — |
| `package_id` | select | Yes | — | hotspot/voucher packages |
| `quantity` | number | Yes | 100 | 1-10000 |
| `pin_length` | number | Yes | 8 | 6-16 |
| `prefix` | text | No | — | Voucher code prefix |
| `expires_at` | datetime | No | — | — |
| `price_per_voucher` | number | No | 0 | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Generate | `primary` | AJAX generate_voucher_batch.php | — |
| Cancel | `link` | voucher_batches.php | — |


**Validation — Client:** quantity max 10000; package required

**Validation — Server:** Batch insert in transaction; generate unique voucher_codes

**Success workflow:** Redirect voucher_batch_view.php?id={id}

**Error workflow:** Rollback on duplicate collision

**UI components:** Progress bar for large batches; PIN format preview

---

#### `voucher_batch_view.php`

**Module:** Vouchers  
**Purpose:** Batch detail  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `vouchers`, `voucher_usage`  
**AJAX:** `ajax/get_voucher_batch_details.php`

**Permission gate:**
```php
permission_check('vouchers.view');
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Edit | `primary` | voucher_batch_edit.php | edit permission |
| Back | `link` | voucher_batchs.php | — |


**Validation — Client:** N/A

**Validation — Server:** Load record by ID + tenant_id; 404 if not found

**Success workflow:** Render detail panels

**Error workflow:** Redirect to list with flash message

**UI components:** Info cards; Related records tabs; Activity timeline

---

#### `voucher_batches.php`

**Module:** Vouchers  
**Purpose:** Batch list  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `voucher_batches`  
**AJAX:** `ajax/get_voucher_batches.php`

**Permission gate:**
```php
permission_check('vouchers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Batch code/name |
| `status_filter` | select | No | all | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Generate Batch | `primary` | voucher_batch_edit.php | vouchers.create |
| Print | `secondary` | voucher_print.php | vouchers.print |


**Validation — Client:** N/A

**Validation — Server:** Tenant-scoped

**Success workflow:** DataTable with inventory counts from vw_voucher_inventory

**Error workflow:** Toast

**UI components:** DataTables: batch_code, package, quantity, available, used, expired, status

---

#### `voucher_print.php`

**Module:** Vouchers  
**Purpose:** Print vouchers  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `voucher_prints`  
**AJAX:** `ajax/get_vouchers_for_print.php`

**Permission gate:**
```php
permission_check('vouchers.print');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `voucher_resellers.php`

**Module:** Vouchers  
**Purpose:** Resellers  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `voucher_resellers`  
**AJAX:** `ajax/save_voucher_reseller.php`

**Permission gate:**
```php
permission_check('vouchers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search voucher_resellers |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | voucher_reseller_edit.php | vouchers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `voucher_templates.php`

**Module:** Vouchers  
**Purpose:** Print templates  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `voucher_templates`  
**AJAX:** `ajax/save_voucher_template.php`

**Permission gate:**
```php
permission_check('vouchers.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search voucher_templates |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | voucher_template_edit.php | vouchers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `vouchers.php`

**Module:** Vouchers  
**Purpose:** Voucher search  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `vouchers`  
**AJAX:** `ajax/search_vouchers.php`

**Permission gate:**
```php
permission_check('vouchers.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search vouchers |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | voucher_edit.php | vouchers.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### Captive Portal

#### `captive_portal_edit.php`

**Module:** Captive Portal  
**Purpose:** Portal builder  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `captive_portal_templates`, `captive_portal_themes`, `captive_portal_pages`  
**AJAX:** `ajax/save_captive_portal.php`, `ajax/preview_captive_portal.php`

**Permission gate:**
```php
permission_check('hotspot.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_captive_portal.php | — |
| Cancel | `link` | captive_portals.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect captive_portal_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `captive_portal_preview.php`

**Module:** Captive Portal  
**Purpose:** Live preview  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `captive_portal_templates`  
**AJAX:** `ajax/render_captive_preview.php`

**Permission gate:**
```php
permission_check('hotspot.view');
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Edit | `primary` | captive_portal_preview.php | edit permission |
| Back | `link` | captive_previews.php | — |


**Validation — Client:** N/A

**Validation — Server:** Load record by ID + tenant_id; 404 if not found

**Success workflow:** Render detail panels

**Error workflow:** Redirect to list with flash message

**UI components:** Info cards; Related records tabs; Activity timeline

---

#### `captive_portal_themes.php`

**Module:** Captive Portal  
**Purpose:** Theme editor  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `captive_portal_themes`, `captive_portal_assets`  
**AJAX:** `ajax/save_captive_theme.php`

**Permission gate:**
```php
permission_check('hotspot.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search captive_themes |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | captive_theme_edit.php | hotspot.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `captive_portals.php`

**Module:** Captive Portal  
**Purpose:** Portal list  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `captive_portal_templates`  
**AJAX:** `ajax/get_captive_portals.php`

**Permission gate:**
```php
permission_check('hotspot.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search captive_portals |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | captive_portal_edit.php | hotspot.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### Ticketing

#### `ticket_categories.php`

**Module:** Ticketing  
**Purpose:** Categories  
**Roles:** ISP Admin, Support  
**Layout:** `main`  
**Tables:** `ticket_categories`  
**AJAX:** `ajax/save_ticket_category.php`

**Permission gate:**
```php
permission_check('tickets.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search ticket_categories |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | ticket_categorie_edit.php | tickets.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `ticket_edit.php`

**Module:** Ticketing  
**Purpose:** Create ticket  
**Roles:** ISP Admin, Support  
**Layout:** `main`  
**Tables:** `tickets`, `ticket_messages`  
**AJAX:** `ajax/save_ticket.php`

**Permission gate:**
```php
permission_check('tickets.create');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `id` | hidden | No | — | Record ID on edit |
| `name` | text | Yes | — | Primary identifier |
| `status` | select | Yes | active | active|inactive |
| `description` | textarea | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_ticket.php | — |
| Cancel | `link` | tickets.php | — |


**Validation — Client:** Required fields; format validation per field type

**Validation — Server:** PDO prepared statements; tenant_id enforced; audit_log on change

**Success workflow:** Redirect ticket_view.php or list page

**Error workflow:** Inline validation errors returned as JSON

**UI components:** Bootstrap 5 form; jQuery Validate

---

#### `ticket_sla.php`

**Module:** Ticketing  
**Purpose:** SLA rules  
**Roles:** ISP Admin, Support  
**Layout:** `main`  
**Tables:** `ticket_sla_rules`  
**AJAX:** `ajax/save_ticket_sla.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `context_fields` | mixed | No | — | Page-specific fields |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Primary Action | `primary` | AJAX | — |


**Validation — Client:** Per form field

**Validation — Server:** Tenant-scoped; permission check

**Success workflow:** Redirect or reload

**Error workflow:** Toast notification

**UI components:** Bootstrap 5 components as needed

---

#### `ticket_view.php`

**Module:** Ticketing  
**Purpose:** Ticket detail  
**Roles:** ISP Admin, Support  
**Layout:** `main`  
**Tables:** `tickets`, `ticket_messages`, `ticket_attachments`  
**AJAX:** `ajax/get_ticket_messages.php`, `ajax/reply_ticket.php`, `ajax/assign_ticket.php`

**Permission gate:**
```php
permission_check('tickets.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `reply_body` | textarea | Yes | — | Ticket reply |
| `is_internal` | checkbox | No | 0 | Staff only note |
| `attachment` | file | No | — | max 10MB |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Reply | `primary` | AJAX reply_ticket.php | — |
| Assign | `secondary` | modal | tickets.assign |
| Close | `success` | AJAX close_ticket.php | tickets.close |
| Escalate | `warning` | AJAX | — |


**Validation — Client:** Reply not empty; file type whitelist

**Validation — Server:** Append ticket_messages; update SLA log; notify customer if not internal

**Success workflow:** Thread refresh via AJAX

**Error workflow:** Cannot close without resolution note

**UI components:** Message thread timeline; Customer info sidebar; SLA countdown badge

---

#### `tickets.php`

**Module:** Ticketing  
**Purpose:** Ticket list  
**Roles:** ISP Admin, Support  
**Layout:** `main`  
**Tables:** `tickets`  
**AJAX:** `ajax/get_tickets.php`

**Permission gate:**
```php
permission_check('tickets.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Ticket number, subject |
| `status_filter` | select | No | open | ticket_statuses |
| `priority_filter` | select | No | — | ticket_priorities |
| `assigned_filter` | select | No | — | staff users |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| New Ticket | `primary` | ticket_edit.php | tickets.create |


**Validation — Client:** N/A

**Validation — Server:** Support sees assigned + unassigned; Admin sees all

**Success workflow:** DataTable

**Error workflow:** Toast

**UI components:** DataTables: ticket_number, subject, customer, priority badge, status, assigned_to, created_at, SLA indicator

---

### Notifications

#### `email_logs.php`

**Module:** Notifications  
**Purpose:** Email log  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `email_logs`  
**AJAX:** `ajax/get_email_logs.php`, `ajax/send_email.php`

**Permission gate:**
```php
permission_check('settings.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search email_logs |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | email_log_edit.php | settings.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `notification_rules.php`

**Module:** Notifications  
**Purpose:** Automation rules  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `notification_rules`  
**AJAX:** `ajax/save_notification_rule.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search notification_rules |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | notification_rule_edit.php | settings.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `notification_templates.php`

**Module:** Notifications  
**Purpose:** Templates  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `notification_templates`  
**AJAX:** `ajax/save_notification_template.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search notification_templates |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | notification_template_edit.php | settings.edit |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `notifications.php`

**Module:** Notifications  
**Purpose:** Notification center  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `notification_queue`  
**AJAX:** `ajax/get_notification_queue.php`

**Permission gate:**
```php
permission_check('settings.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search notifications |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | notification_edit.php | settings.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

#### `sms_logs.php`

**Module:** Notifications  
**Purpose:** SMS log  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `sms_logs`  
**AJAX:** `ajax/get_sms_logs.php`, `ajax/send_sms.php`

**Permission gate:**
```php
permission_check('settings.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `search` | text | No | — | Search sms_logs |
| `status_filter` | select | No | all | Status filter |
| `date_from` | date | No | — | Optional date range |
| `date_to` | date | No | — | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Add New | `primary` | sms_log_edit.php | settings.create |
| Export | `secondary` | AJAX export | If applicable |


**Validation — Client:** Date range: from <= to

**Validation — Server:** Tenant-scoped query; soft-delete filter; pagination max 100 rows

**Success workflow:** DataTable AJAX reload

**Error workflow:** Bootstrap toast with error message

**UI components:** DataTables server-side processing; Responsive columns; Row action dropdown

---

### Reports

#### `report_aging.php`

**Module:** Reports  
**Purpose:** AR aging  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `invoices`, `vw_customer_balances`  
**AJAX:** `ajax/report_aging_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Aging summary KPI cards

---

#### `report_builder.php`

**Module:** Reports  
**Purpose:** Custom report builder  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `report_definitions`  
**AJAX:** `ajax/save_report_definition.php`

**Permission gate:**
```php
permission_check('reports.export');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Builder summary KPI cards

---

#### `report_collections.php`

**Module:** Reports  
**Purpose:** Collections report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `payments`, `payment_allocations`  
**AJAX:** `ajax/report_collections_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Collections summary KPI cards

---

#### `report_customers.php`

**Module:** Reports  
**Purpose:** Customer report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `customer_metrics_daily`  
**AJAX:** `ajax/report_customers_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Customers summary KPI cards

---

#### `report_hotspot.php`

**Module:** Reports  
**Purpose:** Hotspot report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `hotspot_sessions`, `hotspot_usage`  
**AJAX:** `ajax/report_hotspot_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Hotspot summary KPI cards

---

#### `report_network.php`

**Module:** Reports  
**Purpose:** Network report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `network_metrics_hourly`  
**AJAX:** `ajax/report_network_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Network summary KPI cards

---

#### `report_pppoe.php`

**Module:** Reports  
**Purpose:** PPPoE report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `pppoe_sessions`, `radacct`  
**AJAX:** `ajax/report_pppoe_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Pppoe summary KPI cards

---

#### `report_revenue.php`

**Module:** Reports  
**Purpose:** Revenue report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `vw_revenue_by_period`, `revenue_aggregates_daily`  
**AJAX:** `ajax/report_revenue_data.php`, `ajax/export_report.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | Required if custom |
| `date_to` | date | No | — | — |
| `payment_method` | select | No | all | — |
| `group_by` | select | No | day | day|method|package |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export PDF | `secondary` | AJAX export_report.php | reports.export |
| Export Excel | `secondary` | AJAX | reports.export |


**Validation — Client:** Custom range required when period=custom

**Validation — Server:** Query vw_revenue_by_period; cache in report_cache

**Success workflow:** Render chart + summary table

**Error workflow:** No data message for empty range

**UI components:** Chart.js line chart revenue trend; DataTables breakdown; Summary cards: total, count, avg

---

#### `report_routers.php`

**Module:** Reports  
**Purpose:** Router report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `vw_router_health`, `router_metrics`  
**AJAX:** `ajax/report_routers_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Routers summary KPI cards

---

#### `report_schedules.php`

**Module:** Reports  
**Purpose:** Scheduled reports  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `report_schedules`  
**AJAX:** `ajax/save_report_schedule.php`

**Permission gate:**
```php
permission_check('reports.export');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Schedules summary KPI cards

---

#### `report_subscriptions.php`

**Module:** Reports  
**Purpose:** Subscription report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `customer_subscriptions`  
**AJAX:** `ajax/report_subscriptions_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Subscriptions summary KPI cards

---

#### `report_tax.php`

**Module:** Reports  
**Purpose:** Tax report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `invoices`, `tax_rates`  
**AJAX:** `ajax/report_tax_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Tax summary KPI cards

---

#### `report_tickets.php`

**Module:** Reports  
**Purpose:** Ticket report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `tickets`  
**AJAX:** `ajax/report_tickets_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Tickets summary KPI cards

---

#### `report_usage.php`

**Module:** Reports  
**Purpose:** Usage report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `usage_aggregates_daily`, `usage_aggregates_monthly`  
**AJAX:** `ajax/report_usage_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Usage summary KPI cards

---

#### `report_vouchers.php`

**Module:** Reports  
**Purpose:** Voucher report  
**Roles:** ISP Admin, Finance  
**Layout:** `main`  
**Tables:** `vw_voucher_inventory`  
**AJAX:** `ajax/report_vouchers_data.php`

**Permission gate:**
```php
permission_check('reports.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `period` | select | Yes | month | day|week|month|quarter|year|custom |
| `date_from` | date | No | — | — |
| `date_to` | date | No | — | — |
| `export_format` | select | No | screen | screen|pdf|excel|csv |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Run Report | `primary` | AJAX | — |
| Export | `secondary` | AJAX export_report.php | reports.export |


**Validation — Client:** Custom date range required when period=custom

**Validation — Server:** Tenant-scoped aggregation; use report_cache when available

**Success workflow:** Render Chart.js + DataTables

**Error workflow:** Empty state message

**UI components:** Chart.js visualization; DataTables; Vouchers summary KPI cards

---

### Settings

#### `settings_api.php`

**Module:** Settings  
**Purpose:** API settings  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `tenant_api_keys`, `api_tokens`  
**AJAX:** `ajax/save_api_settings.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `api_*` | mixed | No | — | Settings keys for api group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_billing.php`

**Module:** Settings  
**Purpose:** Billing settings  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `tax_rates`, `invoice_sequences`, `billing_cycles`  
**AJAX:** `ajax/save_billing_settings.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `billing_*` | mixed | No | — | Settings keys for billing group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_branding.php`

**Module:** Settings  
**Purpose:** White-label branding  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `tenant_themes`  
**AJAX:** `ajax/save_tenant_theme.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `branding_*` | mixed | No | — | Settings keys for branding group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_general.php`

**Module:** Settings  
**Purpose:** General settings  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `tenant_settings`  
**AJAX:** `ajax/save_tenant_settings.php`

**Permission gate:**
```php
permission_check('settings.view');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `general_*` | mixed | No | — | Settings keys for general group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_localization.php`

**Module:** Settings  
**Purpose:** Locale/currency  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `tenants`, `currencies`, `timezones`  
**AJAX:** `ajax/save_localization.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `localization_*` | mixed | No | — | Settings keys for localization group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_modules.php`

**Module:** Settings  
**Purpose:** Module settings  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `tenant_modules`  
**AJAX:** `ajax/save_module_config.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `modules_*` | mixed | No | — | Settings keys for modules group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_network.php`

**Module:** Settings  
**Purpose:** Network defaults  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `bandwidth_profiles`, `queue_tree_templates`  
**AJAX:** `ajax/save_network_settings.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `network_*` | mixed | No | — | Settings keys for network group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_notifications.php`

**Module:** Settings  
**Purpose:** Notification config  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `notification_channels`, `notification_provider_configs`  
**AJAX:** `ajax/save_notification_config.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `notifications_*` | mixed | No | — | Settings keys for notifications group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_payments.php`

**Module:** Settings  
**Purpose:** Payment gateways  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `payment_gateway_configs`, `payment_gateways`  
**AJAX:** `ajax/save_payment_gateway_config.php`, `ajax/test_gateway_connection.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `mpesa_enabled` | checkbox | No | 0 | — |
| `mpesa_consumer_key` | text | No | — | Encrypted |
| `mpesa_consumer_secret` | password | No | — | Encrypted |
| `mpesa_shortcode` | text | No | — | — |
| `mpesa_passkey` | password | No | — | — |
| `mpesa_environment` | select | No | sandbox | sandbox|production |
| `momo_enabled` | checkbox | No | 0 | — |
| `momo_api_key` | password | No | — | — |
| `airtel_enabled` | checkbox | No | 0 | — |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save | `primary` | AJAX save_payment_gateway_config.php | — |
| Test M-Pesa | `secondary` | AJAX test_gateway_connection.php | — |


**Validation — Client:** If gateway enabled, required credentials must be filled

**Validation — Server:** Encrypt secrets; store in payment_gateway_configs; never return secrets in GET

**Success workflow:** Toast: settings saved

**Error workflow:** Gateway test connection failure detail

**UI components:** Accordion per gateway; Masked credential fields

---

#### `settings_radius.php`

**Module:** Settings  
**Purpose:** RADIUS settings  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `tenant_settings`, `nas`  
**AJAX:** `ajax/save_radius_settings.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `radius_*` | mixed | No | — | Settings keys for radius group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_security.php`

**Module:** Settings  
**Purpose:** Security settings  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `ip_whitelist`, `api_rate_limits`  
**AJAX:** `ajax/save_security_settings.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `security_*` | mixed | No | — | Settings keys for security group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

#### `settings_webhooks.php`

**Module:** Settings  
**Purpose:** Webhooks  
**Roles:** ISP Admin, Support, Finance, Technician  
**Layout:** `main`  
**Tables:** `tenant_webhooks`, `webhook_delivery_log`  
**AJAX:** `ajax/save_webhook.php`, `ajax/test_webhook.php`

**Permission gate:**
```php
permission_check('settings.edit');
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `webhooks_*` | mixed | No | — | Settings keys for webhooks group |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Save Settings | `primary` | AJAX save_tenant_settings.php | — |
| Reset | `secondary` | Confirm modal | — |


**Validation — Client:** Validate required config per enabled feature

**Validation — Server:** Upsert tenant_settings; encrypt sensitive values

**Success workflow:** Toast: Settings saved successfully

**Error workflow:** Field-level errors for invalid config

**UI components:** Accordion sections; Help tooltips per setting

---

### Customer Portal

#### `portal_dashboard.php`

**Module:** Customer Portal  
**Purpose:** Customer dashboard  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `customer_subscriptions`, `usage_aggregates_daily`  
**AJAX:** `ajax/portal_dashboard_data.php`

**Permission gate:**
```php
portalAuthRequired(); // customer session only
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Buy Package | `primary` | portal_packages.php | — |
| Pay Bill | `secondary` | portal_pay.php | — |
| Open Ticket | `secondary` | portal_tickets.php | — |


**Validation — Client:** N/A

**Validation — Server:** Customer_id from session only — no URL param trust

**Success workflow:** Render subscription cards + usage chart

**Error workflow:** Redirect portal_login.php if session expired

**UI components:** Chart.js usage donut; Subscription expiry countdown; Quick pay widget

---

#### `portal_invoice_view.php`

**Module:** Customer Portal  
**Purpose:** Invoice detail  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `invoices`, `invoice_items`  
**AJAX:** `ajax/portal_download_invoice.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_invoices.php`

**Module:** Customer Portal  
**Purpose:** My invoices  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `invoices`  
**AJAX:** `ajax/portal_get_invoices.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_login.php`

**Module:** Customer Portal  
**Purpose:** Customer login  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `customers`, `portal_sessions`  
**AJAX:** `ajax/portal_login.php`

**Permission gate:**
```php
// Public — redirect if portal session active
if (portalLoggedIn()) { header('Location: portal_dashboard.php'); exit; }
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `phone` | tel | Yes | — | Customer phone number |
| `password` | password | Yes | — | Or OTP mode |
| `login_mode` | radio | Yes | password | password|otp |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Login | `primary` | AJAX portal_login.php | — |
| Register | `link` | portal_register.php | — |
| Forgot Password? | `link` | portal_forgot_password flow | — |


**Validation — Client:** Phone format per tenant country

**Validation — Server:** Match customer.phone + portal user password; create portal_session

**Success workflow:** Redirect portal_dashboard.php

**Error workflow:** Account suspended message

**UI components:** Tenant branded theme from tenant_themes; Mobile-first layout

---

#### `portal_logout.php`

**Module:** Customer Portal  
**Purpose:** Portal logout  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `portal_sessions`  
**AJAX:** —

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

_No form fields — display/action page._

**Buttons:**

_No UI buttons — handler/redirect page._


**Validation — Client:** N/A — server-side only

**Validation — Server:** Process and redirect; no HTML output

**Success workflow:** HTTP redirect

**Error workflow:** Redirect with flash error

**UI components:** None — backend handler

---

#### `portal_packages.php`

**Module:** Customer Portal  
**Purpose:** Browse packages  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `packages`, `package_pricing`  
**AJAX:** `ajax/portal_get_packages.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_password.php`

**Module:** Customer Portal  
**Purpose:** Change password  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `users`  
**AJAX:** `ajax/portal_change_password.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_pay.php`

**Module:** Customer Portal  
**Purpose:** Make payment  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `payments`, `mpesa_transactions`  
**AJAX:** `ajax/portal_initiate_payment.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_payments.php`

**Module:** Customer Portal  
**Purpose:** Payment history  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `payments`  
**AJAX:** `ajax/portal_get_payments.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_profile.php`

**Module:** Customer Portal  
**Purpose:** My profile  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `customers`, `customer_addresses`  
**AJAX:** `ajax/portal_save_profile.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_purchase.php`

**Module:** Customer Portal  
**Purpose:** Purchase package  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `customer_subscriptions`, `invoices`, `mpesa_transactions`  
**AJAX:** `ajax/portal_purchase_package.php`, `ajax/portal_stk_push.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_register.php`

**Module:** Customer Portal  
**Purpose:** Self registration  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `customers`  
**AJAX:** `ajax/portal_register.php`

**Permission gate:**
```php
// Public access
// File: portal_register.php
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_ticket_view.php`

**Module:** Customer Portal  
**Purpose:** Ticket detail  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `ticket_messages`  
**AJAX:** `ajax/portal_reply_ticket.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_tickets.php`

**Module:** Customer Portal  
**Purpose:** Support tickets  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `tickets`  
**AJAX:** `ajax/portal_get_tickets.php`, `ajax/portal_create_ticket.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_usage.php`

**Module:** Customer Portal  
**Purpose:** Usage monitor  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `usage_aggregates_daily`, `pppoe_sessions`  
**AJAX:** `ajax/portal_usage_data.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

#### `portal_voucher.php`

**Module:** Customer Portal  
**Purpose:** Redeem voucher  
**Roles:** Customer  
**Layout:** `portal`  
**Tables:** `vouchers`, `voucher_usage`  
**AJAX:** `ajax/portal_redeem_voucher.php`

**Permission gate:**
```php
portalAuthRequired();
```

**Fields:**

| Field | Type | Required | Default | Notes |
|-------|------|----------|---------|-------|
| `customer_field` | varies | Yes | — | Portal-specific inputs |

**Buttons:**

| Button | Style | Action | Permission/Notes |
|--------|-------|--------|------------------|
| Submit | `primary` | AJAX | — |
| Back | `link` | portal_dashboard.php | — |


**Validation — Client:** Mobile-friendly validation

**Validation — Server:** Customer session scoped; customer_id from session only

**Success workflow:** Redirect or AJAX panel update

**Error workflow:** User-friendly error alert

**UI components:** Portal theme layout; Minimal navigation

---

## Appendix A — Shared DataTable Columns

List pages follow consistent column patterns:

| Entity | Default columns |
|--------|-----------------|
| customers | customer_number, full_name, phone, email, status, balance, group, created_at |
| routers | router_code, router_name, ip_address, status, cpu%, sessions, last_check |
| invoices | invoice_number, customer, issue_date, due_date, total, balance, status |
| payments | payment_ref, customer, method, amount, status, date |
| packages | package_code, name, service_type, price, cycle, status |
| tickets | ticket_number, subject, customer, priority, status, assigned, created |
| vouchers | voucher_code, batch, package, status, used_at |

## Appendix B — Chart.js Defaults

| Page | Chart type | Dataset |
|------|------------|---------|
| dashboard.php | line | Revenue 30-day trend |
| dashboard.php | bar | Active PPPoE + Hotspot sessions |
| dashboard.php | doughnut | Package distribution |
| report_revenue.php | line | Revenue by period |
| report_usage.php | bar | Data usage GB |
| report_collections.php | stacked bar | Collected vs outstanding |
| portal_dashboard.php | doughnut | Data quota remaining |

## Change Log

| Version | Date | Change |
|---------|------|--------|
| 1.0 | 2026-06-13 | Initial UI spec for 164 root pages |
