# Contact Support Feature — Developer Reference

---

## 1. Feature Overview

The Contact Support module allows users (both authenticated and guest) to submit inquiries to the
support team via a web form. Admins can view, manage status, add internal notes, and receive email
notifications for every new submission. A dedicated Support Center section is provided in the admin
panel with a dashboard and listing page.

**Key Capabilities:**

| Capability | Detail |
|---|---|
| Public / User form | `/contact-us` — any visitor can submit |
| Attachment support | JPG, JPEG, PNG, WEBP, PDF, DOC, DOCX — max 5 MB |
| Acknowledgement email | Sent to the submitter on every submission |
| Admin notification email | Sent only to the configured admin user |
| Admin listing | Searchable, filterable, sortable, paginated table |
| Admin detail view | Full inquiry details + status management + internal notes |
| Support Center Dashboard | Stats cards + pie chart + latest 10 records |

---

## 2. Business Rules

1. **Anyone can submit** — no authentication is required to fill the form.
2. **Authenticated user ID is captured** — if a logged-in user submits, their `user_id` is stored.
3. **Single attachment** — only one file per inquiry; the field is optional.
4. **Acknowledgement is always sent** — to authenticated users via `->notify()`, to guests via
   `Notification::route('mail', email)`.
5. **Only one admin receives notification** — controlled by `CONTACT_SUPPORT_ADMIN_ID` env var.
6. **If configured admin does not exist** — email is silently skipped, a warning is logged.
7. **Email failures are non-fatal** — DB transaction commits first; email errors are caught and logged.
8. **Status changes do not trigger emails** — purely internal.
9. **Internal notes are admin-only** — never exposed to the submitter.
10. **Soft deletes** — deleted records are not permanently removed; attachment file is deleted from disk.
11. **No ticket number, no conversation threads** — simple inquiry-response model only.

---

## 3. Database Structure

### `contact_support`

| Column | Type | Notes |
|---|---|---|
| `id` | bigint unsigned PK | Auto-increment |
| `user_id` | bigint unsigned | Nullable FK → `users.id`, null on user delete |
| `first_name` | varchar(100) | Required |
| `last_name` | varchar(100) | Required |
| `email` | varchar(100) | Required; contact email |
| `subject` | varchar(255) | Required |
| `message` | text | Required |
| `attachment` | varchar | Nullable; stored filename only |
| `status` | enum | `open` (default), `in_progress`, `closed` |
| `deleted_at` | timestamp | Soft deletes |
| `created_at` | timestamp | — |
| `updated_at` | timestamp | — |

**Indexes:** `status`, `email`, `created_at`

> **Design note:** The project uses `first_name` / `last_name` as the standard name pattern across
> all models (User, etc.). The form follows this same split-name convention for consistency.

---

### `contact_support_notes`

| Column | Type | Notes |
|---|---|---|
| `id` | bigint unsigned PK | Auto-increment |
| `contact_support_id` | bigint unsigned | FK → `contact_support.id`, cascade delete |
| `admin_id` | bigint unsigned | FK → `users.id`, cascade delete |
| `note` | text | Required |
| `created_at` | timestamp | — |
| `updated_at` | timestamp | — |

---

## 4. Relationships

```
User ──────────────────────┐ (optional, nullable)
                           ▼
                  contact_support (1)
                       │
                       │ hasMany
                       ▼
               contact_support_notes (*)
                       │
                       │ belongsTo admin (User)
                       ▼
                     users
```

| Model | Relationship | Method |
|---|---|---|
| `ContactSupport` | `belongsTo(User::class)` | `user()` |
| `ContactSupport` | `hasMany(ContactSupportNote::class)` | `notes()` |
| `ContactSupportNote` | `belongsTo(ContactSupport::class)` | `contactSupport()` |
| `ContactSupportNote` | `belongsTo(User::class, 'admin_id')` | `admin()` |

---

## 5. Routes

### Frontend (user-facing) — `routes/user.php`

| Method | URI | Name | Controller |
|---|---|---|---|
| GET | `/contact-us` | `contact-support.create` | `User\ContactSupport\ContactSupportController@create` |
| POST | `/contact-us` | `contact-support.store` | `User\ContactSupport\ContactSupportController@store` |

