# Recent Updates — Dark Mode, Global Search, Responsiveness & UI

> Dokumentasi perubahan terbaru sistem — merangkumi dark mode setting, global search, dashboard responsiveness,
> dan UI tweaks pada department index.

---

## 1. Dark Mode — Dynamic Sidebar Colors & Bottom Gap Fix

### Latar Belakang

Sebelum ini, dark mode cuma tukar warna sidebar via Tailwind class `dark:bg-black` (hitam pekat `#000000`).
Content area guna `bg-gray-900` (Tailwind) — jadi nampak **"gap" warna berbeza** antara sidebar dengan main content.
Juga, warna sidebar tak boleh dikustomisasi dari Settings page untuk dark mode.

### Perubahan Dilakukan

#### a) SettingSeeder — Dark Mode Defaults (Filament Palette)

**File:** `database/seeders/SettingSeeder.php`

Nilai default dark mode ditukar daripada hitam pekat ke Filament-inspired palette:

| Key | Sebelum (Dark) | Selepas (Filament) |
|---|---|---|
| `sidebar_bg_color_dark` | `#000000` | `#1d2939` (gray-800) |
| `sidebar_text_color_dark` | `#d1d5db` | `#d1d5db` (kekal) |
| `sidebar_active_bg_color_dark` | `#1e3a5f` | `#374151` (gray-700) |
| `sidebar_active_text_color_dark` | `#38bdf8` | `#ffffff` |

Guna `firstOrCreate` — kalau setting dah wujud, dia skip.

#### b) Sidebar — CSS Variables & Dynamic Styles

**File:** `resources/views/backend/layouts/partials/sidebar.blade.php`

Sidebar define CSS custom properties untuk warna:

```css
:root {
    --sidebar-bg-light: {{ $sidebarBgColor }};       /* #ffffff default */
    --sidebar-bg-dark: {{ $sidebarBgColorDark }};    /* #1d2939 default */
}
```

Kemudian guna untuk semua element sidebar:
- `.sidebar-dynamic-bg` — bg untuk light & dark mode
- `.menu-item-inactive` / `.menu-dropdown-item-inactive` — text color
- `.menu-item-icon-inactive` — icon fill color
- `.menu-item-arrow-inactive` — arrow stroke color
- `.menu-item-active` / `.menu-dropdown-item-active` — active state bg + text
- Sama untuk icon-active dan arrow-active

Semua ada pasangan `.dark` selector.

#### c) app.css — Bottom Gap Fix

**File:** `resources/css/app.css`

```css
.dark body,
.dark .flex.h-screen,
.dark .relative.flex-col.flex-1,
.dark main {
    background-color: var(--sidebar-bg-dark, #0f172a);
}
```

Ini penting sebab:
- `body` — base background
- `.flex.h-screen` — wrapper sidebar + content
- `.relative.flex-col.flex-1` — content wrapper
- `main` — actual content area

Keempat-empat guna `var(--sidebar-bg-dark)` yang sama dengan sidebar, jadi nampak **seamless**,
tiada gap warna putih atau kelabu berbeza.

#### d) Setting View — 8 Color Pickers

**File:** `resources/views/backend/module/theme/theme-setting/index.blade.php`

Tab **Admin Panel** ada 4 pasang color picker (Light + Dark):

```
Sidebar Background      → Light ☐  Dark ☐
Sidebar Text & Icon     → Light ☐  Dark ☐
Sidebar Active Bg       → Light ☐  Dark ☐
Sidebar Active Text     → Light ☐  Dark ☐
```

Guna HTML `<input type="color">` — bila submit, `ThemeSettingController@update` save guna `SettingHelper::set()`.

#### e) Cache System

**File:** `app/Http/Helpers/SettingHelper.php`

```php
get() → Cache::rememberForever('settings', fn() => Setting::pluck('value', 'key'))
set() → Setting::updateOrCreate(...) → Cache::forget('settings')
```

Semua setting di-cache forever. Lepas update, cache di-flush supaya next page load ambil value baru.

### Flow End-to-End

