# Email Template Module

> Dokumen ini menerangkan implementasi semasa modul Email Template, Email
> Event, Manual Send, Email Log, Email Branding dan integrasi dengan notification
> serta Mailable dalam CMS.

## 1. Overview

Email transaksi dalam CMS menggunakan dua lapisan:

1. **Kod aplikasi** menentukan URL, token, penerima dan saat email dihantar.
2. **Database CMS** menyimpan subject, body HTML dan body teks yang boleh
   diedit oleh admin.

Modul ini menyediakan:

- Edit template transaksi sistem yang disediakan oleh kod
- Cipta template custom untuk manual send
- Assign template aktif kepada event automatik
- Hantar template secara manual kepada satu penerima
- Log penghantaran manual (berjaya/gagal)
- Branding header/footer gaya government untuk semua email

Template **tidak** menyimpan atau menjalankan Blade/PHP. Placeholder menggunakan
sintaks `[[token]]` dan nilai dimasukkan oleh `EmailTemplateService`. Nilai HTML
di-escape sebelum dimasukkan ke body HTML.

Menu admin:

```text
Email Templates
├── Email Templates   → /admin/email-template
├── Assign Templates  → /admin/email-event
├── Send Email        → /admin/email-send
├── Email Logs        → /admin/email-log
└── Email Branding    → /admin/email-branding
```

## 2. Architecture and data flow

```text
Notification / Mailable / Manual Send
                │
                ▼
     EmailTemplateService::render()
                │
     1. explicit template ID (optional)
     2. active event assignment
     3. active template_key row
                │
                ▼
     EmailTemplateMessage
     (subject + HTML + text)
                │
                ▼
        EmailTemplateMail
                │
                ▼
     emails.template + branding
                │
                ▼
          Laravel Mailer
```

Jika tiada template aktif atau rendering gagal, service memulangkan `null`.
Notification/Mailable kemudian menggunakan template asal dalam kod. Ini
menghalang template CMS yang rosak daripada memecahkan password reset atau
email verification.

## 3. Database schema

### 3.1 `email_templates`

| Column | Type | Notes |
|---|---|---|
| `id` | bigint PK | Primary key |
| `template_key` | string, unique | Kod kontrak dengan aplikasi; custom key bermula `custom.` |
| `template_name` | string | Nama paparan |
| `category` | string(100) | Menyimpan **code** kategori dari `ref`, default `General` |
| `subject` | string | Boleh mengandungi `[[token]]` |
| `body_html` | longText | HTML yang diedit melalui Summernote |
| `body_text` | longText, nullable | Jika kosong, dijana daripada HTML |
| `variables` | json, nullable | Metadata token yang didaftarkan |
| `is_active` | boolean | Template inactive tidak digunakan |
| `created_by` / `updated_by` | bigint, nullable | Admin user ID |
| `created_at` / `updated_at` | timestamp | - |

### 3.2 `email_template_assignments`

| Column | Type | Notes |
|---|---|---|
| `id` | bigint PK | Primary key |
| `event_key` | string, unique | Contoh: `admin.activate_account` |
| `template_id` | FK nullable | `nullOnDelete()`; null bermaksud guna default kod |
| `updated_by` | bigint, nullable | Admin yang update assignment |
| timestamps | - | - |

### 3.3 `email_logs`

| Column | Type | Notes |
|---|---|---|
| `id` | bigint PK | Primary key |
| `template_id` | FK nullable | Template yang digunakan |
| `event_key` | string, nullable | Disediakan untuk event automatik masa hadapan |
| `recipient` | string | Alamat penerima |
| `subject` | string, nullable | Subject selepas placeholder diganti |
| `status` | string(20) | `sent` atau `failed` |
| `error` | text, nullable | Exception message jika gagal |
| `sent_by` | bigint, nullable | Admin user ID untuk manual send |
| timestamps | - | `created_at` digunakan untuk susunan log |

Index: `created_at`.

## 4. Template keys and events

Registry dan default content berada dalam `config/email_templates.php`.