### Admin — `routes/admin.php` (under `auth:admin` middleware)

| Method | URI | Name | Controller |
|---|---|---|---|
| GET | `admin/support-center/dashboard` | `admin.support-center.dashboard` | `Admin\SupportCenter\DashboardController@index` |
| GET | `admin/contact-support` | `admin.contact-support.index` | `Admin\ContactSupport\ContactSupportController@index` |
| GET | `admin/contact-support/{id}` | `admin.contact-support.show` | `Admin\ContactSupport\ContactSupportController@show` |
| PUT | `admin/contact-support/{id}` | `admin.contact-support.update` | `Admin\ContactSupport\ContactSupportController@update` |
| DELETE | `admin/contact-support/{id}` | `admin.contact-support.destroy` | `Admin\ContactSupport\ContactSupportController@destroy` |
| POST | `admin/contact-support/{id}/notes` | `admin.contact-support.notes.store` | `Admin\ContactSupport\ContactSupportController@storeNote` |

---

## 6. Controllers

### `App\Http\Controllers\User\ContactSupport\ContactSupportController`

| Method | Description |
|---|---|
| `create()` | Renders the public contact form. Passes the authenticated user (or `null`) for pre-filling. |
| `store(StoreContactSupportRequest)` | Validates, delegates to `ContactSupportService::store()`, returns JSON. |

### `App\Http\Controllers\Admin\ContactSupport\ContactSupportController`

| Method | Description |
|---|---|
| `index(Request)` | Full page on normal request; returns rendered table partial HTML on AJAX. |
| `show(int $id)` | Loads record with relations (`user`, `notes.admin`), renders detail view. |
| `update(UpdateContactSupportRequest, int $id)` | Updates status; returns JSON response. |
| `destroy(int $id)` | Soft-deletes the record (also removes attachment file); returns JSON. |
| `storeNote(StoreContactSupportNoteRequest, int $id)` | Adds internal note; returns JSON with rendered HTML partial. |

### `App\Http\Controllers\Admin\SupportCenter\DashboardController`

| Method | Description |
|---|---|
| `index(Request)` | Fetches dashboard stats and latest 10 records; renders dashboard view. |

---

## 7. Models

### `App\Models\ContactSupport`

**Traits:** `SoftDeletes`

**Constants:**
```php
const STATUSES = ['open', 'in_progress', 'closed'];
```

**Accessors:**

| Accessor | Returns |
|---|---|
| `full_name` | `"{first_name} {last_name}"` trimmed |
| `attachment_url` | Full public URL from `contactSupport` disk, or `null` |
| `contact_email` | `email` column; falls back to linked user's email |
| `status_label` | Translated status string via `__('pages/contact-support.status.{status}')` |
| `status_badge_class` | Tailwind CSS classes for the status badge |
| `formatted_created_at` | `"M d, Y H:i:s"` format |
| `formatted_updated_at` | `"M d, Y H:i:s"` format |

### `App\Models\ContactSupportNote`

**Accessors:**

| Accessor | Returns |
|---|---|
| `formatted_created_at` | `"M d, Y H:i:s"` format |

---

## 8. Services

### `App\Services\ContactSupportService`

All public methods wrap their logic in `try/catch` and re-throw as generic `Exception`.

| Method | Parameters | Returns | Description |
|---|---|---|---|
| `index(Request)` | — | `LengthAwarePaginator` | Filtered, sorted, paginated listing |
| `store(array, ?UploadedFile, $user)` | — | `ContactSupport` | Creates record, stores attachment, sends emails |
| `find(int\|string)` | — | `ContactSupport` | Eager-loads `user` and `notes.admin` |
| `updateStatus(int\|string, string)` | — | `ContactSupport` | Updates status within transaction |
| `addNote(int\|string, string, int)` | — | `ContactSupportNote` | Creates note with loaded `admin` relation |
| `destroy(int\|string)` | — | `bool` | Deletes attachment file + soft-deletes record |
| `allCount()` | — | `int` | Total record count |
| `dashboardStats()` | — | `array` | `{total, open, in_progress, closed}` |
| `latestRecords(int $limit=10)` | — | `Collection` | Latest N records with `user` |

