# Data Dashboard System — Dokumentasi Penuh

## 1. Struktur Database

**Table:** `data_dashboard`

| Column            | Type          | Default | Description                              |
|-------------------|---------------|---------|------------------------------------------|
| `id`              | bigint (PK)   | auto    | Primary key                              |
| `title`           | string(255)   | null    | Nama widget — **UNIQUE INDEX**           |
| `content`         | longText      | null    | PHP/HTML/JS code widget                  |
| `sorting`         | integer       | null    | Urutan display (ascending)               |
| `activation_status` | integer     | 1       | 1 = aktif, 0 = hidden                    |
| `col_span`        | tinyInteger   | 1       | Lebar grid dashboard (1-4)               |
| `created_by`      | integer       | null    | ID user yang create                      |
| `created_at`      | datetime      | null    | Timestamp create                         |
| `updated_by`      | integer       | null    | ID user yang update                      |
| `updated_at`      | datetime      | null    | Timestamp update                         |

**Index:**
- `PRIMARY KEY (id)`
- `UNIQUE KEY data_dashboard_title_unique (title)` — unique constraint peringkat DB

---

## 2. Aliran Data (Data Flow)

```
┌────────────────────────────────────────────────────────────────────┐
│                    ADMIN CREATE/EDIT WIDGET                        │
│                                                                    │
│  [Create View] ──form──> [DataDashboardController@store] ──save──>│
│  [Edit View]   ──form──> [DataDashboardController@update]         │
└────────────────────────────────┬───────────────────────────────────┘
                                 │ DB: data_dashboard
                                 ▼
┌────────────────────────────────────────────────────────────────────┐
│                    DASHBOARD DISPLAY                                │
│                                                                    │
│  BackendHomeController@index ──query active widgets──> home.blade  │
│                                                                    │
│  home.blade.php:                                                   │
│    ┌─ load ApexCharts (vendor/apexcharts/)                         │
│    └─ foreach($dashboardItems as $item)                            │
│         └─ DashboardHelper::render($item->content)                  │
│              └─ Blade::render($content)                             │
│                   └─ Output: HTML + JS dengan chart data            │
└────────────────────────────────────────────────────────────────────┘
```

---

## 3. Penjelasan Setiap File

### 3.1 Model: `app/Models/Backend/DataDashboard.php`

```php
#[Table('data_dashboard')]
#[Fillable(['title', 'content', 'sorting', 'activation_status', 'col_span', 'created_by', 'updated_by'])]
class DataDashboard extends Model
```

- Guna PHP 8 **Attribute** (`#[Table]`, `#[Fillable]`) — setara dgn `protected $table` dan `protected $fillable`.
- `casts()` pastikan `sorting`, `activation_status`, `col_span` sentiasa integer.
- Tiada relationship — widget standalone, tak terkait table lain.

### 3.2 Helper: `app/Helpers/DashboardHelper.php`

```php
public static function render($content): string
{
    if (empty($content)) return '';
    try {
        return Blade::render($content);
    } catch (\Throwable $e) {
        return '<div class="text-red-500">Error: ' . e($e->getMessage()) . '</div>';
    }
}
```

**Cara kerja `Blade::render()`:**

1. Content diproses oleh **Blade compiler** Laravel (sama macam view biasa)
2. `<?php ... ?>` blocks — dilaksanakan seperti biasa
3. `<?= json_encode(...) ?>` — diproses sebagai PHP
4. `{{ ... }}` — escaped output
5. `{!! ... !!}` — unescaped output
6. `@if`, `@foreach`, dll — Blade directives

**Kelebihan berbanding `eval()`:**
- Compiled + cached — lebih laju
- Blade syntax support (`@if`, `@foreach`, `{{ }}`)
- Zero `eval()` — lebih selamat
- Guna Laravel native compiler

**Error handling:** Tangkap `\Throwable`, return `<div class="text-red-500">Error: ...</div>`.

### 3.3 Controller CRUD: `app/Http/Controllers/Backend/DataDashboardController.php`

**Routes** (dalam `routes/web.php`):