```
User buka Settings → Admin Panel tab → tukar sidebar_bg_color_dark
  → submit form
  → SettingController@update
    → SettingHelper::set('sidebar_bg_color_dark', '#1d2939')
    → Setting::updateOrCreate → Cache::forget('settings')
  → redirect balik

Loading seterusnya:
  → sidebar.blade.php @php: SettingHelper::get(...)
    → Cache miss → query DB → Cache::rememberForever
  → :root { --sidebar-bg-dark: #1d2939 }
  → .dark .sidebar-dynamic-bg { background-color: #1d2939 }
  → .dark body, main { background-color: #1d2939 }
  → Sidebar & content area sama warna → seamless
```

---

## 2. Global Search

### Architecture Overview

```
User types "keyword" di search bar header
  → Alpine @keyup (debounced 300ms)
  → fetch /admin/global-search?q=keyword
  → GlobalSearchController@search
    → 13 method search berasingan
    → Return JSON [{label, type, url}]
  → Alpine render grouped dropdown
```

### a) Controller — GlobalSearchController

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

**Route:**
```php
Route::get('/admin/global-search', [GlobalSearchController::class, 'search'])
    ->name('admin.global-search')
    ->middleware('auth:admin');
```

**Method `search(Request)`:**
1. Validasi `q` — mesti ≥ 2 characters
2. Init `$results = []`
3. Panggil 13 method search, masing-masing pass `$results` by reference
4. Return `response()->json($results)`

**Method `add(&$results, $label, $type, $url)`:**
```php
$results[] = [
    'label' => $label,    // Display text
    'type'  => $type,     // Group name (e.g. "Articles", "Departments")
    'url'   => $url,      // Link ke edit page
];
```

**13 Modules yang dicari:**

| Method | Model | Search Fields | Label Format |
|---|---|---|---|
| `searchArticles` | `ContentArticle` | `article_code`, translation title | `[code] title` |
| `searchDownloads` | `ContentDownload` | `download_title`, `download_category` | title |
| `searchImages` | `ContentImage` | `image_title`, `image_cat` | title |
| `searchVideos` | `ContentVideo` | `video_code`, translation title | `[code] title` |
| `searchSliders` | `ContentSlider` | translation title | title or `Slider #id` |
| `searchPublicHolidays` | `ContentPublicHoliday` | `public_holiday_title` | title |
| `searchCalendars` | `ContentCalendar` | translation title | title or `Calendar #id` |
| `searchDepartments` | `ContentDirectoryDepartment` | `directory_department_name`, `_name_en`, `_code` | `[code] name` |
| `searchStaff` | `ContentDirectoryStaff` | `directory_staff_name`, `_email`, `_position` | `name — position` |
| `searchFeedback` | `ContentFeedbackMain` | `ticket_no`, `name`, `email`, `subject` | `[ticket] name — subject` |
| `searchBackendUsers` | `BackendUser` | `username`, `email`, `first_name`, `last_name` | `Full Name (username)` |
| `searchFrontendUsers` | `FrontendUser` | `username`, `email`, `first_name`, `last_name` | `Full Name (username)` |
| `searchRefs` | `Ref` | `cat`, `code`, `descr`, `descr_en` | `[cat] code — descr` |

**Common pattern setiap method:**
```php
$items = Model::where(function ($b) use ($q) {
    $b->where('field', 'LIKE', "%{$q}%")
      ->orWhere('field2', 'LIKE', "%{$q}%");
})
->limit($this->limit)  // max 5 per module
->get();

foreach ($items as $m) {
    $this->add($results, $label, $type, $url);
}
```

> **Important:** Guna `foreach` loop, bukan `->each(fn() => ...)`. Arrow function PHP (`fn()`) capture variable by value,
> jadi `$results` tak akan berubah. Ini adalah **root cause** kenapa search tak return hasil pada percubaan pertama.

### b) Frontend — Alpine Component

**File:** `resources/views/backend/layouts/partials/header.blade.php`

