| Current Path : /var/www/html/reddsis/docs/module/ |
| Current File : /var/www/html/reddsis/docs/module/data-dashboard-system.md |
# 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.