# Menu Module — Frontend Views & Related Files

## Overview

Menu module terdiri dari **tiga sub-sistem**: Frontend Menu (public site navigation), Backend Menu (admin sidebar), dan Menu Category (grouping). Dokumen ini fokus pada **Frontend Menu** — dari segi views, controller, helpers, dan view composers.

---

## Struktur Direktori

```
resources/views/backend/module/menu/
├── frontend/                    ← Admin UI untuk urus public/frontend menu
│   ├── index.blade.php          — Senarai semua menu items
│   ├── create.blade.php         — Tambah menu baru
│   ├── edit.blade.php           — Edit menu
│   ├── form.blade.php           — Partial form (digunakan create & edit)
│   ├── assign.blade.php         — Assign menu ke role (tree view + modal)
│   └── _tree_node.blade.php     — Recursive partial untuk tree node
├── frontendMenuCategory/        ← Admin UI untuk kategori menu
│   ├── index.blade.php
│   ├── create.blade.php
│   ├── edit.blade.php
│   └── form.blade.php
└── backend/                     ← Admin UI untuk backend/admin menu
```

---

## View Files — Frontend Menu

### 1. `index.blade.php` — Senarai Menu

**Senario:** Papar semua menu items dalam bentuk jadual dengan search, pagination, dan button delete.

| Feature | Implementation |
|---------|---------------|
| **Search** | Input `search` → auto-submit dalam 1500ms (debounce) via jQuery |
| **Filter** | Hidden form submit on search/keyword change |
| **Delete** | SweetAlert popup confirmation → submit `.delete-form` |
| **Pagination** | Laravel `$data->links('pagination::bootstrap-4')` |
| **Breadcrumb** | `$bcrum` — link ke Assign Menu, Create, dan senarai |

**Flow:**
1. Admin click "Create New Frontend Menu" → `route('frontend-menu.create')`
2. Admin click menu name → `route('frontend-menu.edit', $d->menu_id)`
3. Admin click "Assign Menu" → `route('frontend-menu.assign')`
4. Admin click trash icon → SweetAlert confirm → submit DELETE via POST

**Key code snippet — search debounce:**
```js
$('#search').on('keyup', function() {
    clearTimeout(delay);
    delay = setTimeout(function() {
        $('#form-search').submit();
    }, 1500);
});
```

---

### 2. `create.blade.php` — Tambah Menu Baru

**Senario:** Form untuk create frontend menu item.

- Extends `layouts.backend`
- Render `form.blade.php` partial
- Breadcrumb: Menu > Create
- Di-submit ke `route('frontend-menu.store')`

---

### 3. `edit.blade.php` — Edit Menu

**Senario:** Sama macam create tapi untuk edit.

- Extends `layouts.backend`
- Render `form.blade.php` partial
- Breadcrumb: Menu > Edit
- Di-submit ke `route('frontend-menu.update', $d->menu_id)`

---

### 4. `form.blade.php` — Partial Form

**Senario:** Digunakan oleh create & edit. Mengandungi field:

| Field | Type | Notes |
|-------|------|-------|
| `menu_name` | Text | Wajib diisi |
| `menu_parent_id` | Select | Dropdown parent menu — optional, untuk hierarchy |
| `menu_link` | Select with Select2 | Options dari `$route` + manual input |
| `menu_icon` | Text | CSS class icon (FontAwesome etc) |
| `menu_class` | Text | CSS class tambahan |
| `menu_status` | Select | Active/Inactive |

**Logic condition:**
- Jika page adalah `edit` → show `menu_id` in readonly
- Jika page adalah `create` → hidden field `menu_id` kosong

---

### 5. `assign.blade.php` — Assign Menu ke Role

**Senario:** Page utama untuk assign menu items ke role. Paling kompleks — mengandungi tree view, filtering, modal parent/child, dan interaksi jQuery.

#### Layout & Components