**Component structure:**
```html
<div x-data="{
    query: '',
    results: [],
    open: false,
    highlightedIndex: -1,
    ...
}">
    <input type="text"
        @keyup.debounce.300ms="fetch"
        @focus="open = results.length > 0"
        @keydown.down.prevent="highlightedIndex++"
        @keydown.up.prevent="highlightedIndex--"
        @keydown.enter.prevent="goToHighlighted"
        @keydown.escape.prevent="open = false"
        placeholder="Search keyword..." />
    
    <!-- Dropdown -->
    <div x-show="open" @click.outside="open = false">
        <template x-for="(group, gIdx) in groupedResults">
            <div>
                <div class="..." x-text="group.type"></div>
                <template x-for="(item, i) in group.items">
                    <a :href="item.url" x-text="item.label"></a>
                </template>
            </div>
        </template>
    </div>
</div>
```

**Key Alpine properties:**
- `query` — current search string
- `results` — raw array dari API
- `open` — toggle dropdown
- `highlightedIndex` — keyboard navigation index (merentasi semua group)
- `groupedResults` — computed getter yang group `results` by `type`

**Grouping getter:**
```js
groupedResults() {
    let groups = {};
    this.results.forEach(item => {
        if (!groups[item.type]) groups[item.type] = [];
        groups[item.type].push(item);
    });
    return Object.entries(groups).map(([type, items]) => ({ type, items }));
}
```

**Flat list untuk keyboard nav:**
```js
flatResults() {
    return this.groupedResults.flatMap(g => g.items);
}
```

**Keyboard navigation:**
- **Arrow Down** → `highlightedIndex++` (wrap)
- **Arrow Up** → `highlightedIndex--` (wrap)
- **Enter** → `window.location.href = flatResults[highlightedIndex].url`
- **Escape / click outside** → `open = false`

**Styling di tiap item:**
```html
<a :class="{ 'bg-gray-100 dark:bg-gray-700': globalIndex === idx }">
```
Item yang di-highlight ada background berbeza.

**Debounce:**
Guna `@keyup.debounce.300ms` supaya tak bombard API setiap kali user taip. Tunggu 300ms selepas last keyup baru fetch.

### c) JSON Response Example

```json
[
    {"label": "[PENT] Pusat Pentadbiran", "type": "Departments", "url": "/admin/content-directory-department/1/edit"},
    {"label": "Super Admin (superadmin)", "type": "Backend Users", "url": "/admin/backend-user/1/edit"},
    {"label": "[FDB-20260611-0001] MUHAMMAD", "type": "Feedback", "url": "/admin/content-feedback/1/edit"}
]
```

### d) Files Changed

| File | Change |
|---|---|
| `app/Http/Controllers/Backend/GlobalSearchController.php` | **New** — Controller with 13 search methods |
| `routes/web.php` | **Modified** — Added global-search route |
| `resources/views/backend/layouts/partials/header.blade.php` | **Modified** — Alpine search component |
| `public/tailadmin/src/js/index.js` | **Modified** — Removed old search-button handlers |

---

## 3. Dashboard Responsiveness Fix

### Problem

Dashboard grid guna Tailwind breakpoint `sm:` (640px) untuk layout switching.
Pada screen 641px–767px, widget-card masih dalam grid tapi content (charts, tables) melimpah keluar container.

### Solution

**File:** `resources/views/backend/module/home.blade.php`

a) Breakpoint raised from `sm:` to `md:` (768px):
```html
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6 md:grid-cols-2">
```

b) `overflow-x: auto` pada widget-card untuk elak chart clipping:
```html
<div class="widget-card" style="overflow-x: auto;">
```

Ini cover semua saiz phone/tablet (below 768px) — content jadi horizontally scrollable dalam card.

---

## 4. Department Index — Parent Row Font Bold

### Problem

Parent rows dalam ContentDirectoryDepartment index tiada visual distinction daripada child rows.
User susah nak beza mana parent, mana child dalam hierarchy.

### Solution

**File:** `resources/views/backend/module/contentDirectoryDepartment/index.blade.php`

Parent rows (where `directory_department_main == 1`) guna class `font-bold`:

```blade
<tr class="{{ $department->directory_department_main == 1 ? 'font-bold' : '' }}">
```

Sebelum ni guna `bg-brand-50` (background color) — ditukar ke `font-bold` sebab:
- Lebih standard (parent-child distinction via typography)
- Tak bergantung pada brand color yang mungkin berubah
- Better accessibility — bold lebih dikenali sebagai penanda hierarchy