| Key | Category | Digunakan oleh | Variables | Required |
|---|---|---|---|---|
| `admin.password_reset` | Authentication | `AdminResetPassword` | `name`, `email`, `app_name`, `action_url`, `expires_in`, `site_url` | `action_url` |
| `user.password_reset` | Authentication | `FrontendResetPassword` | sama | `action_url` |
| `admin.verify_email` | Authentication | `TemplatedVerifyEmail('admin.verify_email')` | sama | `action_url` |
| `user.verify_email` | Authentication | `TemplatedVerifyEmail('user.verify_email')` | sama | `action_url` |
| `admin.activate_account` | Account | `AdminActivateAccount` | sama | `action_url` |
| `admin.temporary_password` | Account | `AdminTemporaryPassword` | `name`, `email`, `app_name`, `temporary_password`, `action_url`, `login_url`, `site_url` | `temporary_password`, `action_url` |
| `admin.password_reminder` | Account | `AdminPasswordChangeReminder` | `name`, `email`, `app_name`, `days`, `action_url`, `site_url` | `action_url` |
| `feedback.user_confirmation` | Feedback | `FeedbackSubmittedMail` type `user` | `name`, `email`, `app_name`, `ticket_no`, `subject`, `message`, `site_url` | `ticket_no`, `message` |
| `feedback.admin_notification` | Feedback | `FeedbackSubmittedMail` type `admin` | `name`, `email`, `app_name`, `ticket_no`, `subject`, `message`, `feedback_url`, `site_url` | `ticket_no`, `message` |
| `feedback.reply` | Feedback | `FeedbackSubmittedMail` type `reply` | `name`, `email`, `app_name`, `ticket_no`, `subject`, `message`, `site_url` | `ticket_no`, `message` |

`app_name` dan `site_url` sentiasa ditambah secara automatik oleh service.
Custom template menggunakan key `custom.<slug>`; jika slug sudah wujud, suffix
`-2`, `-3` dan seterusnya ditambah.

## 5. Categories (`EMAIL_TEMPLATE_CATEGORY`)

Category template adalah dynamic dari table `ref`:

```text
ref.cat   = EMAIL_TEMPLATE_CATEGORY
ref.code  = nilai yang disimpan dalam email_templates.category
ref.descr = label BM
ref.descr_en = label English
```

Data default (`RefDataSeeder`):

| Code | Description (BM) | Description (EN) | Sort |
|---|---|---|---|
| `General` | Umum | General | 1 |
| `Authentication` | Pengesahan | Authentication | 2 |
| `Account` | Akaun | Account | 3 |
| `Feedback` | Maklum Balas | Feedback | 4 |
| `Announcement` | Pengumuman | Announcement | 5 |

UI Email Template menggunakan `descr_en` sebagai label English. Code kekal
disimpan dalam database supaya menukar label tidak merosakkan template sedia ada.

Tambah kategori baru tanpa ubah kod:

```text
Admin → Parameters (/admin/ref) → Create
cat      : EMAIL_TEMPLATE_CATEGORY
code     : Billing
descr    : Bil
descr_en : Billing
sort     : 6
```

Jika group `ref` belum di-seed, `EmailTemplateService::categories()` fallback
kepada senarai category dalam config supaya form masih berfungsi. Validation
`EmailTemplateRequest` turut fallback kepada category config dalam keadaan itu.

## 6. Template library UI

### 6.1 Index — `/admin/email-template`

Column:

```text
# | Template | Key | Category | Subject | Status | Actions
```

- `#` ialah nombor baris mengikut hasil semasa
- Search mencari `template_name`, `template_key` dan `subject`
- Filter Category menggunakan code dari `ref`
- Search dan filter boleh digunakan serentak
- Semua template dipaparkan tanpa pagination dan disusun mengikut category,
  kemudian nama
- Action: Preview, Edit dan Delete
- Delete hanya dipaparkan untuk custom template

`EmailTemplateController::index()` memanggil `ensureDefaults()` sebelum query.
Ini memastikan template sistem baru wujud selepas deployment.

### 6.2 Create — `/admin/email-template/create`

Field:

| Field | Validation |
|---|---|
| Template Name | required, string, max 255 |
| Category | required, code mesti wujud dalam `EMAIL_TEMPLATE_CATEGORY` |
| Subject | required, max 255 |
| HTML Body | required, max 200,000 aksara |
| Plain Text Body | nullable, max 200,000 aksara |
| Active | boolean |