| Method | URI | Action | Permission |
|--------|-----|--------|------------|
| GET | `/data-dashboard` | `index` | `data-dashboard.view` |
| GET | `/data-dashboard/create` | `create` | `data-dashboard.create` |
| POST | `/data-dashboard` | `store` | `data-dashboard.create` |
| GET | `/data-dashboard/{id}/edit` | `edit` | `data-dashboard.update` |
| PUT | `/data-dashboard/{id}` | `update` | `data-dashboard.update` |
| DELETE | `/data-dashboard/{id}` | `destroy` | `data-dashboard.delete` |
| POST | `/data-dashboard/format` | `format` | `data-dashboard.create` |
| POST | `/data-dashboard/lint` | `lint` | `data-dashboard.create` |
| POST | `/data-dashboard/reorder` | `reorder` | `data-dashboard.update` |

**Functions:**

- **`index()`** — Paginated list widget dengan search (by title). Guna `orderBy('sorting')`.
- **`create()`** — Return create view.
- **`store()`** — Validate fields (title: `unique:data_dashboard,title`), set `created_by`/`updated_by` dgn `auth()->id()`, save.
- **`edit($id)`** — Find by ID, return edit view dengan data.
- **`update()`** — Validate fields (title: `unique:data_dashboard,title,{id}` — ignore current record), set `updated_by`, update record.
- **`destroy($id)`** — Delete record.
- **`reorder()`** — Susun semula urutan widget (AJAX POST).
- **`format()`** — Format code menggunakan `formatMixedCode()` (indentation).
- **`lint()`** — Lint PHP code guna `php -l` command line.
- **`formatMixedCode()`** — Private method untuk auto-indent PHP code.

### 3.4 Controller Dashboard: `app/Http/Controllers/Backend/BackendHomeController.php`

```php
public function index()
{
    $dashboardItems = DataDashboard::where('activation_status', 1)
        ->orderBy('sorting')
        ->get();

    return view('backend.module.home', [
        'dashboardItems' => $dashboardItems,
    ]);
}
```

**Route:** `GET /home` → `admin.home` (with permission middleware)

Query hanya widget dengan `activation_status = 1`, diorder ikut `sorting`.

### 3.5 Layout Dashboard: `resources/views/backend/module/home.blade.php`

```
extends → backend.layouts.app
         └── @yield('content')
               └── home.blade.php (section content)
                     ├── <script src="vendor/apexcharts/apexcharts.min.js">
                     ├── @foreach($dashboardItems as $item)
                     │    └── <div class="widget-card col-span-{col_span}">
                     │         └── DashboardHelper::render($item->content)
                     │              └── Blade::render() → HTML + JS output
                     └── @endforeach
```

**CSS Grid:** Guna `display: grid; grid-template-columns: repeat(4, 1fr)`. Setiap widget guna `col-span-N` berdasarkan nilai `col_span` dari DB. Responsive turun ke 2 kolum (tablet) dan 1 kolum (phone).

### 3.6 CRUD Views: `resources/views/backend/module/dataDashboard/`

#### `index.blade.php`
- List widget dalam table: No, Title, Sorting, Col Span, Status, Actions
- Search with debounce (1500ms delay)
- Pagination guna `vendor.pagination.custom`
- Delete confirmation guna SweetAlert2 (`Swal.fire`)
- Permission gates (`@can`)