---

## 5. Cara Tambah Module Baru Ke Global Search

Bila ada module baru, cuma perlu buat **3 perubahan** dalam `GlobalSearchController`:

### Langkah 1 — Import Model

```php
use App\Models\Backend\YourNewModel;
```

### Langkah 2 — Panggil Method Dalam `search()`

```php
// Line ~48, dalam search() method
$this->searchYourModule($q, $results);
```

### Langkah 3 — Create Search Method

```php
protected function searchYourModule(string $q, array &$results): void
{
    $items = YourNewModel::where(function ($b) use ($q) {
            $b->where('field1', 'LIKE', "%{$q}%")
              ->orWhere('field2', 'LIKE', "%{$q}%");
        })
        ->limit($this->limit)
        ->get();

    foreach ($items as $m) {
        $this->add(
            $results,
            $m->field1,                              // label — display text
            'Your Module Type',                       // type — group name dalam dropdown
            route('your-module.edit', $m->id)          // url — link ke edit page
        );
    }
}
```

### Rule of Thumb
| Perkara | Kenapa |
|---|---|
| Guna `foreach`, jangan `fn()` arrow function | Arrow function PHP capture `$results` **by value**, jadi perubahan tak sampai ke array asal |
| `$this->limit` (default 5) | Max results per module, elak response terlalu besar |
| Type mesti konsisten | Frontend group guna field `type` — guna nama yang sama untuk grouping proper |
| `&$results` dalam parameter | Pass by reference — penting supaya `add()` ubah array yang sama |
| Jangan lupa refresh cache browser | Alpine mungkin cache response lama |

### Contoh Lengkap

Nak tambah module `ContentPhotoGallery`:

```php
// 1. Import
use App\Models\Backend\ContentPhotoGallery;

// 2. Panggil dalam search()
$this->searchPhotoGalleries($q, $results);

// 3. Method
protected function searchPhotoGalleries(string $q, array &$results): void
{
    $items = ContentPhotoGallery::whereHas('translations', fn($b) => $b->where('gallery_translation_title', 'LIKE', "%{$q}%"))
        ->with('translations')
        ->limit($this->limit)
        ->get();

    foreach ($items as $m) {
        $this->add(
            $results,
            $m->translations->first()?->gallery_translation_title ?? "Gallery #{$m->gallery_id}",
            'Photo Galleries',
            route('content-photo-gallery.edit', $m->gallery_id)
        );
    }
}
```

> **Nota:** Untuk module yang guna `whereHas` (translation table berasingan), arrow function `fn()` kat situ **tak apa** — sebab ia guna dalam query builder, bukan untuk modify `$results`.

### Apa Yang Tak Perlu Diubah
| Komponen | Sebab |
|---|---|
| Frontend Alpine | Auto-group guna field `type` — tak perlu edit |
| Route | Dah ada satu endpoint `/admin/global-search` |
| Permission | Search tak guna permission — accessible to all authenticated admin |

---

## 6. File Index

### New Files
| Path | Description |
|---|---|
| `app/Http/Controllers/Backend/GlobalSearchController.php` | Global search logic — 13 modules |

### Modified Files
| Path | Description |
|---|---|
| `database/seeders/SettingSeeder.php` | Dark mode color defaults (Filament palette) |
| `resources/css/app.css` | Dark mode bottom gap fix; `[x-cloak]` |
| `resources/views/backend/layouts/partials/sidebar.blade.php` | CSS variables for dynamic sidebar colors |
| `resources/views/backend/module/setting/index.blade.php` | Added dark mode color pickers |
| `resources/views/backend/layouts/partials/header.blade.php` | Alpine search component in search bar |
| `resources/views/backend/module/home.blade.php` | Dashboard responsiveness (breakpoint, overflow) |
| `resources/views/backend/module/contentDirectoryDepartment/index.blade.php` | Parent row font-bold |
| `routes/web.php` | Global search API route |
| `public/tailadmin/src/js/index.js` | Removed old search handlers |
| `app/Http/Helpers/SettingHelper.php` | (Unchanged) Existing cache system |