HTML body menggunakan Summernote. Placeholder chip boleh diklik untuk masukkan
token. `variables` disimpan daripada senarai placeholder yang tersedia.

Selepas create, admin diarah ke page Edit template tersebut.

### 6.3 Edit — `/admin/email-template/{id}/edit`

- Category boleh ditukar melalui dropdown untuk custom template
- Untuk system template, `ensureDefaults()` akan refresh category daripada
  `config/email_templates.php` pada lawatan index/event seterusnya
- Subject, HTML body, plain-text body dan status boleh dikemas kini
- Placeholder yang tidak dikenali akan ditolak
- Placeholder `required` yang tiada dalam subject/body akan ditolak
- System template tidak boleh ditukar `template_key`
- `Reset to default` tersedia untuk memulihkan kandungan default daripada config
- Preview membuka paparan HTML dalam iframe sandbox
- Test send menghantar sample data ke alamat yang dimasukkan; ia tidak mengubah
  template dan tidak menulis ke `email_logs`

### 6.4 Delete rules

- System template (key wujud dalam `config/email_templates.php`) tidak boleh
  dipadam; deactivate sebaliknya
- Custom template yang masih assigned kepada event tidak boleh dipadam
- Custom template yang tidak assigned boleh dipadam
- Menghapus template yang dirujuk log akan set `email_logs.template_id` kepada
  null melalui `nullOnDelete()`

## 7. Event assignment — `/admin/email-event`

Setiap event dalam registry dipaparkan bersama:

- Event key
- Nama event
- Category
- Description
- Dropdown template aktif
- Status assignment semasa

Admin boleh:

- Pilih template aktif untuk event
- Kosongkan pilihan untuk kembali kepada default template kod

`EmailEventController::update()`:

- Validate `event_key` mesti wujud dalam registry config
- Validate `template_id` mesti wujud dan **active**
- `updateOrCreate` row dalam `email_template_assignments`
- Simpan `updated_by`

Jika template yang di-assign kemudiannya inactive atau dipadam, service tidak
menggunakannya dan fallback ke row `template_key` yang aktif.

## 8. Manual send — `/admin/email-send`

Langkah:

1. Pilih template aktif.
2. Masukkan satu alamat penerima.
3. Isi semua placeholder yang dikesan daripada subject dan HTML body.
4. Hantar.

Validation:

- `template_id` required dan mesti wujud
- Template mesti masih active semasa submit
- `recipient` required dan format email sah
- `variables.*` nullable string, max 10,000 aksara
- Semua token template mesti diisi; token base seperti `app_name` dan `site_url`
  tidak perlu diisi oleh admin

Selepas render:

- Berjaya: tulis `email_logs` dengan status `sent`
- Gagal: tulis `email_logs` dengan status `failed` dan exception message
- Redirect kembali ke page Send Email dengan template yang sama

Manual send menghantar kepada **satu penerima** sahaja dan tidak menyokong
CC/BCC, attachment, scheduling atau queue.

## 9. Email logs — `/admin/email-log`

Log ini merekod **manual send** sahaja.

Column UI:

```text
Recipient | Subject | Template | Status | Sent
```

- Disusun `latest()` dan paginate 20 rekod
- Search mencari `recipient`, `subject` dan `event_key`
- `error` dan `sent_by` disimpan dalam database tetapi belum dipaparkan di UI
- `status = sent` bermaksud `Mail::send()` tidak throw exception; ia bukan bukti
  email sampai ke inbox penerima

Yang **belum** direkod secara automatik:

- Test email dari page Preview
- Password reset
- Email verification
- Admin activation dan temporary password
- Password-change reminder
- Feedback confirmation, admin notification dan reply

Jika `.env` menggunakan `MAIL_MAILER=log`, email sebenar ditulis ke
`storage/logs/laravel.log` walaupun status log CMS ialah `sent`.

## 10. Placeholder engine

Sintaks:

```text
[[name]]
[[email]]
[[app_name]]
[[site_url]]
[[action_url]]
[[expires_in]]
[[temporary_password]]
[[login_url]]
[[days]]
[[ticket_no]]
[[subject]]
[[message]]
[[feedback_url]]
```

Peraturan rendering (`EmailTemplateService`):