**Protected helpers:**
- `storeAttachment(UploadedFile)` — Generates a random filename `cs-{16chars}.{ext}` and stores on `contactSupport` disk.
- `notifySubmitter(ContactSupport, $user, array)` — Notifies authenticated user or routes to guest email.
- `notifyAdmin(ContactSupport)` — Reads `config('contact-support.admin_id')`, finds User, sends notification; logs warning and returns if not found.

---

## 9. Requests (Form Validation)

### `StoreContactSupportRequest`

| Field | Rules |
|---|---|
| `first_name` | required, string, min:3, max:100, regex (name pattern) |
| `last_name` | required, string, min:3, max:100, regex (name pattern) |
| `email` | required, email:rfc,dns, max:255 |
| `subject` | required, string, min:3, max:255 |
| `message` | required, string, min:10, max:1000 |
| `attachment` | nullable, mimes:jpg,jpeg,png,webp,pdf,doc,docx, max:5120 (5 MB) |

All validation lengths are pulled from `config('validation-rules.*')` for centralised control.

### `UpdateContactSupportRequest`

| Field | Rules |
|---|---|
| `status` | required, string, `Rule::in(ContactSupport::STATUSES)` |

### `StoreContactSupportNoteRequest`

| Field | Rules |
|---|---|
| `note` | required, string, min:1, max:1000 |

---

## 10. Dashboard Functionality

**Route:** `admin.support-center.dashboard`

**Stats cards** (driven by `ContactSupportService::dashboardStats()`):

| Card | Description |
|---|---|
| Total Messages | All records (no status filter) |
| Open Messages | `status = 'open'` count |
| In Progress | `status = 'in_progress'` count |
| Closed Messages | `status = 'closed'` count |

**Chart:** Doughnut chart (`Chart.js`) showing Open / In Progress / Closed distribution with
percentages in tooltips.

**Latest Records Table:** 10 most recent records; columns: ID, Name, Email, Subject, Status,
Submitted Date, Action (View link).

---

## 11. Contact Support Workflow

```
Visitor fills form
        │
        ▼
StoreContactSupportRequest validates
        │
        ├─ 422 → JS validation errors displayed inline
        │
        ▼
ContactSupportService::store()
        │
        ├─ DB::beginTransaction()
        │
        ├─ ContactSupport::create(…)          ← status = 'open'
        │
        ├─ if attachment → storeAttachment()   ← writes to public/uploads/contact-support/
        │                   record->update(['attachment' => filename])
        │
        ├─ DB::commit()
        │
        ├─ notifySubmitter()                   ← non-fatal; caught if fails
        │
        └─ notifyAdmin()                       ← non-fatal; caught if fails
                │
                └─ success → JSON { message }  ← form resets on frontend
```

---

## 12. Status Workflow

| Status | Meaning |
|---|---|
| `open` | Default on submission; awaiting review |
| `in_progress` | Admin is actively investigating |
| `closed` | Resolved / no further action needed |

Admin changes status via the status panel on the show page. The action is a `PUT` AJAX request to
`admin.contact-support.update`. On success the page reloads to reflect updated badges. **No emails
are sent on status changes.**

---

## 13. Internal Notes Workflow

- Notes are created via `POST admin/contact-support/{id}/notes`.
- `admin_id` is automatically taken from `auth()->id()` (the currently logged-in admin).
- The response returns both a success message and the rendered `note-item` partial HTML.
- The front-end injects the HTML at the top of `#notes-list` and removes the "no notes" empty message.
- Notes are **never deleted** and **never visible to users**.

---

## 14. File Upload Handling

**Filesystem disk:** `contactSupport`  
**Physical path:** `public/uploads/contact-support/`  
**Public URL:** `APP_URL/uploads/contact-support/{filename}`

**Stored filename format:** `cs-{16-char-random-lowercase}.{original_extension}`

**On destroy:** `Storage::disk('contactSupport')->delete($record->attachment)` is called before soft-delete.

**Allowed MIME types (validated server-side):** `jpg, jpeg, png, webp, pdf, doc, docx`  
**Maximum size:** 5 120 KB (5 MB)

The storage disk is configured in `config/filesystems.php` under the key `contactSupport`.

---

## 15. Email Notification Flow

