# Content Feedback Module

## 1. Pengenalan

Module ticketing/feedback — admin boleh create feedback untuk user, reply, update status, dan export data. Tidak seperti modul content lain, module ini **tiada multi-language support** (single language).

### Architecture

Guna **2-table ticketing system** (standard industri macam Zendesk, Freshdesk):

```
content_feedback_main (ticket utama)
    └── content_feedback_messages (reply/chat)
```

Bila admin reply, dia **create row baru** dalam `content_feedback_messages`, bukan update column `feedback_reply` dalam main table.

---

## 2. Database

### Migration

**File:** `database/migrations/2026_06_11_000001_create_content_feedback_tables.php`

```php
Schema::create('content_feedback_main', function (Blueprint $table) {
    $table->id();
    $table->string('ticket_no', 50)->unique();
    $table->string('name', 255)->nullable();
    $table->string('email', 255)->nullable();
    $table->string('type', 255)->nullable();       // Ref: FEEDBACK_TYPE
    $table->string('cat', 255)->nullable();         // Ref: FEEDBACK_CATEGORY
    $table->string('subject', 255)->nullable();
    $table->text('feedback');
    $table->string('status', 255)->default('PENDING'); // Ref: FEEDBACK_STATUS
    $table->string('created_user_type', 50)->nullable();
    $table->timestamps();
    $table->unsignedBigInteger('created_by')->nullable();
    $table->unsignedBigInteger('updated_by')->nullable();
    $table->string('updated_user_type', 50)->nullable();
});

Schema::create('content_feedback_messages', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('feedback_id');
    $table->text('message');
    $table->string('read_status', 50)->default('UNREAD');
    $table->string('send_to', 255)->nullable();       // 'user' / 'admin'
    $table->string('created_user_type', 50)->nullable();
    $table->timestamp('created_at')->nullable();
    $table->unsignedBigInteger('created_by')->nullable();

    $table->foreign('feedback_id')
          ->references('id')->on('content_feedback_main')
          ->onDelete('cascade');  // auto-delete messages bila main ticket delete
});
```

### Table: `content_feedback_main`

| Column | Type | Description |
|--------|------|-------------|
| id | bigint unsigned (PK) | Primary key |
| ticket_no | varchar(50) (unique) | Auto-generated: `FDB-YYYYMMDD-XXXX` |
| name | varchar(255) (nullable) | Nama pengirim |
| email | varchar(255) (nullable) | Email pengirim |
| type | varchar(255) (nullable) | Ref: `FEEDBACK_TYPE` |
| cat | varchar(255) (nullable) | Ref: `FEEDBACK_CATEGORY` |
| subject | varchar(255) (nullable) | Subjek feedback |
| feedback | text | Isi feedback |
| status | varchar(255) | Default `PENDING`, Ref: `FEEDBACK_STATUS` |
| created_user_type | varchar(50) (nullable) | `admin` atau `user` |
| created_by | bigint unsigned (nullable) | FK → `backend_users.id` |
| updated_by | bigint unsigned (nullable) | FK → `backend_users.id` |
| updated_user_type | varchar(50) (nullable) | `admin` atau `user` |
| created_at | timestamp | |
| updated_at | timestamp | |

### Table: `content_feedback_messages`

| Column | Type | Description |
|--------|------|-------------|
| id | bigint unsigned (PK) | Primary key |
| feedback_id | bigint unsigned | FK → `content_feedback_main.id` (cascade delete) |
| message | text | Isi reply |
| read_status | varchar(50) | Default `UNREAD` |
| send_to | varchar(255) (nullable) | `user` atau `admin` |
| created_user_type | varchar(50) (nullable) | `admin` atau `user` |
| created_at | timestamp | |
| created_by | bigint unsigned (nullable) | FK → `backend_users.id` |

---

## 3. Routes