- Token regex: `[[token_name]]`; whitespace dalam bracket dibenarkan
- `app_name` dan `site_url` ditambah automatik melalui `baseData()`
- Nama site diambil daripada setting `site_name`, fallback `config('app.name')`
- Subject tidak di-escape; whitespace berturutan diringkaskan
- Body HTML: nilai di-escape menggunakan `htmlspecialchars`, newline menjadi
  `<br>`
- Body text: nilai tidak di-escape
- Jika `body_text` kosong, teks dijana daripada HTML
- Token yang tiada dalam data dikekalkan sebagai `[[token]]`
- HTML admin sendiri dianggap trusted; escaping hanya terpakai kepada nilai
  placeholder
- Rendering exception direport dan `render()` memulangkan `null`

Validation:

- Unknown placeholder ditolak untuk system template
- Required placeholder yang tiada ditolak
- Contoh: `admin.password_reset` memerlukan `action_url`
- Custom template boleh menggunakan mana-mana label daripada
  `config('email_templates.placeholders')`
- `EmailSendController` memeriksa semua token yang benar-benar digunakan dalam
  subject/HTML dan meminta admin mengisi yang belum diisi

## 11. Rendering and fallback chain

`EmailTemplateService::render($key, $data, $templateId = null)`:

1. Jika `$templateId` diberi, guna template itu jika **active**
2. Jika tidak, guna template yang di-assign kepada `event_key` jika **active**
3. Jika tidak, guna row `template_key` yang **active**
4. Jika tiada, return `null`
5. Render subject, HTML dan text
6. Jika exception berlaku, report dan return `null`

`assignedTemplate()` hanya memulangkan template aktif. Ini bermakna assignment
yang merujuk template inactive diabaikan secara automatik.

`ensureDefaults()`:

- Berjalan pada index template dan index event
- Cipta row sistem yang belum wujud
- Refresh metadata `template_name`, `category` dan `variables` untuk row sedia ada
- **Tidak** overwrite `subject`, `body_html`, `body_text` atau `is_active`
- Cipta assignment default jika belum wujud

`resetToDefault()` memulihkan nama, category, subject, HTML, text dan variables
daripada config untuk system template.

## 12. Notification integration

Semua notification berikut menggunakan concern
`App\Notifications\Concerns\UsesEmailTemplates`. Helper `templatedMail()`
memanggil service, kemudian menetapkan penerima secara eksplisit melalui
`routeNotificationFor('mail')` atau `recipientEmail()`.

Penetapan `->to($recipient)` ini penting kerana custom `Mailable` yang
dikembalikan daripada `toMail()` tidak menerima recipient automatik daripada
saluran notification Laravel.

| Notification | Template key | Data utama | Fallback |
|---|---|---|---|
| `AdminResetPassword` | `admin.password_reset` | `name`, `email`, `action_url`, `expires_in` | `parent::toMail()` |
| `FrontendResetPassword` | `user.password_reset` | sama | `MailMessage` dalam kod |
| `TemplatedVerifyEmail` | `admin.verify_email` / `user.verify_email` | sama | `parent::toMail()` |
| `AdminActivateAccount` | `admin.activate_account` | sama + optional `templateId` | `MailMessage` |
| `AdminTemporaryPassword` | `admin.temporary_password` | `temporary_password`, `action_url`, `login_url` | `MailMessage` |
| `AdminPasswordChangeReminder` | `admin.password_reminder` | `days`, `action_url` | `MailMessage` |

Nota:

- `AdminPasswordChangeReminder` sengaja sync, bukan `ShouldQueue`
- `AdminActivateAccount` dan `AdminTemporaryPassword` menerima `templateId`
  pilihan daripada create Backend User
- `BackendUser` override `sendPasswordResetNotification()` dan
  `sendEmailVerificationNotification()`
- `FrontendUser` override kedua-dua method yang sama
- Jika template inactive/tiada/rosak, email asal masih dihantar melalui fallback

### Backend User create integration

Create Backend User menyokong tiga mode:

| Mode | Email | Template selection |
|---|---|---|
| `manual` | Tiada email automatik | - |
| `temporary` | `AdminTemporaryPassword` | `temporary_password_template_id`, optional |
| `invite` | `AdminActivateAccount` | `activation_template_id`, optional |