| Komponen | Description |
|----------|-------------|
| **Filter bar** | Form dengan `site_id`, `page_id`, `category_id` — auto-submit onChange |
| **Role tabs/buttons** | Loop `$role` — setiap role ada button. Active role disimpan dalam `$role_code` |
| **Tree list** | Recursive render guna `_tree_node.blade.php` untuk setiap root node |
| **Parent modal** | Modal untuk tambah parent menu — guna Select2 `$menus` |
| **Child modal** | Modal untuk tambah submenu — muncul bila click "+" pada tree node, atau auto-muncul bila URL ada parameter `parent_id` & `role_code` |

#### Flow Lengkap

```
1. Admin buka page → GET /admin/frontend-menu/assign-menu
   ├── filter context dari query string (site_id, page_id, category_id)
   ├── role_code dari query string (default: role pertama)
   └── tree data dari $tree (RoleMapping::nestedmenu())

2. Admin pilih role → klik button role → reload page dengan ?role_code=xxx

3. Admin tukar filter → dropdown change → auto-submit form → reload page
   └── Semua filter disimpan dalam query string

4. Admin click "+" pada tree node → parent_id + role_code set in URL
   └── Child modal auto-muncul (disebabkan JS detect parameter)

5. Admin submit child modal → POST ke route(frontend-menu.assign)
   └── Controller save → redirect balik dengan clean params (parent_id dibuang)
```

#### JavaScript Logic (inline, ~100 lines)

```js
// Select2 initialization
$('.select2').select2({ dropdownParent: ... });

// Filter auto-submit on change
$('.filter-auto-submit').on('change', function() { $('#form-filter').submit(); });

// Modal close handlers — backdrop click, escape key, close button
$(document).on('click', function(e) { ... });  // backdrop
$(document).on('keydown', function(e) { ... }); // ESC

// Wire submenu buttons
$('[data-submenu]').on('click', function() {
    var parent_id = $(this).data('parent');
    var role_code = $(this).data('role');
    window.location.href = updateQueryString('parent_id', parent_id);
});

// Auto-open child modal if URL has parent_id & role_code
var urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('parent_id') && urlParams.has('role_code')) {
    $('#childModal').modal('show');
}

// Close modal → remove parent_id from URL
$('#childModal').on('hidden.bs.modal', function() {
    window.location.href = removeQueryString('parent_id');
});

// SweetAlert delete confirmation
$('.btn-delete').on('click', function() { ... });
```

**Key fix — Child Modal Loop:**
Dulu lepas submit submenu, `parent_id` still ada dalam URL → modal auto-muncul semula. Fix:
- Controller buang `parent_id` dari redirect data: `unset($data['parent_id'])`
- Lepas submit, redirect URL takda `parent_id` → modal **tak muncul**

#### Modal Structure

**Parent Modal:**
- Form dengan select `$menus` (Select2) + hidden `role_code` + select `category_id`
- Submit → POST route `frontend-menu.assign`

**Child Modal:**
- Form dengan select `$menus` (Select2, filtered by parent)
- Hidden fields: `parent_id`, `role_code`, select `category_id`
- Submit → POST route `frontend-menu.assign`

---

### 6. `_tree_node.blade.php` — Recursive Tree Node

**Senario:** Partial yang panggil dirinya sendiri untuk render nested menu tree.

**Parameter:** `$items` (array of nodes)

| Element | Action |
|---------|--------|
| **Node row** | Menu name, icon, badge |
| **Toggle icon** | `route('frontend-menu.disabled', ...)` — enable/disable item |
| **Up/Down arrows** | `route('frontend-menu.level', ...)` — reorder |
| **Trash** | SweetAlert → submit form delete |
| **Add submenu (+)** | `data-submenu` — redirect dengan `parent_id` → trigger child modal |

**Recursive rendering:**
```blade
@foreach($items as $item)
    {{-- render node --}}
    @if(!empty($item['children']))
        @include('backend.module.menu.frontend._tree_node', [
            'items' => $item['children']
        ])
    @endif
@endforeach
```

---

## Related Backend Files

### `FrontendMenuController.php`

**Lokasi:** `app\Http\Controllers\Backend\FrontendMenuController.php`