| Method | URI | Name | Permission |
|--------|-----|------|------------|
| GET | `/admin/content-feedback` | `content-feedback.index` | `content-feedback.view` |
| GET | `/admin/content-feedback/create` | `content-feedback.create` | `content-feedback.create` |
| POST | `/admin/content-feedback` | `content-feedback.store` | `content-feedback.create` |
| GET | `/admin/content-feedback/{id}/edit` | `content-feedback.edit` | `content-feedback.update` |
| PUT | `/admin/content-feedback/{id}` | `content-feedback.update` | `content-feedback.update` |
| DELETE | `/admin/content-feedback/{id}` | `content-feedback.destroy` | `content-feedback.delete` |
| GET | `/admin/content-feedback/export` | `content-feedback.export` | `content-feedback.export` |

```php
Route::group(['middleware' => ['auth:admin', 'permission:content-feedback.view,admin']], function () {
    Route::get('/content-feedback', 'index')->name('content-feedback.index');
    Route::get('/content-feedback/create', 'create')->middleware('permission:content-feedback.create,admin')->name('content-feedback.create');
    Route::post('/content-feedback', 'store')->middleware('permission:content-feedback.create,admin')->name('content-feedback.store');
    Route::get('/content-feedback/{id}/edit', 'edit')->middleware('permission:content-feedback.update,admin')->name('content-feedback.edit');
    Route::put('/content-feedback/{id}', 'update')->middleware('permission:content-feedback.update,admin')->name('content-feedback.update');
    Route::delete('/content-feedback/{id}', 'destroy')->middleware('permission:content-feedback.delete,admin')->name('content-feedback.destroy');
    Route::get('/content-feedback/export', 'export')->middleware('permission:content-feedback.export,admin')->name('content-feedback.export');
});
```

---

## 4. Controller

**File:** `app/Http/Controllers/Backend/ContentFeedbackController.php`

### index()

Guna `withCount` untuk kira unread messages. Filter by search, date range, status, type.

```php
$feedbacks = ContentFeedbackMain::query()
    ->withCount(['messages' => fn($q) => $q->where('read_status', 'UNREAD')])
    ->when($search, function ($q) use ($search) {
        $q->where(function ($sub) use ($search) {
            $sub->where('ticket_no', 'like', "%{$search}%")
                ->orWhere('name', 'like', "%{$search}%")
                ->orWhere('email', 'like', "%{$search}%")
                ->orWhere('subject', 'like', "%{$search}%")
                ->orWhere('feedback', 'like', "%{$search}%");
        });
    })
    ->when($from, fn($q) => $q->whereDate('created_at', '>=', $from))
    ->when($to, fn($q) => $q->whereDate('created_at', '<=', $to))
    ->when($statusFilter, fn($q) => $q->where('status', $statusFilter))
    ->when($typeFilter, fn($q) => $q->where('type', $typeFilter))
    ->orderBy('created_at', 'desc')
    ->paginate(10)->onEachSide(1)->withQueryString();
```

### store() — Create & Email

Auto-generate ticket number, then send 2 emails:

```php
$feedback = ContentFeedbackMain::create([
    'ticket_no' => ContentFeedbackMain::generateTicketNo(),
    'name' => $request->name,
    'email' => $request->email,
    'type' => $request->type,
    'cat' => $request->cat,
    'subject' => $request->subject,
    'feedback' => $request->feedback,
    'status' => 'PENDING',
    'created_user_type' => 'admin',
    'created_by' => auth()->id(),
]);

// Email confirmation to user
if ($feedback->email) {
    Mail::to($feedback->email)->send(
        new FeedbackSubmittedMail([...], 'user')
    );
}

// Email notification to all superadmins
$admins = BackendUser::role('superadmin')->get();
foreach ($admins as $admin) {
    Mail::to($admin->email)->send(
        new FeedbackSubmittedMail([...], 'admin')
    );
}
```

### edit() — Auto-mark as READ