Kedua-dua field divalidasi sebagai `exists:email_templates,id` dengan syarat
template active. Jika kosong, notification menggunakan fallback chain biasa.

## 13. Feedback mailables

`App\Mail\FeedbackSubmittedMail` menggunakan DB template mengikut type:

| Type | Template key | Recipient | Legacy fallback view |
|---|---|---|---|
| `user` | `feedback.user_confirmation` | Pengirim feedback | `emails.feedback.user-confirmation` |
| `admin` | `feedback.admin_notification` | Superadmin | `emails.feedback.admin-notification` |
| `reply` | `feedback.reply` | Pengirim feedback | `emails.feedback.reply-notification` |

Data asal feedback digabung dengan `feedback_url` (URL admin feedback) sebelum
render. Jika tiada template aktif, Mailable kembali kepada view legacy dan
subject default mengikut type.

Feedback masih menggunakan `Mail::send()`, bukan `Mail::queue()`, kerana
`QUEUE_CONNECTION=database` dan queue worker tidak semestinya berjalan.

## 14. Email branding

Page: **Email Templates → Email Branding**.

Field dan validation:

| Field | Setting key | Validation |
|---|---|---|
| Logo | `email_logo` | JPG/JPEG/PNG/WebP, max 15MB |
| Remove logo | - | boolean |
| Organisation name | `email_org_name` | required, max 255 |
| Subtitle / department | `email_org_subtitle` | nullable, max 255 |
| Address | `email_address` | nullable, max 500 |
| Phone | `email_phone` | nullable, max 100 |
| Contact email | `email_contact_email` | nullable, email, max 255 |
| Website | `email_website` | nullable, max 500 |
| Footer note | `email_footer_note` | nullable, max 1000 |
| Primary colour | `email_primary_color` | required, hex `#RRGGBB` |

Default:

```text
email_org_name      → setting site_name → config('app.name') → CMS
email_contact_email → config('mail.from.address')
email_website       → url('/')
email_footer_note   → This is an automated message. Please do not reply to this email.
email_primary_color → #1d4ed8
```

Logo:

- Disimpan pada disk `public` dalam `settings/email-branding`
- Fail logo upload lama dipadam apabila replace atau remove (path `dynaweb4/`
  dan URL luar tidak dipadam)
- Path `dynaweb4/...` dan `favicon...` menggunakan `asset()`
- URL `http://` / `https://` dikekalkan
- Path storage lain menggunakan `url(Storage::url($path))`
- URL mesti absolute kerana email client tidak menerima path relatif

Branding digunakan oleh:

```text
resources/views/emails/template.blade.php
resources/views/emails/template-text.blade.php
resources/views/vendor/mail/html/header.blade.php
resources/views/vendor/mail/html/footer.blade.php
```

Header memaparkan logo, nama organisasi, subtitle dan alamat. Bar warna
memisahkan header daripada body. Footer memaparkan contact details dan
disclaimer. Footer "CMS" telah dibuang daripada layout email.

## 15. Admin routes and permissions

| Method | Route | Permission |
|---|---|---|
| GET | `/admin/email-template` | `email-template.view` |
| GET | `/admin/email-template/create` | `email-template.create` |
| POST | `/admin/email-template` | `email-template.create` |
| GET | `/admin/email-template/{id}/edit` | `email-template.update` |
| PUT | `/admin/email-template/{id}` | `email-template.update` |
| DELETE | `/admin/email-template/{id}` | `email-template.delete` |
| GET | `/admin/email-template/{id}/preview` | `email-template.view` |
| POST | `/admin/email-template/{id}/test` | `email-template.send-test` |
| POST | `/admin/email-template/{id}/reset` | `email-template.update` |
| GET | `/admin/email-event` | `email-event.view` |
| POST | `/admin/email-event/update` | `email-event.update` |
| GET | `/admin/email-send` | `email-send.create` |
| POST | `/admin/email-send` | `email-send.create` |
| GET | `/admin/email-log` | `email-log.view` |
| GET | `/admin/email-branding` | `email-branding.view` |
| POST | `/admin/email-branding` | `email-branding.update` |

Semua route berada dalam kumpulan `auth:admin` + `admin`.

Permission list:

```text
email-template.view
email-template.create
email-template.update
email-template.delete
email-template.send-test
email-event.view
email-event.update
email-send.create
email-log.view
email-branding.view
email-branding.update
```