| Method | Route | Purpose |
|--------|-------|---------|
| `index()` | `frontend-menu.index` | List menu with search, pagination |
| `create()` | `frontend-menu.create` | Show create form |
| `store()` | `frontend-menu.store` | Save new menu item |
| `edit($id)` | `frontend-menu.edit` | Show edit form |
| `update($id)` | `frontend-menu.update` | Update menu item |
| `destroy($id)` | `frontend-menu.destroy` | Delete menu item |
| `delete($id)` | `frontend-menu.delete` | POST-based delete |
| `assignMenu(Request)` | `frontend-menu.assign` | Assign menu to role (GET + POST) |
| `level(...)` | `frontend-menu.level` | Reorder (move up/down) |
| `disabled($id)` | `frontend-menu.disabled` | Toggle status |
| `toggleMapping(Request)` | (inline) | Toggle role mapping status |
| `articleItem()` | `frontend-menu.article-item` | Ajax — list articles |
| `sliderItem()` | `frontend-menu.slider-item` | Ajax — list sliders |

**Key logic in `assignMenu()`:**
```php
public function assignMenu(Request $request)
{
    $data = $this->menuFilter->filter($request);

    if ($request->isMethod('POST')) {
        RoleMapping::create($data);
        flash()->success('Menu assigned successfully.');

        unset($data['parent_id']); // penting — buang parent_id dari redirect

        return $this->menuFilter->redirectAfterSave($data);
    }

    $roleMapping = config('site.role_mapping');
    // ... load tree, roles, filters

    return view('backend.module.menu.frontend.assign', compact(...));
}
```

### `MenuFilterService.php`

**Lokasi:** `app\Services\MenuFilterService.php`

Service yang handle **filtering & redirect** untuk page assign menu. Di-inject ke `FrontendMenuController` sebagai `$this->menuFilter`.

| Method | Dipanggil di | Guna |
|--------|-------------|------|
| `getParam()` | `assignMenu()` (GET) | Ambil `role_code`, `menu_group`, `page_id`, `site_id`, `category_id`, `status` dari query string |
| `cleanParam()` | `redirectAfterSave()`, `buildTabLink()` | Buang parameter kosong/null/false dari array — pastikan URL bersih |
| `redirectAfterSave()` | `assignMenu()` (POST) | Redirect balik ke `frontend-menu.assign` dengan parameter relevant lepas save |
| `buildTabLink()` | View `assign.blade.php` | Build href untuk setiap tab menu set — gabung parameter sedia ada dengan `page_id`/`site_id` |

**Kenapa guna service berasingan?** — Supaya logic filtering & redirect boleh diguna semula dan senang di-test. Controller tak perlu重复 logic buang parameter kosong.

### `PortalHandler.php` — Render Engine untuk Public Site

**Lokasi:** `app\Services\PortalHandler.php`

Service utama yang render frontend site. **Ini yang kena setup** — dialah yang query menu assignments & pass ke Blade components.

```
PortalHandler::handle('my-site', 'about')
```

**Aliran menu dalam `handle()`:**

```
1. Query RoleMapping → role_code = 'public', site_id = current site
   ├── Filter: status = true
   ├── Order: sort
   └── Hanya ambil yang ada menu relation & menu_status = true

2. Build nested tree → guna recursion ikut parent_id → menu_id
   └── Hasil: $menuTree (array nested)

3. Fallback kalau tiada mapping:
   └── Guna semua page site sebagai flat menu (nama page → link)

4. Pass variables ke semua component/layout/header/footer:
   ├── $menus     — Collection flat semua menu item
   ├── $menuTree  — Array nested parent-child
   ├── $siteSlug  — Slug site semasa
   └── $siteName  — Nama site semasa
```

**Important:** PortalHandler guna `RoleMapping` + `Menu` model, filter by `category_id` (main-navbar). Ia query terus ikut `role_code` dan `site_id`.

#### Module Content Detail

PortalHandler hanya ada **satu method** — semua content module detail dimuat melalui page component guna query param `?id=`:

| Method | Fungsi |
|--------|--------|
| `handle($siteSlug, $pageSlug)` | Render FrontendPage biasa |

**Detail ContentArticle / PhotoGallery / Video:** Guna page component dengan `request()->query('id')` — lihat `docs/FRONTEND-COMPONENT-GUIDE.md` section 4.2 untuk contoh.