```php
$feedback = ContentFeedbackMain::with('messages')->findOrFail($id);

// PENDING → READ bila admin buka ticket
if ($feedback->status === 'PENDING') {
    $feedback->update(['status' => 'READ', 'updated_by' => auth()->id()]);
}

// Mark unread admin messages as READ
ContentFeedbackMessage::where('feedback_id', $id)
    ->where('read_status', 'UNREAD')
    ->where('send_to', 'admin')
    ->update(['read_status' => 'READ']);
```

### update() — Reply & Status

```php
$data = [
    'type' => $request->type ?? $feedback->type,
    'cat' => $request->cat ?? $feedback->cat,
    'status' => $request->status ?? $feedback->status,
    'updated_by' => auth()->id(),
    'updated_user_type' => 'admin',
];

if ($request->filled('reply_message')) {
    ContentFeedbackMessage::create([
        'feedback_id' => $feedback->id,
        'message' => $request->reply_message,
        'read_status' => 'UNREAD',
        'send_to' => 'user',
        'created_user_type' => 'admin',
        'created_at' => now(),
        'created_by' => auth()->id(),
    ]);

    $data['status'] = 'REPLIED';  // auto-update status

    if ($feedback->email) {
        Mail::to($feedback->email)->send(
            new FeedbackSubmittedMail([...], 'reply')
        );
    }
}

$feedback->update($data);
```

### export()

Accept `format` param and delegate to export classes:

```php
return match ($format) {
    'xlsx' => app(FeedbackExport::class)->xlsx($from, $to, $statusFilter, $typeFilter),
    'pdf'  => app(FeedbackPdfExport::class)->pdf($from, $to, $statusFilter, $typeFilter),
    default => app(FeedbackExport::class)->csv($from, $to, $statusFilter, $typeFilter),
};
```

---

## 5. Models

### ContentFeedbackMain

**File:** `app/Models/Backend/ContentFeedbackMain.php` | **Table:** `content_feedback_main`

```php
#[Table('content_feedback_main')]
#[Fillable(['ticket_no', 'name', 'email', 'type', 'cat', 'subject', 'feedback', 'status',
            'created_user_type', 'created_by', 'updated_by', 'updated_user_type'])]
class ContentFeedbackMain extends Model
{
    use HasFactory;

    public function messages(): HasMany
    {
        return $this->hasMany(ContentFeedbackMessage::class, 'feedback_id', 'id');
    }

    public static function generateTicketNo(): string
    {
        $prefix = 'FDB';
        $date = now()->format('Ymd');
        $last = static::whereDate('created_at', today())->count();
        $seq = str_pad($last + 1, 4, '0', STR_PAD_LEFT);
        return "{$prefix}-{$date}-{$seq}";
        // Contoh: FDB-20260611-0001
    }
}
```

### ContentFeedbackMessage

**File:** `app/Models/Backend/ContentFeedbackMessage.php` | **Table:** `content_feedback_messages`

```php
#[Table('content_feedback_messages')]
#[Fillable(['feedback_id', 'message', 'read_status', 'send_to', 'created_user_type', 'created_at', 'created_by'])]
class ContentFeedbackMessage extends Model
{
    use HasFactory;

    public $timestamps = false;  // created_at di-set manual

    public function feedback(): BelongsTo
    {
        return $this->belongsTo(ContentFeedbackMain::class, 'feedback_id', 'id');
    }

    public function sender(): BelongsTo
    {
        return $this->belongsTo(BackendUser::class, 'created_by');
    }

    public function getSenderLabelAttribute(): string
    {
        $name = $this->sender
            ? trim($this->sender->first_name . ' ' . $this->sender->last_name)
            : null;
        $role = $this->sender?->roles->first()?->name;

        return $name && $role ? "$name ($role)" : ($name ?: 'Admin');
    }
}
```

Aksesor `sender_label` dipanggil secara automatik — `$msg->sender_label` → **"Nizar (Super Admin)"**.