### Submitter Acknowledgement

- **Class:** `App\Notifications\ContactSupportSubmitted`
- **View:** `emails.contact-support.submitted`
- **Subject:** `pages/contact-support.email.subject` — `"We received your inquiry — {name}"`
- **Triggered by:** `ContactSupportService::notifySubmitter()`
- **Delivery:**
  - Authenticated user → `$user->notify(new ContactSupportSubmitted($record))`
  - Guest → `Notification::route('mail', $data['email'])->notify(...)`

### Admin Notification

- **Class:** `App\Notifications\ContactSupportAdminNotification`
- **View:** `emails.contact-support.admin-notification`
- **Subject:** `pages/contact-support.email.admin_subject` — `"New Contact Support inquiry received"`
- **Triggered by:** `ContactSupportService::notifyAdmin()`
- **Recipient:** `User::find(config('contact-support.admin_id'))`
- **Failure handling:** If user not found → `Log::warning(...)` + return (no exception thrown)

---

## 16. Environment Variables

| Variable | Default | Description |
|---|---|---|
| `CONTACT_SUPPORT_ADMIN_ID` | `1` | User ID of the admin who receives notification emails |

Add to `.env` and `.env.example`:

```env
CONTACT_SUPPORT_ADMIN_ID=1
```

---

## 17. Configuration Setup

**File:** `config/contact-support.php`

```php
return [
    'admin_id' => env('CONTACT_SUPPORT_ADMIN_ID', 1),
];
```

Throughout the codebase always read via `config('contact-support.admin_id')`, never `env(...)` directly.

**Filesystem disk** (in `config/filesystems.php`):

```php
'contactSupport' => [
    'driver'     => 'local',
    'root'       => public_path('uploads/contact-support'),
    'url'        => env('APP_URL') . '/uploads/contact-support',
    'visibility' => 'public',
    'throw'      => false,
],
```

---

## 18. Admin Access Rules

- **Middleware:** `auth:admin` + `prevent-back-history`
- **Permissions module:** None — the project does not use a permissions module.
- **All authenticated admins** can access all Contact Support and Support Center routes.
- No role or permission gates are applied to these routes.

---

## 19. Admin Screens

| Screen | Route | Description |
|---|---|---|
| Support Center Dashboard | `admin.support-center.dashboard` | Stats, chart, latest 10 records |
| Contact Support Listing | `admin.contact-support.index` | Filterable/sortable AJAX table |
| Contact Support Detail | `admin.contact-support.show` | Full view + status + notes |

### Navigation Structure (config/admin-nav.php)

```
Support Center
├── Dashboard           admin.support-center.dashboard
└── Contact Support     admin.contact-support.index
```

---

## 20. Admin Navigation

Defined in `config/admin-nav.php`. The `Support Center` group uses icon `heroicon-o-lifebuoy` with two children:

```php
[
    'label'    => 'Support Center',
    'icon'     => 'heroicon-o-lifebuoy',
    'children' => [
        ['label' => 'Dashboard',        'route' => 'admin.support-center.dashboard', 'icon' => 'heroicon-o-chart-pie'],
        ['label' => 'Contact Support',  'route' => 'admin.contact-support.index',    'icon' => 'heroicon-o-envelope'],
    ],
],
```

---

## 21. Translation Keys Added

**File:** `lang/en/pages/contact-support.php`

All keys are nested under `pages/contact-support`. Key groups:

| Group | Keys |
|---|---|
| Root | `title`, `singular`, `create_title`, `create_subtitle`, `support`, `message_submitted`, `guest_email_hint`, `attachment_hint`, `view_attachment`, `no_results_filtered` |
| `labels` | `id`, `user_id`, `user`, `user_details`, `first_name`, `last_name`, `email`, `subject`, `message`, `attachment`, `status`, `submitted_date`, `updated_date`, `created_date`, `actions`, `internal_notes`, `inquiry_information`, `timeline`, `back_to_list`, `note`, `add_note`, `no_notes` |
| `placeholders` | `message`, `subject`, `note` |
| `filters` | `all_status`, `date_from`, `date_to` |
| `confirm` | `delete_title`, `delete_description` |
| `messages` | `deleted`, `load_failed`, `details_failed`, `update_failed`, `delete_failed`, `updated`, `deleted_singular`, `submit_failed`, `note_added`, `note_failed`, `server_validation_failed` |
| `status` | `open`, `in_progress`, `closed` |
| `email` | `subject`, `heading`, `subheading`, `greeting`, `intro`, `details`, `notice`, `admin_subject`, `admin_heading`, `admin_subheading`, `admin_greeting`, `admin_intro`, `admin_submitter_details`, `admin_user_info`, `admin_name`, `admin_email`, `admin_notice` |
| `noDataAvailable` | `title`, `description` |
| `dashboard` | `title`, `subtitle`, `support_center_label`, `dashboard_label`, `total`, `open`, `in_progress`, `closed`, `chart_title`, `chart_sub`, `recent`, `latest_records`, `view_all`, `no_recent` |
| `modal` | `details` |

To add a new language (e.g., `ar`), create `lang/ar/pages/contact-support.php` with translated values.
RTL layout is already handled at the project level via the existing `dir` attribute on `<html>`.

---

## 22. Testing Checklist

### Submission

- [ ] Guest submits form — record created with `user_id = null`
- [ ] Authenticated user submits — `user_id` stored correctly
- [ ] Attachment upload (JPG, PDF, DOCX) — stored and URL accessible
- [ ] Invalid file type (e.g., `.exe`) — rejected with validation error
- [ ] File > 5 MB — rejected with validation error
- [ ] Missing required fields — validation errors shown inline
- [ ] Acknowledgement email received by submitter
- [ ] Admin notification email received by configured admin

### Admin Listing

- [ ] Page loads and AJAX table renders
- [ ] Search by name/email/subject filters correctly
- [ ] Status filter works for all three statuses
- [ ] Date range filter (from/to) works
- [ ] Sort by Name, Email, Subject, Status, Submitted Date, Updated Date
- [ ] Pagination works
- [ ] Delete action triggers confirmation dialog and removes record
- [ ] Empty state appears when no records exist

### Admin Detail

- [ ] User information section shows correct data
- [ ] Guest inquiry shows `user_id: —`
- [ ] Attachment link is shown when an attachment exists
- [ ] Status change from Open → In Progress → Closed works
- [ ] Status change updates badge after page reload
- [ ] Adding a note saves and appears at top of notes list
- [ ] Notes list shows admin name and timestamp
- [ ] "No notes" message disappears after first note is added

### Dashboard

- [ ] Stats cards show correct counts
- [ ] Pie chart renders with correct data
- [ ] Latest 10 records table displays correctly
- [ ] "View All" link navigates to listing page

### Email Failure Graceful Handling

- [ ] Set `CONTACT_SUPPORT_ADMIN_ID` to a non-existent user ID → submission succeeds, warning in logs

### RTL / Localization

- [ ] Switch app to a RTL language — admin and frontend views render correctly
- [ ] All labels use `__()` translations (no hardcoded English in templates)

---

## 23. Future Extension Points

| Extension | Guidance |
|---|---|
| **New status values** | Add to `ContactSupport::STATUSES` constant, the migration enum, the lang file `status` array, and the badge class `match`. |
| **Status change emails** | Add a new notification class and call it inside `ContactSupportService::updateStatus()`. |
| **Multiple admin recipients** | Change `contact-support.admin_id` to an array; iterate and notify each. |
| **File download (private)** | Move disk to `local` (non-public), add a signed-URL download route with `auth:admin` middleware. |
| **User-facing history** | Create a `user_contact_support` listing route and controller with `where('user_id', auth()->id())` scope. |
| **Note deletion** | Add `DELETE admin/contact-support/{id}/notes/{noteId}` route and soft-delete to the notes table. |
| **Support Tickets** | Implement in a separate branch. The `support_tickets` model/migration is not present in this branch — do not add it here. |
| **Queue email notifications** | Both notification classes already use `Queueable`. Set `QUEUE_CONNECTION=redis` and dispatch; no code change needed. |
| **Pagination per-page control** | The listing already uses `$request->input('per_page', 15)`. Expose a per-page selector in the UI if needed. |
| **Search by user ID** | Add `orWhere('user_id', ...)` to the search clause in `ContactSupportService::index()`. |