---

### `MenuHelper.php`

**Lokasi:** `app\Http\Helpers\MenuHelper.php`

Helper untuk **backend admin sidebar menu**. Bukan untuk frontend menu. Fungsi utama:

| Method | Purpose |
|--------|---------|
| `cached()` | Cache menu by role |
| `setBaseParam()` | Set parameter untuk route generation |
| `nestedmenu()` | Build nested tree dari `menuArray` |
| `getRouteList()` | Senarai route names untuk select dropdown |

Helper ini **independent** dari frontend menu — hanya guna untuk admin panel sidebar.

---

## View Composers

### 1. `MenuComposer.php`

**Lokasi:** `app/View/Composers/MenuComposer.php`

Legacy composer — masih digunakan oleh landing page navbar lama. Query terus dari `Menu` model, build nested menu, dan tambah hardcoded links (Home, Features, About, Login).

**Nota:** Ini adalah **fallback** — untuk sistem baru, guna `PublicMenuComposer`.

### 2. `PublicMenuComposer.php`

**Lokasi:** `app/View/Composers/PublicMenuComposer.php`

**Registered di:** `AppServiceProvider` → view `home.layouts.master`

| Parameter | Default |
|-----------|---------|
| `$roleCode` | `'public'` |
| `$categoryName` | `'main-navbar'` |

**Logic:**
1. Query `RoleMapping` where `role_code = 'public'`, `category_id = main-navbar category`
2. Jika ada data → display sebagai navigation items
3. Jika tiada data → fallback ke query `Menu` model direct
4. Hantar `$navItems` ke view

### 3. `FrontendUserMenuComposer.php`

**Lokasi:** `app/View/Composers/FrontendUserMenuComposer.php`

**Registered di:** `AppServiceProvider` → view `frontend-user.layouts.partials.sidebar`

| Parameter | Default |
|-----------|---------|
| `$menuSet` | `null` (ambil semua) |

**Logic:**
1. Ambil user dari guard `user`
2. Dapatkan role names via Spatie `getRoleNames()`
3. Query `RoleMapping` where `role_code` = user's roles, `category_id = sidebar category`
4. Build tree structure from `parent_id` → `menu_id` hierarchy
5. Resolve URL (route name → full URL atau `url()`)
6. Hantar `$menuTree` ke view (tree array with `children`)

---

## Database Tables

| Table | Purpose |
|-------|---------|
| `frontend_menu` | Senarai menu items (name, link, icon, parent, status) |
| `frontend_role_mapping` | Assignment menu ke role (role_code, category_id, menu_group, page_id, site_id, parent_id) |
| `frontend_menu_category` | Kategori menu (main-navbar, mobile-navbar, sidebar, footer) |

### Table: `frontend_role_mapping`

| Field | Type | Description |
|-------|------|-------------|
| `role_code` | string | Nama role (Spatie role name) |
| `category_id` | FK → `frontend_menu_category.category_id` | Kategori menu (main-navbar, sidebar, footer, dll) |
| `menu_group` | string | Sub-kumpulan dalam category |
| `menu_id` | FK → `frontend_menu.menu_id` | Menu item |
| `parent_id` | integer | Parent mapping ID (0 = root) |
| `sort` | integer | Urutan paparan |
| `page_id` | integer | Filter by page (0 = semua) |
| `site_id` | integer | Filter by site (0 = semua) |
| `status` | boolean | Active/inactive |

---

## Routes