---

## 6. Status Flow & Color Coding

```
PENDING ──(admin click edit/pencil)──→ READ
READ ──(admin hantar reply)──→ REPLIED
PENDING / READ / REPLIED ──(admin tukar manual)──→ CLOSED
```

| Status | Maksud | CSS Classes | Warna |
|--------|--------|-------------|-------|
| PENDING | Belum dibaca | `bg-warning-50 text-warning-600` | Yellow |
| READ | Dah dibaca, belum reply | `bg-error-50 text-error-600` | Red |
| REPLIED | Dah dibalas | `bg-success-50 text-success-600` | Green |
| CLOSED | Selesai | `bg-gray-100 text-gray-600` | Gray |

```php
$statusClass = match($feedback->status) {
    'PENDING' => 'bg-warning-50 text-warning-600',
    'READ'    => 'bg-error-50 text-error-600',
    'REPLIED' => 'bg-success-50 text-success-600',
    'CLOSED'  => 'bg-gray-100 text-gray-600',
    default   => 'bg-gray-100 text-gray-600',
};
```

---

## 7. Email Notifications

**Mailable:** `app/Mail/FeedbackSubmittedMail.php`

```php
public function envelope(): Envelope
{
    return match($this->type) {
        'admin' => new Envelope(subject: 'New Feedback: ' . ($this->data['subject'] ?? 'No Subject')),
        'reply' => new Envelope(subject: 'Feedback Reply: ' . ($this->data['ticket_no'] ?? '')),
        default => new Envelope(subject: 'Thank You! Your Feedback Has Been Received'),
    };
}

public function content(): Content
{
    $view = match($this->type) {
        'admin' => 'emails.feedback.admin-notification',
        'reply' => 'emails.feedback.reply-notification',
        default => 'emails.feedback.user-confirmation',
    };
    return new Content(view: $view, with: ['data' => $this->data]);
}
```

| Type | Recipient | Subject | View |
|------|-----------|---------|------|
| `user` | Pengirim feedback | "Thank You! Your Feedback Has Been Received" | `emails.feedback.user-confirmation` |
| `admin` | All superadmin users | "New Feedback: {subject}" | `emails.feedback.admin-notification` |
| `reply` | Pengirim feedback | "Feedback Reply: {ticket_no}" | `emails.feedback.reply-notification` |

**Important:** Guna `Mail::send()` bukan `Mail::queue()` sebab `QUEUE_CONNECTION=database`. Queue tak akan process sampai worker jalan.

---

## 8. Exports

**Export classes:** `app/Exports/FeedbackExport.php`, `app/Exports/FeedbackPdfExport.php`

| Format | Library | Method |
|--------|---------|--------|
| CSV | Native PHP (`fputcsv`) | `FeedbackExport::csv()` |
| XLSX | OpenSpout | `FeedbackExport::xlsx()` |
| PDF | Dompdf | `FeedbackPdfExport::pdf()` |

Export respect semua active filters (date range, status, type) — kalau user filter dulu, data yang diexport ikut filter tu.

---

## 9. Validation

**File:** `app/Http/Requests/Backend/ContentFeedbackRequest.php`

```php
public function rules(): array
{
    return [
        'name'     => $this->isMethod('POST') ? 'required|string|max:255' : 'nullable',
        'email'    => 'nullable|email|max:255',
        'subject'  => $this->isMethod('POST') ? 'required|string|max:255' : 'nullable',
        'feedback' => $this->isMethod('POST') ? 'required|string' : 'nullable',
        'type'     => 'nullable|string',
        'cat'      => 'nullable|string',
        'status'   => 'nullable|string',
        'reply_message' => 'nullable|string',
    ];
}
```

Guna `$this->isMethod('POST')` untuk bezakan validation masa create vs update.

---

## 10. Views — Key Design Decisions

### index.blade.php