`BackendMenuSeeder` mencipta permission dan menu berikut:

```text
Email Templates
├── Email Templates
├── Assign Templates
├── Send Email
├── Email Logs
└── Email Branding
```

`AdminRolePermissionSeeder` turut mendaftarkan permission yang sama untuk
`super-admin`. `AutoPermissionController` memetakan `email-log` kepada model
`EmailLog`.

## 16. Setup, migration and seeding

Migration:

```text
2026_10_19_000001_create_email_templates_table.php
2026_10_19_000002_create_email_template_assignments_table.php
2026_10_19_000003_create_email_logs_table.php
```

Jangan padam fail migration walaupun table sudah wujud. Migration diperlukan
untuk fresh install, `migrate:fresh`, test `RefreshDatabase` dan deployment baru.

Setup selepas deploy:

```bash
php artisan migrate
php artisan db:seed --class=RefDataSeeder
php artisan db:seed --class=SettingSeeder
php artisan db:seed --class=EmailTemplateSeeder
php artisan db:seed --class=BackendMenuSeeder
php artisan optimize:clear
php artisan storage:link
```

Nota seeding:

- `RefDataSeeder` menambah `EMAIL_TEMPLATE_CATEGORY` dengan `updateOrCreate`
  berdasarkan `cat` + `code`; selamat dijalankan berulang
- `SettingSeeder` menggunakan `firstOrCreate`; nilai branding sedia ada tidak
  ditimpa
- `EmailTemplateSeeder` memanggil `ensureDefaults()`; ia mencipta template yang
  tiada dan refresh metadata, tetapi tidak menimpa subject/body admin
- `BackendMenuSeeder` mencipta permission dan menu; fresh install perlu juga
  `AdminRolePermissionSeeder`
- Jika deployment menggunakan cache, jalankan `php artisan optimize:clear`
- `storage:link` diperlukan untuk logo branding yang dimuat naik

Menambah system template baru:

1. Tambah definition dalam `config/email_templates.php`
2. Pastikan notification/Mailable menggunakan key tersebut
3. Jalankan `php artisan db:seed --class=EmailTemplateSeeder`
4. Template baru akan muncul dalam index

Menambah category baru:

1. Tambah row dalam `/admin/ref` dengan `cat = EMAIL_TEMPLATE_CATEGORY`
2. Isi `code`, `descr` (BM), `descr_en` (English) dan `sort`
3. Category terus muncul dalam dropdown dan filter; tiada deployment diperlukan

## 17. SMTP and mail transport

Modul ini tidak menyimpan konfigurasi SMTP. Mail transport dibaca daripada
`.env`.

Default pembangunan:

```env
MAIL_MAILER=log
```

Maksudnya email ditulis ke `storage/logs/laravel.log`, bukan dihantar ke
penerima sebenar. Untuk delivery sebenar:

```env
MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=...
MAIL_PASSWORD=...
MAIL_FROM_ADDRESS=no-reply@example.com
MAIL_FROM_NAME="Organisation Name"
```

Selepas ubah `.env`:

```bash
php artisan config:clear
```

Jika `MAIL_MAILER=log`, test send dan manual send tetap boleh menunjukkan
`sent` dalam CMS kerana tiada exception, walaupun email tidak dihantar ke luar.

## 18. Tests

Fail test:

```text
tests/Feature/EmailTemplateModuleTest.php
tests/Feature/EmailTemplateAuthMailTest.php
tests/Feature/EmailTemplateAdminFlowTest.php
tests/Feature/EmailBrandingTest.php
```

Liputan utama:

- System template seeding dan index
- Validation subject/body/placeholder
- Preview sample data
- Test send
- Escaping placeholder HTML
- Fallback template inactive
- Render `EmailTemplateMail`
- Admin create custom template
- Assign template kepada event
- Manual send dan email log
- Required variables manual send
- Backend User invite memilih activation template
- Event page, create page, send page dan log page
- Category filter
- Invalid category validation
- Password reset admin/frontend
- Email verification frontend
- Admin activation
- Recipient header pada notification
- Fallback notification apabila template inactive
- Feedback Mailable menggunakan DB template
- Branding page, upload logo dan layout email

Jalankan:

```bash
php artisan test --filter=EmailTemplate
php artisan test --filter=EmailBranding
```

Atau semua test email:

```bash
php artisan test --filter=EmailTemplate|EmailBranding
```

## 19. Limitations and operational notes

- Email log hanya untuk manual send; automatic notifications dan test send tidak
  direkod
- Tiada resend, attachment, CC/BCC, scheduled send atau bulk recipient
- `status = sent` bukan delivery receipt
- UI log belum memaparkan `error` dan `sent_by` walaupun data disimpan
- Template inactive tidak digunakan; fallback ke template_key aktif atau template
  kod asal
- System template tidak boleh dipadam
- Assigned custom template tidak boleh dipadam sehingga assignment ditukar
- `template_key` adalah kontrak kod; jangan rename tanpa update notification atau
  Mailable
- Category code tidak boleh rename secara sembarangan kerana ia disimpan dalam
  `email_templates.category`
- Mengubah `descr`/`descr_en` kategori adalah selamat; UI menggunakan label,
  database menggunakan code
- Placeholder HTML di-escape, tetapi HTML yang admin taip sendiri adalah trusted
- Jika `.env` menggunakan `MAIL_MAILER=log`, email tidak keluar dari server
- `AdminPasswordChangeReminder` sync; notification lain tidak di-queue secara
  automatik
- Queue worker mesti berjalan jika pada masa hadapan notification dijadikan
  `ShouldQueue`

## 20. File map

Models:

```text
app/Models/Backend/EmailTemplate.php
app/Models/Backend/EmailTemplateAssignment.php
app/Models/Backend/EmailLog.php
```

Services:

```text
app/Services/EmailTemplateService.php
app/Services/EmailBrandingService.php
```

Mail:

```text
app/Mail/EmailTemplateMail.php
app/Mail/EmailTemplateMessage.php
app/Mail/FeedbackSubmittedMail.php
```

Notifications:

```text
app/Notifications/Concerns/UsesEmailTemplates.php
app/Notifications/AdminResetPassword.php
app/Notifications/FrontendResetPassword.php
app/Notifications/TemplatedVerifyEmail.php
app/Notifications/AdminActivateAccount.php
app/Notifications/AdminTemporaryPassword.php
app/Notifications/AdminPasswordChangeReminder.php
```

Controllers:

```text
app/Http/Controllers/Backend/EmailTemplateController.php
app/Http/Controllers/Backend/EmailEventController.php
app/Http/Controllers/Backend/EmailSendController.php
app/Http/Controllers/Backend/EmailLogController.php
app/Http/Controllers/Backend/EmailBrandingController.php
```

Request:

```text
app/Http/Requests/Backend/EmailTemplateRequest.php
app/Http/Requests/Backend/UserRequest.php
```

Views:

```text
resources/views/emails/template.blade.php
resources/views/emails/template-text.blade.php
resources/views/emails/feedback/*.blade.php
resources/views/backend/module/emailTemplate/index.blade.php
resources/views/backend/module/emailTemplate/create.blade.php
resources/views/backend/module/emailTemplate/edit.blade.php
resources/views/backend/module/emailTemplate/preview.blade.php
resources/views/backend/module/emailTemplate/events.blade.php
resources/views/backend/module/emailTemplate/send.blade.php
resources/views/backend/module/emailTemplate/logs.blade.php
resources/views/backend/module/emailBranding/index.blade.php
resources/views/vendor/mail/html/header.blade.php
resources/views/vendor/mail/html/footer.blade.php
```

Config, migration and seeders:

```text
config/email_templates.php
database/migrations/2026_10_19_000001_create_email_templates_table.php
database/migrations/2026_10_19_000002_create_email_template_assignments_table.php
database/migrations/2026_10_19_000003_create_email_logs_table.php
database/seeders/EmailTemplateSeeder.php
database/seeders/RefDataSeeder.php
database/seeders/SettingSeeder.php
database/seeders/BackendMenuSeeder.php
database/seeders/AdminRolePermissionSeeder.php
```

Tests:

```text
tests/Feature/EmailTemplateModuleTest.php
tests/Feature/EmailTemplateAuthMailTest.php
tests/Feature/EmailTemplateAdminFlowTest.php
tests/Feature/EmailBrandingTest.php
```