#### `create.blade.php`
- Form: Title, Column Span, Sorting, Status, Content (code editor)
- Code editor guna **Monaco Editor** (VS Code's editor) — loaded dari `vendor/monaco-editor/`
- PHP Lint auto detect — hantar content ke `/data-dashboard/lint` endpoint untuk validate PHP syntax
- Format Code — hantar ke `/data-dashboard/format` endpoint (Shift+Alt+F)
- Content disimpan ke hidden `<textarea>` on form submit

#### `edit.blade.php`
- Sama dengan create tapi pre-filled dengan data sedia ada
- Monaco Editor loaded dari CDN (`cdn.jsdelivr.net`)
- Format Code guna `editor.action.formatDocument` (built-in Monaco formatter)

### 3.7 Seeder: `database/seeders/DataDashboardSeeder.php`

Guna `updateOrCreate` ikut title:

```php
DataDashboard::updateOrCreate(
    ['title' => 'System Clock'],
    ['content' => <<<'PHP' ... PHP, 'sorting' => 1, ...]
);

DataDashboard::updateOrCreate(
    ['title' => 'Visitor Statistics'],
    ['content' => <<<'PHP' ... PHP, 'sorting' => 2, ...]
);
```

Sebab guna `<<<'PHP'` (nowdoc), content disimpan literally — `<?php`, `<?=` etc. diproses masa render.

---

## 4. Cara Widget Berfungsi

### 4.1 Proses Render Widget

1. Content widget dalam DB (contoh):
```php
<?php
use App\Models\Backend\Visit;
use Carbon\Carbon;
// ... query data ...
?>
<div id="chartBar"></div>
<script>
var chart = new ApexCharts(...).render();
</script>
```

2. `DashboardHelper::render()` panggil `Blade::render()` yang:
   - Execute PHP block → set `$labels7`, `$data7` dll
   - Output HTML (`<div id="chartBar"></div>`)
   - Process `<?= json_encode($data7) ?>` → output JSON array
   - Output `<script>` dengan data sebenar

3. Browser terima output:
```html
<div id="chartBar"></div>
<script>
var chart = new ApexCharts(...).render();
</script>
```

4. ApexCharts (loaded via `<script>` tag sebelum widget loop) render chart.

### 4.2 Widget Content Format

```
<?php
// 1. PHP Block — query data
use App\Models\Backend\Visit;
$data = Visit::whereDate('created_at', $date)->count();
?>
// 2. HTML Container — guna <div>, bukan <canvas>
<div id="unique-chart-id"></div>
// 3. JavaScript — initialize ApexCharts
<script>
new ApexCharts(document.getElementById('unique-chart-id'), options).render();
</script>
```

**PENTING (ApexCharts):**
- Container guna `<div>` bukan `<canvas>` (ApexCharts render SVG)
- Chart ID mesti UNIQUE (jangan ulang dalam widget lain)
- Script mesti wrap dalam **`DOMContentLoaded`** sebab ApexCharts loading secara deferred dari tailadmin bundle

**PENTING (Chart.js):**
- Container guna `<canvas>`, bukan `<div>`
- Kalau guna CDN terus dalam widget, script synchronous, **tak perlu `DOMContentLoaded`**

---

### Perbandingan Cara Loading Chart

| Cara | Library | Container | Script Loading | DOMContentLoaded? |
|------|---------|-----------|----------------|-------------------|
| **Tailadmin Bundle** | ApexCharts | `<div>` | Deferred (bundle.js) | **Ya**, wajib wrap |
| **CDN dalam Widget** | ApexCharts / Chart.js | `<div>` / `<canvas>` | Synchronous | **Tak perlu** |
| **CDN di Layout** | ApexCharts / Chart.js | `<div>` / `<canvas>` | Synchronous sebelum widget loop | **Tak perlu** |

> **Nota:** Contoh-contoh di bawah menggunakan ApexCharts via tailadmin bundle, jadi semuanya guna `DOMContentLoaded`. Kalau tukar ke CDN, buang je `DOMContentLoaded` dan terus guna `new ApexCharts(...).render()`.

### 4.3 Contoh Chart Types Lain

#### Pie Chart
```php
<?php
use App\Models\Backend\Visit;
$devices = Visit::select('device', \Illuminate\Support\Facades\DB::raw('count(*) as count'))
    ->groupBy('device')->pluck('count', 'device');
$labels = $devices->keys()->toArray();
$data = $devices->values()->toArray();
?>
<div id="chartPie"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
    new ApexCharts(document.getElementById('chartPie'), {
        series: <?= json_encode($data) ?>,
        chart: { type: 'pie', height: 300 },
        labels: <?= json_encode($labels) ?>,
        colors: ['#3b82f6', '#ef4444', '#22c55e', '#f59e0b'],
        legend: { position: 'bottom' }
    }).render();
});
</script>
```

#### Donut Chart
```php
<?php
use App\Models\Backend\Visit;
$browsers = Visit::select('browser', \Illuminate\Support\Facades\DB::raw('count(*) as count'))
    ->groupBy('browser')->pluck('count', 'browser');
$labels = $browsers->keys()->toArray();
$data = $browsers->values()->toArray();
?>
<div id="chartDonut"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
    new ApexCharts(document.getElementById('chartDonut'), {
        series: <?= json_encode($data) ?>,
        chart: { type: 'donut', height: 300 },
        labels: <?= json_encode($labels) ?>,
        colors: ['#3b82f6', '#ef4444', '#22c55e', '#f59e0b', '#8b5cf6'],
        legend: { position: 'bottom' },
        plotOptions: { pie: { donut: { size: '65%' } } }
    }).render();
});
</script>
```

#### Area Chart
```php
<?php
use App\Models\Backend\Visit;
use Carbon\Carbon;
$labels = []; $data = [];
for ($i = 11; $i >= 0; $i--) {
    $date = Carbon::now()->subMonths($i);
    $labels[] = $date->format('M Y');
    $data[] = Visit::whereYear('created_at', $date->year)
        ->whereMonth('created_at', $date->month)->count();
}
?>
<div id="chartArea"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
    new ApexCharts(document.getElementById('chartArea'), {
        series: [{ name: 'Visits', data: <?= json_encode($data) ?> }],
        chart: { type: 'area', height: 300, toolbar: { show: false } },
        colors: ['#8b5cf6'],
        stroke: { curve: 'smooth', width: 2 },
        fill: { type: 'gradient', gradient: { opacityFrom: 0.5, opacityTo: 0 } },
        dataLabels: { enabled: false },
        xaxis: { categories: <?= json_encode($labels) ?> },
        yaxis: { min: 0 },
        legend: { show: false }
    }).render();
});
</script>
```

#### Radial Bar (Single)
```php
<?php
use App\Models\Backend\Visit;
$total = Visit::count();
$today = Visit::whereDate('created_at', today())->count();
$percentage = $total > 0 ? round(($today / $total) * 100) : 0;
?>
<div id="chartRadial"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
    new ApexCharts(document.getElementById('chartRadial'), {
        series: [<?= $percentage ?>],
        chart: { type: 'radialBar', height: 300 },
        colors: ['#3b82f6'],
        plotOptions: {
            radialBar: {
                hollow: { size: '70%' },
                dataLabels: {
                    value: { fontSize: '28px', fontWeight: '600' },
                    name: { show: true, fontSize: '14px' }
                }
            }
        },
        labels: ['Today vs Total']
    }).render();
});
</script>
```

#### Mixed Chart (Bar + Line)
```php
<?php
use App\Models\Backend\Visit;
use Carbon\Carbon;
$labels = []; $visits = []; $unique = [];
for ($i = 6; $i >= 0; $i--) {
    $date = Carbon::now()->subDays($i);
    $labels[] = $date->format('D');
    $visits[] = Visit::whereDate('created_at', $date)->count();
    $unique[] = Visit::whereDate('created_at', $date)->distinct('ip_address')->count('ip_address');
}
?>
<div id="chartMixed"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
    new ApexCharts(document.getElementById('chartMixed'), {
        series: [
            { name: 'Visits', type: 'bar', data: <?= json_encode($visits) ?> },
            { name: 'Unique IP', type: 'line', data: <?= json_encode($unique) ?> }
        ],
        chart: { height: 300, toolbar: { show: false } },
        colors: ['#3b82f6', '#22c55e'],
        stroke: { width: [0, 2], curve: 'smooth' },
        dataLabels: { enabled: false },
        xaxis: { categories: <?= json_encode($labels) ?> },
        yaxis: { min: 0 },
        legend: { position: 'top' }
    }).render();
});
</script>
```

### 4.4 Cara Loading Chart

Ada **dua pendekatan** untuk loading chart library di dashboard ni.

#### Pendekatan A: Tailadmin Bundle (ApexCharts) — Sedang Digunakan

ApexCharts loading sebagai sebahagian daripada `tailadmin/build/bundle.js` dan di-expose sebagai global.

**Setup:**

1. Dalam `public/tailadmin/src/js/index.js`:
```js
import ApexCharts from "apexcharts";
window.ApexCharts = ApexCharts;
```

2. Rebuild webpack: `npm run build` (dalam folder `public/tailadmin/`)

3. Layout `app.blade.php` loading bundle.js secara **deferred**:
```blade
<script defer src="{{ asset('tailadmin/build/bundle.js') }}"></script>
```

4. Disebabkan `defer`, ApexCharts hanya tersedia **selepas** HTML parsing selesai.

5. Inline script widget mesti wrap `DOMContentLoaded`:
```js
document.addEventListener('DOMContentLoaded', function() {
    new ApexCharts(element, options).render();
});
```

**Kelebihan:** Satu copy ApexCharts, loading sekali, offline, konsisten dengan tailadmin.
**Kekurangan:** Kena wrap DOMContentLoaded setiap kali guna chart dalam widget.

---

#### Pendekatan B: CDN Script — Alternatif

Kalau tak suka wrap `DOMContentLoaded`, boleh letak CDN script **di `home.blade.php`** sebelum widget loop:

```blade
{{-- home.blade.php --}}
@section('content')
    <div class="col-span-12">

        {{-- CDN ApexCharts --}}
        <script src="https://cdn.jsdelivr.net/npm/apexcharts@3.54.1/dist/apexcharts.min.js"></script>

        {{-- ATAU CDN Chart.js --}}
        <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>

        @if ($dashboardItems->isNotEmpty())
            ...
        @endif
    </div>
@endsection
```

Script synchronous, jadi widget inline script boleh terus guna tanpa `DOMContentLoaded`:
```php
<div id="chartBar"></div>
<script>
new ApexCharts(document.getElementById('chartBar'), options).render();
</script>
```

Atau untuk Chart.js:
```php
<canvas id="chartBar"></canvas>
<script>
new Chart(document.getElementById('chartBar'), {...});
</script>
```

**Note:** Kalau guna CDN, pastikan buang `window.ApexCharts = ApexCharts` dari tailadmin index.js (tidak perlu) dan jangan load bundle.js yang mengandungi ApexCharts bersama CDN — nanti conflict. Boleh biarkan bundle.js asal tanpa import ApexCharts.

---

## 5. Hubungan Antara Komponen

```
┌──────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   Routes     │────>│   Controllers    │────>│     Views       │
│  web.php     │     │                  │     │                 │
│              │     │ BackendHomeCtrl  │     │ home.blade.php  │
│ /home        │────>│   → index()      │────>│   ↑             │
│              │     │   → query widget │     │   │ render()    │
│ /data-dash   │     │   → return view  │     │   │             │
│   -board/*   │────>│ DataDashboardCtrl│     │ dataDashboard/  │
│              │     │   → CRUD methods │────>│   index, create,│
│              │     │   → format/lint  │     │   edit          │
└──────────────┘     └────────┬─────────┘     └────────┬────────┘
                              │                        │
                              ▼                        ▼
                     ┌────────────────┐      ┌─────────────────┐
                     │    Model       │      │  Helper          │
                     │ DataDashboard  │      │ DashboardHelper  │
                     │   → table      │      │   → render()     │
                     │   → casts()    │      │   → eval()       │
                     └───────┬────────┘      └────────┬────────┘
                             │                        │
                             ▼                        ▼
                     ┌──────────────────────────────────────┐
                     │            Database                   │
                     │      data_dashboard table             │
                     │  ├─ title (unique identifier)         │
                     │  ├─ content (PHP/HTML/JS)             │
                     │  ├─ sorting, col_span, status         │
                     │  └─ timestamps                        │
                     └──────────────────────────────────────┘
```

### Ringkasan Aliran:

1. **Seeder** → insert widget records ke DB
2. **Routes** → map URLs ke controllers
3. **BackendHomeController** → query widget aktif → pass ke view
4. **home.blade.php** → loop widget, panggil helper untuk render
 5. **DashboardHelper** → Blade::render(content) → execute PHP → return HTML/JS
 6. **ApexCharts** (dari tailadmin bundle.js, deferred) → `window.ApexCharts` → chart render dalam `DOMContentLoaded`

---

## 6. Migration Notes

### Chart.js → ApexCharts (via Tailadmin Bundle)

| Langkah | Detail |
|---------|--------|
| 1 | Install apexcharts: `npm install apexcharts` |
| 2 | `public/tailadmin/src/js/index.js` — tambah `import ApexCharts from "apexcharts"` + `window.ApexCharts = ApexCharts` |
| 3 | Rebuild tailadmin: `npm run build` (dalam `public/tailadmin/`) |
| 4 | `home.blade.php` — buang `<script src="vendor/apexcharts/...">` (guna bundle je) |
| 5 | `DataDashboardSeeder` — convert `new Chart(...)` → `new ApexCharts(...).render()` + wrap `DOMContentLoaded` |
| 6 | `<canvas>` → `<div>` |
| 7 | Delete widget lama dalam DB yang masih guna Chart.js code |
| 8 | `package.json` — buang `"chart.js"` dependency jika tak guna lagi |

### Chart.js → ApexCharts (via CDN)

Alternatif lebih ringkas tanpa kena rebuild tailadmin:

| Langkah | Detail |
|---------|--------|
| 1 | Install apexcharts: `npm install apexcharts` |
| 2 | `home.blade.php` — ganti `<script src="vendor/chart.js/...">` → `https://cdn.jsdelivr.net/npm/apexcharts@3.54.1/dist/apexcharts.min.js` |
| 3 | `DataDashboardSeeder` — convert `new Chart(...)` → `new ApexCharts(...).render()` |
| 4 | `<canvas>` → `<div>` |
| 5 | Delete widget lama dalam DB yang masih guna Chart.js code |

### Tukar Balik ke Chart.js

Kalau nak guna Chart.js semula, cukup letak CDN dalam `home.blade.php`:
```blade
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
```

Widget content guna `<canvas>` dan `new Chart(...)`. Tak perlu `DOMContentLoaded`.

### Perbezaan Chart.js vs ApexCharts

| Chart.js | ApexCharts |
|----------|------------|
| `<canvas>` container | `<div>` container |
| `new Chart(...)` (auto-render) | `new ApexCharts(...).render()` (explicit render) |
| `responsive: true` | `chart.height` mesti ditetapkan |
| Bundled via Vite (`import`) | Exposed dari tailadmin bundle (webpack) |
| Plugin options dalam `plugins` | Options terus di root config |

---

## 7. Tips & Troubleshooting

### Chart tak muncul
- Pastikan `<div>` container bukan `<canvas>`
- Check console F12 — ade error JavaScript?
- Pastikan widget inline script wrap dalam `DOMContentLoaded`
- Check widget `activation_status = 1`
- Check `sorting` order — mungkin widget tersembunyi
- Hard refresh browser (Ctrl+Shift+R)

### PHP Error dalam widget
`DashboardHelper::render()` return `<div class="text-red-500">Error: ...</div>` jika `Blade::render()` gagal. Semak error message yang dipaparkan.

### Widget content tak update
Seeder guna `updateOrCreate` ikut `title`. Pastikan title sama. Jika tak, record baru akan dicipta.

### ApexCharts Conflict (Duplicate Load)
**Masalah:** ApexCharts loading **dua kali** — sekali dalam `tailadmin/build/bundle.js` (webpack) dan sekali dari `vendor/apexcharts/apexcharts.min.js`. Dua versi bertempur → error:
```
Uncaught TypeError: t.put is not a function
    at create.addTo (apexcharts.common.js)
```

**Penyelesaian 1 — Guna Bundle Sahaja (lagi kemas):**
```js
// public/tailadmin/src/js/index.js
import ApexCharts from "apexcharts";
window.ApexCharts = ApexCharts;
```
Rebuild: `npm run build` dalam `public/tailadmin/`.
Buang vendor script dari `home.blade.php`.
Widget kena wrap `DOMContentLoaded`.

**Penyelesaian 2 — Guna CDN Sahaja (lagi ringkas):**
Buang import ApexCharts dari tailadmin index.js, rebuild.
Letak CDN dalam `home.blade.php` sebagai regular `<script>` sebelum widget loop.
Widget tak perlu `DOMContentLoaded`.

> **Penting:** Jangan guna dua-dua sekali — pilih SATU cara.

---

## 8. Unique Title Validation

### Controller Level
- **`store()`** — `unique:data_dashboard,title` memastikan title tak duplicate masa create.
- **`update()`** — `unique:data_dashboard,title,{id}` — ignore current record sendiri, jadi user boleh update widget tanpa kena error title sendiri.

### Database Level
Migration `2026_05_21_045007_create_data_dashboard_table.php` ada `$table->unique('title')` — unique index pada `title` column.

**Double protection:** Validation kat controller block sebelum sampai DB, unique index kat DB block kalau ada direct insert/update (phpMyAdmin, tinker, etc.).

### Seeder & Unique
Seeder guna `updateOrCreate(['title' => ...])` — ia bergantung kepada unique identifier `title`. Kalau ada duplicate title, `updateOrCreate` mungkin jadi keliru. Unique index/pun kat controller memastikan tiada duplicate. 