- **Filter form guna `GET`** — URL boleh dibookmark, support back button
- **Date picker** guna `onclick="this.showPicker()"` + `appearance-none` + ikon kalendar — consistent dengan module ContentArticle
- **Export dropdown** guna Alpine.js (`x-data`, `x-show`, `@click.outside`) — dropdown button dengan 3 pilihan format
- **Status badges** guna match expression untuk color coding

### create.blade.php

- Form layout guna grid 2-column macam modul content lain — consistent UX
- Type + Category guna select dropdown dari Ref table
- Cancel/Save button **dalam card** (bukan luar)

### edit.blade.php

- **Ticket Information Card** — guna grid 2-column, setiap value dalam `bg-gray-50` container (bezakan label vs value)
- **Conversation Card** — chat bubbles:
  - Admin → right-aligned, brand color (`bg-brand-50`)
  - User → left-aligned, gray (`bg-gray-50`)
  - Label guna `$msg->sender_label` → **"Nama (Role)"**
- **Reply/Update Form** — Type & Category **readonly** (tukar ke text display + hidden input) sebab user tak sepatutnya ubah kategori lepas tiket dah jadi

---

## 11. Seeder

### RefDataSeeder

```php
// FEEDBACK_STATUS
['cat' => 'FEEDBACK_STATUS', 'code' => 'PENDING', 'descr' => 'Pending', ...],
['cat' => 'FEEDBACK_STATUS', 'code' => 'READ', 'descr' => 'Read', ...],
['cat' => 'FEEDBACK_STATUS', 'code' => 'REPLIED', 'descr' => 'Replied', ...],
['cat' => 'FEEDBACK_STATUS', 'code' => 'CLOSED', 'descr' => 'Closed', ...],

// FEEDBACK_TYPE
['cat' => 'FEEDBACK_TYPE', 'code' => 'INQUIRY', 'descr' => 'Inquiry', ...],
['cat' => 'FEEDBACK_TYPE', 'code' => 'FEEDBACK', 'descr' => 'Feedback', ...],
['cat' => 'FEEDBACK_TYPE', 'code' => 'COMPLAINT', 'descr' => 'Complaint', ...],
['cat' => 'FEEDBACK_TYPE', 'code' => 'SUGGESTION', 'descr' => 'Suggestion', ...],
['cat' => 'FEEDBACK_TYPE', 'code' => 'REQUEST', 'descr' => 'Request', ...],

// FEEDBACK_CATEGORY
['cat' => 'FEEDBACK_CATEGORY', 'code' => 'ACCOUNTS', 'descr' => 'Accounts', ...],
['cat' => 'FEEDBACK_CATEGORY', 'code' => 'OTHERS', 'descr' => 'Others', ...],
```

### BackendMenuSeeder

- Menu: **Feedback** — icon `fa-solid fa-comment`, diletak bawah **Videos** dalam group Content
- Permissions: `content-feedback.view`, `.create`, `.update`, `.delete`, `.export`
- Assigned to role: **super-admin** (sahaja buat masa ni)

---

## 12. Complete Flow

```text
Admin create ticket → POST /content-feedback
  ├── Generate ticket_no (FDB-20260611-0001)
  ├── Simpan ke content_feedback_main (status PENDING)
  ├── Email confirmation ke user
  └── Email notification ke semua superadmin

Admin buka ticket → GET /content-feedback/{id}/edit
  ├── Status auto jadi READ
  ├── Unread messages (send_to=admin) auto mark READ
  └── Display ticket info + conversation

Admin reply → PUT /content-feedback/{id}
  ├── Simpan message ke content_feedback_messages
  ├── Status auto jadi REPLIED
  └── Email reply ke user

Admin tutup → PUT /content-feedback/{id} (tukar status CLOSED)

Admin export → GET /content-feedback/export?format=xlsx&from=...&to=...
  └── Download CSV / Excel / PDF (ikut filter aktif)
```