| Method | URI | Permission | Name |
|--------|-----|------------|------|
| GET/POST | `/admin/frontend-menu` | `frontend-menu.view` | `frontend-menu.index` |
| GET | `/admin/frontend-menu/create` | `frontend-menu.create` | `frontend-menu.create` |
| POST | `/admin/frontend-menu` | `frontend-menu.create` | `frontend-menu.store` |
| GET | `/admin/frontend-menu/{id}/edit` | `frontend-menu.update` | `frontend-menu.edit` |
| PUT | `/admin/frontend-menu/{id}` | `frontend-menu.update` | `frontend-menu.update` |
| DELETE | `/admin/frontend-menu/{id}` | `frontend-menu.delete` | `frontend-menu.destroy` |
| POST | `/admin/frontend-menu/delete` | `frontend-menu.delete` | `frontend-menu.delete` |
| **GET/POST** | **`/admin/frontend-menu/assign-menu`** | `frontend-menu.update` | **`frontend-menu.assign`** |
| GET | `/admin/frontend-menu/level/{id}/{role}/{pos}/{sort}` | `frontend-menu.update` | `frontend-menu.level` |
| GET | `/admin/frontend-menu/disabled/{id}` | `frontend-menu.update` | `frontend-menu.disabled` |
| GET | `/admin/frontend-menu/article-item` | `frontend-menu.view` | `frontend-menu.article-item` |
| GET | `/admin/frontend-menu/slider-item` | `frontend-menu.view` | `frontend-menu.slider-item` |

Route utama adalah `frontend-menu.assign` — handle **GET** (display page) dan **POST** (save mapping).

---

## Aliran Data — Dari Admin ke Frontend

```
┌──────────────┐     ┌──────────────────┐     ┌─────────────────────┐
│  Admin Page  │────▶│  Controller       │────▶│  Database           │
│  assign.blade│     │  FrontendMenu     │     │  frontend_role_     │
│  .php        │     │  Controller.php   │     │  mapping            │
│              │     │                   │     │  frontend_menu      │
│  - CRUD menu │     │  - assignMenu()   │     └──────────┬──────────┘
│  - Assign    │     │  - store()        │                │
│  - Reorder   │     │  - level()        │                │
│  - Toggle    │     │  - disabled()     │                │
└──────────────┘     └──────────────────┘                │
                                                          │
                                ┌─────────────────────────┼──────────────────────┐
                                │                         │                      │
                                ▼                         ▼                      ▼
                  ┌───────────────────────┐  ┌─────────────────────┐  ┌──────────────────────┐
                   │  PortalHandler        │  │  View Composer      │  │  View Composer       │
                   │  (public site render) │  │  PublicMenuComposer │  │  FrontendUserMenu    │
                   │                       │  │  (landing page)     │  │  Composer            │
                   │  Query RoleMapping    │  │                     │  │  (user panel)        │
                    │  → role_code=public   │  │  Query RoleMapping  │  │                     │
                    │    site_id            │  │  → role_code=public │  │  Query RoleMapping   │
                   │    category_id=       │  │    category_id=     │  │  → role_code=user    │
                   │    main-navbar        │  │    main-navbar      │  │    category_id=      │
                   │                       │  │                     │  │    sidebar           │
                   │  Build $menuTree      │  │                     │  │                     │
                   │  Pass ke blade vars   │  │  Resolve named      │  │  Resolve routes      │
                  └──────────┬────────────┘  │  routes → URL       │  │  Resolve routes      │
                             │               └──────────┬──────────┘  └──────────┬───────────┘
                             │                          │                      │
                             └──────────────────────────┼──────────────────────┘
                                                        │
                                                        ▼
                                             ┌─────────────────────────┐
                                             │  Frontend View          │
                                             │                         │
                                             │  home/layouts/master    │
                                             │  frontend-user/         │
                                             │  layouts/partials/      │
                                             │  sidebar.blade.php      │
                                             │                         │
                                             │  Variable dari          │
                                             │  PortalHandler:         │
                                             │  $menus, $menuTree,     │
                                             │  $siteSlug, $siteName   │
                                             └─────────────────────────┘
```

---

## Common Issues & Fixes

| Issue | Cause | Fix |
|-------|-------|-----|
| Child modal muncul semula lepas submit | URL masih ada `parent_id` | `unset($data['parent_id'])` sebelum redirect |
| Tree tak update lepas assign | Cache | Clear cache atau refresh page |
| Menu tak muncul di frontend | Role mapping takde / status = 0 | Check `frontend_role_mapping` untuk role & status |
| Select2 dropdown tak muncul dalam modal | `dropdownParent` tak diset | Guna `dropdownParent: $('#parentModal')` |
