@rozaqi02/reusable-dashboard 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +662 -123
- package/dist/index.cjs +869 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +284 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +866 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Modul dashboard admin reusable untuk aplikasi UMKM berbasis React + Supabase.
|
|
|
4
4
|
Mendukung berbagai domain bisnis (travel, toko online, UMKM generik) tanpa menulis ulang komponen.
|
|
5
5
|
CSS sudah terbundle — tidak perlu konfigurasi Tailwind.
|
|
6
6
|
|
|
7
|
-
[](./CHANGELOG.md)
|
|
8
8
|
[](./LICENSE)
|
|
9
9
|
[](https://www.npmjs.com/package/@rozaqi02/reusable-dashboard)
|
|
10
10
|
|
|
@@ -16,13 +16,15 @@ CSS sudah terbundle — tidak perlu konfigurasi Tailwind.
|
|
|
16
16
|
2. [Prasyarat Teknis](#2-prasyarat-teknis)
|
|
17
17
|
3. [Preset yang Tersedia](#3-preset-yang-tersedia)
|
|
18
18
|
4. [Quick Start — Toko Sepatu (Contoh Lengkap)](#4-quick-start--toko-sepatu)
|
|
19
|
+
- [4.5 Cara Cepat dengan 1 File Config](#45-cara-cepat-dengan-createdasboardconfig)
|
|
19
20
|
5. [Quick Start — Cidika Travel](#5-quick-start--cidika-travel)
|
|
20
21
|
6. [Setup Database Supabase](#6-setup-database-supabase)
|
|
21
22
|
7. [Cara Membuat Adapter untuk Domain Bisnis Baru](#7-cara-membuat-adapter-untuk-domain-bisnis-baru)
|
|
22
23
|
8. [Konfigurasi Widget](#8-konfigurasi-widget)
|
|
23
24
|
9. [Label & Internasionalisasi](#9-label--internasionalisasi)
|
|
24
25
|
10. [API Reference Lengkap](#10-api-reference-lengkap)
|
|
25
|
-
11. [
|
|
26
|
+
11. [Setup Wizard — Panduan Konfigurasi Interaktif](#11-setup-wizard--panduan-konfigurasi-interaktif)
|
|
27
|
+
12. [Pengembangan & Kontribusi](#12-pengembangan--kontribusi)
|
|
26
28
|
|
|
27
29
|
---
|
|
28
30
|
|
|
@@ -33,8 +35,29 @@ npm install @rozaqi02/reusable-dashboard
|
|
|
33
35
|
npm install recharts @supabase/supabase-js
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
### Import CSS
|
|
39
|
+
|
|
40
|
+
CSS modul harus di-import secara eksplisit di file CSS utama project kamu.
|
|
41
|
+
|
|
42
|
+
**Create React App** — tambahkan di `src/index.css`:
|
|
43
|
+
```css
|
|
44
|
+
@import "../node_modules/@rozaqi02/reusable-dashboard/dist/index.css";
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Vite** — tambahkan di `src/index.css` atau `src/main.jsx`:
|
|
48
|
+
```css
|
|
49
|
+
/* index.css */
|
|
50
|
+
@import "@rozaqi02/reusable-dashboard/dist/index.css";
|
|
51
|
+
```
|
|
52
|
+
```jsx
|
|
53
|
+
// atau di main.jsx
|
|
54
|
+
import "@rozaqi02/reusable-dashboard/dist/index.css";
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Next.js** — tambahkan di `app/globals.css` atau `pages/_app.jsx`:
|
|
58
|
+
```css
|
|
59
|
+
@import "@rozaqi02/reusable-dashboard/dist/index.css";
|
|
60
|
+
```
|
|
38
61
|
|
|
39
62
|
---
|
|
40
63
|
|
|
@@ -42,8 +65,8 @@ npm install recharts @supabase/supabase-js
|
|
|
42
65
|
|
|
43
66
|
| Prasyarat | Versi Minimum | Keterangan |
|
|
44
67
|
|-----------|---------------|------------|
|
|
45
|
-
| Node.js |
|
|
46
|
-
| React | 18.0.0 | UI framework |
|
|
68
|
+
| Node.js | 18.x | Runtime JavaScript (18 atau lebih baru) |
|
|
69
|
+
| React | 18.0.0 | UI framework (mendukung React 19) |
|
|
47
70
|
| recharts | 2.0.0 | Library chart (AreaChart, PieChart, BarChart) |
|
|
48
71
|
| @supabase/supabase-js | 2.0.0 | Client Supabase untuk koneksi database |
|
|
49
72
|
| Akun Supabase | — | Gratis di [supabase.com](https://supabase.com) |
|
|
@@ -57,13 +80,23 @@ Belum mendukung backend lain (Firebase, REST API, GraphQL) tanpa menulis data so
|
|
|
57
80
|
|
|
58
81
|
Modul menyediakan **3 preset siap pakai** untuk domain bisnis yang berbeda:
|
|
59
82
|
|
|
60
|
-
| Preset | Domain | Tabel Supabase yang Dibutuhkan |
|
|
61
|
-
|
|
62
|
-
| **Cidika Travel** | Travel agency / paket wisata | `bookings`, `packages`, `package_locales`, `page_sections` |
|
|
63
|
-
| **Toko Sepatu** | Toko online / e-commerce | `orders`, `order_items`, `products`, `customers` |
|
|
64
|
-
| **Dummy UMKM** | UMKM generik |
|
|
83
|
+
| Preset | Domain | Tabel Supabase yang Dibutuhkan | Siap Pakai? |
|
|
84
|
+
|--------|--------|-------------------------------|-------------|
|
|
85
|
+
| **Cidika Travel** | Travel agency / paket wisata | `bookings`, `packages`, `package_locales`, `page_sections` | Config + Adapter + Source |
|
|
86
|
+
| **Toko Sepatu** | Toko online / e-commerce | `orders`, `order_items`, `products`, `customers` | Config + Adapter + Source |
|
|
87
|
+
| **Dummy UMKM** | UMKM generik (contoh domain baru) | `orders`, `products`, `customers` | Config + Adapter saja* |
|
|
88
|
+
|
|
89
|
+
> *Preset **Dummy UMKM** menyediakan widget config (`dummyUmkmWidgetConfig`) dan
|
|
90
|
+
> adapter (`adaptDummyUmkmData`), tetapi **tidak** menyertakan data source siap pakai.
|
|
91
|
+
> Data source-nya ditulis manual — lihat contoh di
|
|
92
|
+
> `examples/dummy-umkm-example/DummyUmkmDashboard.jsx` (repo GitHub).
|
|
93
|
+
> Ini sengaja dijadikan **template** untuk kamu yang ingin membuat domain baru
|
|
94
|
+
> (lihat [Bab 7](#7-cara-membuat-adapter-untuk-domain-bisnis-baru)).
|
|
65
95
|
|
|
66
|
-
|
|
96
|
+
Hanya **2 data source siap pakai** yang diekspor modul:
|
|
97
|
+
`createCidikaSupabaseSource` dan `createTokoSepatuSupabaseSource`.
|
|
98
|
+
|
|
99
|
+
Setiap preset siap pakai sudah termasuk: konfigurasi widget, data adapter, dan data source Supabase.
|
|
67
100
|
|
|
68
101
|
---
|
|
69
102
|
|
|
@@ -138,8 +171,12 @@ ALTER PUBLICATION supabase_realtime ADD TABLE public.customers;
|
|
|
138
171
|
|
|
139
172
|
**C. Isi data contoh** (opsional, untuk testing):
|
|
140
173
|
|
|
141
|
-
File seed data lengkap tersedia di:
|
|
142
|
-
`examples/toko-sepatu-example/seed-data.sql
|
|
174
|
+
File seed data lengkap tersedia di repo GitHub:
|
|
175
|
+
`examples/toko-sepatu-example/seed-data.sql`.
|
|
176
|
+
|
|
177
|
+
> Catatan: folder `examples/` **tidak ikut** saat `npm install` (paket npm hanya
|
|
178
|
+
> berisi `dist/`). Untuk mengambil file SQL contoh, kloning repo GitHub atau
|
|
179
|
+
> salin manual dari halaman repositori.
|
|
143
180
|
|
|
144
181
|
**D. Aktifkan Realtime** di Supabase Dashboard:
|
|
145
182
|
Database → Replication → centang tabel `orders`, `products`, `customers`.
|
|
@@ -278,6 +315,100 @@ Buka browser → dashboard tampil dengan data dari Supabase.
|
|
|
278
315
|
|
|
279
316
|
---
|
|
280
317
|
|
|
318
|
+
## 4.5 Cara Cepat dengan `createDashboardConfig`
|
|
319
|
+
|
|
320
|
+
Mulai versi **1.1.3**, modul menyediakan factory function `createDashboardConfig` yang
|
|
321
|
+
mengemas semua konfigurasi (widgetConfig + dataSource + adapter + labels) menjadi
|
|
322
|
+
**1 objek tunggal**. Ini cara yang lebih ringkas dan direkomendasikan untuk domain baru.
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
Sebelum (4 parameter terpisah ke hook): Sesudah (1 objek terpadu):
|
|
326
|
+
|
|
327
|
+
useReusableDashboard({ const cfg = createDashboardConfig({
|
|
328
|
+
config: tokoSepatuWidgetConfig, widgetConfig: tokoSepatuWidgetConfig,
|
|
329
|
+
dataSource: source, →→→ dataSource: source,
|
|
330
|
+
adapter: adaptTokoSepatuData, adapter: adaptTokoSepatuData,
|
|
331
|
+
createEmptyState: createEmpty, createEmptyState: createEmpty,
|
|
332
|
+
languageCode: "id", languageCode: "id",
|
|
333
|
+
dateLocale: "id-ID", dateLocale: "id-ID",
|
|
334
|
+
labels, });
|
|
335
|
+
}); useReusableDashboard({ ...cfg, labels });
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Contoh penggunaan lengkap:**
|
|
339
|
+
|
|
340
|
+
```jsx
|
|
341
|
+
import { supabase } from "../lib/supabaseClient";
|
|
342
|
+
import {
|
|
343
|
+
ReusableDashboardView,
|
|
344
|
+
useReusableDashboard,
|
|
345
|
+
createDashboardConfig, // ← factory baru
|
|
346
|
+
tokoSepatuWidgetConfig,
|
|
347
|
+
createTokoSepatuSupabaseSource,
|
|
348
|
+
adaptTokoSepatuData,
|
|
349
|
+
createEmptyTokoSepatuData,
|
|
350
|
+
} from "@rozaqi02/reusable-dashboard";
|
|
351
|
+
|
|
352
|
+
import { myWidgetConfig } from "./myWidgetConfig";
|
|
353
|
+
import { createMySource } from "./myDataSource";
|
|
354
|
+
import { adaptMyData, createEmptyMyData } from "./myAdapter";
|
|
355
|
+
|
|
356
|
+
// Buat di luar komponen — sekali saja
|
|
357
|
+
const dashboardConfig = createDashboardConfig({
|
|
358
|
+
widgetConfig: myWidgetConfig,
|
|
359
|
+
dataSource: createMySource(supabase),
|
|
360
|
+
adapter: adaptMyData,
|
|
361
|
+
createEmptyState: createEmptyMyData,
|
|
362
|
+
languageCode: "id",
|
|
363
|
+
dateLocale: "id-ID",
|
|
364
|
+
// labels bisa disertakan di sini atau di-override saat render
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const labels = { title: "Dashboard Saya", /* ... */ };
|
|
368
|
+
|
|
369
|
+
export default function MyDashboard() {
|
|
370
|
+
const state = useReusableDashboard({ ...dashboardConfig, labels });
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<ReusableDashboardView
|
|
374
|
+
config={dashboardConfig.config}
|
|
375
|
+
labels={labels}
|
|
376
|
+
loading={state.loading}
|
|
377
|
+
error={state.error}
|
|
378
|
+
filters={state.filters}
|
|
379
|
+
onFilterChange={state.updateFilter}
|
|
380
|
+
onResetFilters={state.resetFilters}
|
|
381
|
+
onRefresh={state.refresh}
|
|
382
|
+
data={state.data}
|
|
383
|
+
dateLocale={dashboardConfig.dateLocale}
|
|
384
|
+
liveUpdateEnabled={state.liveUpdateEnabled}
|
|
385
|
+
supabase={supabase} // aktifkan fitur baca tabel di wizard
|
|
386
|
+
dashboardConfig={dashboardConfig} // aktifkan validasi otomatis
|
|
387
|
+
/>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Apa yang terjadi jika config belum lengkap?**
|
|
393
|
+
|
|
394
|
+
`createDashboardConfig` menyimpan info validasi di `dashboardConfig._meta`:
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
dashboardConfig._meta = {
|
|
398
|
+
isValid: true, // false jika ada bagian yang kurang
|
|
399
|
+
missing: [], // ["adapter", "dataSource"] jika belum diisi
|
|
400
|
+
hasDataSource: true,
|
|
401
|
+
hasRealtimeSupport: true, // false jika subscribeLiveUpdate tidak ada
|
|
402
|
+
hasLabels: true,
|
|
403
|
+
widgetCount: { stats: 4, charts: 4, tableColumns: 6 },
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Jika ada bagian yang kurang, `ReusableDashboardView` secara otomatis menampilkan
|
|
408
|
+
**Setup Wizard** (lihat [Bab 11](#11-setup-wizard--panduan-konfigurasi-interaktif)).
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
281
412
|
## 5. Quick Start — Cidika Travel
|
|
282
413
|
|
|
283
414
|
Ganti hanya 4 baris dari contoh Toko Sepatu di atas:
|
|
@@ -378,92 +509,235 @@ CREATE POLICY "Allow read for anon" ON public.orders
|
|
|
378
509
|
|
|
379
510
|
## 7. Cara Membuat Adapter untuk Domain Bisnis Baru
|
|
380
511
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
512
|
+
Bab ini adalah panduan **dari nol** untuk client yang domainnya **bukan** travel
|
|
513
|
+
atau toko sepatu (misal: laundry, klinik, rental mobil, kursus, dll). Kamu hanya
|
|
514
|
+
perlu menulis **3 file kecil**, tanpa pernah menyentuh kode inti modul.
|
|
515
|
+
|
|
516
|
+
### 7.0 Pahami dulu alurnya (WAJIB dibaca)
|
|
517
|
+
|
|
518
|
+
Modul ini memisahkan 3 tanggung jawab. Memahami peran masing-masing membuat
|
|
519
|
+
sisanya gampang:
|
|
520
|
+
|
|
521
|
+
```
|
|
522
|
+
DATABASE Supabase
|
|
523
|
+
│
|
|
524
|
+
▼
|
|
525
|
+
┌───────────────┐ "Ambil data mentah dari Supabase"
|
|
526
|
+
│ 1. DATA SOURCE │ → tahu nama tabel & kolom kamu
|
|
527
|
+
└───────────────┘ → output: objek { bookings, recent, staticCounts }
|
|
528
|
+
│
|
|
529
|
+
▼
|
|
530
|
+
┌───────────────┐ "Ubah data mentah → format standar dashboard"
|
|
531
|
+
│ 2. ADAPTER │ → hitung stats, susun data chart & tabel
|
|
532
|
+
└───────────────┘ → output: objek { stats, charts, table }
|
|
533
|
+
│
|
|
534
|
+
▼
|
|
535
|
+
┌───────────────┐ "Tentukan WIDGET apa yang tampil & labelnya"
|
|
536
|
+
│ 3. WIDGET │ → kartu mana, chart mana, kolom tabel mana
|
|
537
|
+
│ CONFIG │
|
|
538
|
+
└───────────────┘
|
|
539
|
+
│
|
|
540
|
+
▼
|
|
541
|
+
useReusableDashboard(...) → <ReusableDashboardView /> (UI jadi)
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Aturan emas:
|
|
545
|
+
- **Data Source** = satu-satunya bagian yang tahu nama tabel/kolom database kamu.
|
|
546
|
+
- **Adapter** = satu-satunya bagian yang berisi logika hitung (revenue, konversi, dll).
|
|
547
|
+
- **Widget Config** = satu-satunya bagian yang menentukan tampilan (deklaratif, tanpa logika).
|
|
548
|
+
|
|
549
|
+
Urutan mengerjakan: **Data Source → Adapter → Widget Config → rangkai di halaman**.
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
### 7.1 Kontrak antar-lapisan (referensi cepat)
|
|
554
|
+
|
|
555
|
+
**Output Data Source** (yang masuk ke adapter sebagai `raw`):
|
|
556
|
+
|
|
557
|
+
```javascript
|
|
558
|
+
{
|
|
559
|
+
bookings: [...], // WAJIB. Array semua transaksi dalam rentang tanggal.
|
|
560
|
+
recent: [...], // WAJIB. Array 10 transaksi terbaru (untuk tabel).
|
|
561
|
+
packageLocales: [], // Opsional. Untuk nama multi-bahasa. Boleh [].
|
|
562
|
+
staticCounts: { // Opsional. Angka agregat (mis. jumlah produk).
|
|
563
|
+
packages: 0,
|
|
564
|
+
sections: 0,
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
```
|
|
384
568
|
|
|
385
|
-
|
|
569
|
+
> Nama field **harus** `bookings` dan `recent` walau domainmu bukan booking —
|
|
570
|
+
> ini istilah internal modul. Isi datanya bebas (order, transaksi, reservasi, dll).
|
|
571
|
+
|
|
572
|
+
**Output Adapter** (yang dirender oleh komponen):
|
|
386
573
|
|
|
387
574
|
```javascript
|
|
388
|
-
// Format yang harus dikembalikan oleh adapter
|
|
389
575
|
{
|
|
390
576
|
stats: {
|
|
391
|
-
bookingsConfirm: number, //
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
packages: number, // jumlah produk/paket
|
|
395
|
-
sections: number, // jumlah kategori/section (opsional, bisa 0)
|
|
396
|
-
avgRevenue: number, // rata-rata pendapatan per transaksi
|
|
397
|
-
conversionRate: number, // persentase konversi (0–100)
|
|
577
|
+
bookingsConfirm: number, // angka untuk stat card (key bebas, asal cocok config)
|
|
578
|
+
revenueConfirm: number,
|
|
579
|
+
// ...tambahkan key lain sesuai kebutuhan
|
|
398
580
|
},
|
|
399
581
|
charts: {
|
|
400
|
-
dailyTrends: [ //
|
|
401
|
-
{
|
|
402
|
-
dateKey: "2026-05-01", // format YYYY-MM-DD
|
|
403
|
-
label: "01 Mei", // label untuk sumbu X chart
|
|
404
|
-
count: 5, // jumlah transaksi confirmed
|
|
405
|
-
revenue: 2500000, // total revenue
|
|
406
|
-
pendingCount: 2, // jumlah pending (untuk overlay)
|
|
407
|
-
}
|
|
582
|
+
dailyTrends: [ // area chart tren harian
|
|
583
|
+
{ dateKey: "2026-05-01", label: "01 Mei", count: 5, revenue: 2500000, pendingCount: 2 }
|
|
408
584
|
],
|
|
409
|
-
statusDistribution: [ //
|
|
585
|
+
statusDistribution: [ // pie chart distribusi status
|
|
410
586
|
{ status: "confirmed", label: "Confirmed", count: 30 },
|
|
411
|
-
{ status: "pending", label: "Pending", count: 10 },
|
|
412
587
|
],
|
|
413
|
-
audienceDistribution: [], //
|
|
414
|
-
topPackages: [ //
|
|
588
|
+
audienceDistribution: [], // pie chart kedua (boleh kosong [])
|
|
589
|
+
topPackages: [ // bar chart terlaris (boleh kosong [])
|
|
415
590
|
{ packageId: "uuid", name: "Nama Produk", value: 10 },
|
|
416
591
|
],
|
|
417
592
|
},
|
|
418
593
|
table: {
|
|
419
|
-
recentBookings: [ //
|
|
594
|
+
recentBookings: [ // baris tabel transaksi terbaru
|
|
420
595
|
{
|
|
421
596
|
id: "uuid",
|
|
422
597
|
createdAt: "2026-05-01T10:00:00Z", // ISO string
|
|
423
598
|
customerName: "Budi Santoso",
|
|
424
599
|
packageName: "Nama Produk",
|
|
425
|
-
audienceLabel: "Domestic", //
|
|
426
|
-
totalIDR: 500000, //
|
|
600
|
+
audienceLabel: "Domestic", // boleh "-"
|
|
601
|
+
totalIDR: 500000, // integer Rupiah
|
|
427
602
|
status: "confirmed", // lowercase
|
|
428
|
-
statusLabel: "Confirmed",
|
|
603
|
+
statusLabel: "Confirmed",
|
|
429
604
|
}
|
|
430
605
|
],
|
|
431
606
|
},
|
|
432
607
|
}
|
|
433
608
|
```
|
|
434
609
|
|
|
435
|
-
|
|
610
|
+
> Penting: `valueKey` di widget config (Bab 8) harus cocok dengan key di `stats`.
|
|
611
|
+
> Contoh: jika config punya `valueKey: "revenueConfirm"`, adapter wajib
|
|
612
|
+
> mengembalikan `stats.revenueConfirm`.
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### 7.2 STUDI KASUS: Aplikasi Laundry (dari nol)
|
|
617
|
+
|
|
618
|
+
Misalkan client punya 1 tabel Supabase bernama `laundry_orders`:
|
|
619
|
+
|
|
620
|
+
| Kolom | Tipe | Contoh |
|
|
621
|
+
|-------|------|--------|
|
|
622
|
+
| `id` | uuid | `a1b2...` |
|
|
623
|
+
| `created_at` | timestamptz | `2026-05-01T10:00:00Z` |
|
|
624
|
+
| `customer_name` | text | `Budi Santoso` |
|
|
625
|
+
| `total_price` | integer | `50000` |
|
|
626
|
+
| `status` | text | `confirmed` / `pending` |
|
|
627
|
+
| `service_type` | text | `Cuci Kering` |
|
|
628
|
+
|
|
629
|
+
Target dashboard: 2 stat card (total order, total pendapatan), 2 chart
|
|
630
|
+
(tren harian + distribusi status), dan 1 tabel order terbaru.
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
#### Langkah 1 — Tulis DATA SOURCE
|
|
635
|
+
|
|
636
|
+
Buat file `src/datasources/laundryDataSource.js`. Tugasnya: query Supabase,
|
|
637
|
+
kembalikan dalam bentuk `{ bookings, recent, staticCounts }`.
|
|
638
|
+
|
|
639
|
+
```javascript
|
|
640
|
+
// src/datasources/laundryDataSource.js
|
|
641
|
+
export function createLaundrySupabaseSource(supabase) {
|
|
642
|
+
return {
|
|
643
|
+
// Dipanggil otomatis oleh useReusableDashboard setiap filter berubah.
|
|
644
|
+
// Parameter fromISO/toISO/statusScope sudah dihitung oleh modul.
|
|
645
|
+
async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
|
|
646
|
+
// (a) Semua order dalam rentang tanggal → untuk chart & stats
|
|
647
|
+
const { data: orders = [] } = await supabase
|
|
648
|
+
.from("laundry_orders")
|
|
649
|
+
.select("id, created_at, customer_name, total_price, status, service_type")
|
|
650
|
+
.gte("created_at", fromISO)
|
|
651
|
+
.lte("created_at", toISO)
|
|
652
|
+
.order("created_at", { ascending: true });
|
|
653
|
+
|
|
654
|
+
// (b) 10 order terbaru → untuk tabel "Order Terbaru"
|
|
655
|
+
const recentQuery = supabase
|
|
656
|
+
.from("laundry_orders")
|
|
657
|
+
.select("id, created_at, customer_name, total_price, status, service_type")
|
|
658
|
+
.gte("created_at", fromISO)
|
|
659
|
+
.lte("created_at", toISO)
|
|
660
|
+
.order("created_at", { ascending: false })
|
|
661
|
+
.limit(10);
|
|
662
|
+
|
|
663
|
+
// Hormati filter status dari UI (confirmed/pending/all)
|
|
664
|
+
if (statusScope && statusScope !== "all") {
|
|
665
|
+
recentQuery.eq("status", statusScope);
|
|
666
|
+
}
|
|
667
|
+
const { data: recent = [] } = await recentQuery;
|
|
668
|
+
|
|
669
|
+
// (c) Angka agregat opsional, mis. jumlah jenis layanan
|
|
670
|
+
const { count: serviceCount } = await supabase
|
|
671
|
+
.from("services")
|
|
672
|
+
.select("*", { count: "exact", head: true });
|
|
673
|
+
|
|
674
|
+
// Kembalikan SESUAI KONTRAK (nama bookings & recent wajib)
|
|
675
|
+
return {
|
|
676
|
+
bookings: orders,
|
|
677
|
+
recent,
|
|
678
|
+
packageLocales: [],
|
|
679
|
+
staticCounts: { packages: serviceCount || 0, sections: 0 },
|
|
680
|
+
};
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
// OPSIONAL — aktifkan live update realtime. Hapus jika tak perlu.
|
|
684
|
+
subscribeLiveUpdate(onEvent) {
|
|
685
|
+
const channel = supabase
|
|
686
|
+
.channel("laundry-dashboard-live")
|
|
687
|
+
.on("postgres_changes",
|
|
688
|
+
{ event: "*", schema: "public", table: "laundry_orders" }, onEvent)
|
|
689
|
+
.subscribe();
|
|
690
|
+
return () => supabase.removeChannel(channel);
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
> Tips: kalau kamu **tidak** butuh realtime, cukup hapus fungsi
|
|
697
|
+
> `subscribeLiveUpdate`. Badge "Live" otomatis tidak muncul.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
#### Langkah 2 — Tulis ADAPTER
|
|
702
|
+
|
|
703
|
+
Buat file `src/adapters/laundryAdapter.js`. Tugasnya: ubah `raw` dari data source
|
|
704
|
+
menjadi `{ stats, charts, table }`. Pakai helper `toNumber` & `buildDayBuckets`
|
|
705
|
+
dari modul agar tidak menulis ulang logika tanggal.
|
|
436
706
|
|
|
437
707
|
```javascript
|
|
438
708
|
// src/adapters/laundryAdapter.js
|
|
439
709
|
import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
|
|
440
710
|
|
|
711
|
+
// (1) State kosong — dipakai modul sebagai initial state & saat error.
|
|
712
|
+
// Strukturnya HARUS sama dengan output adaptLaundryData di bawah.
|
|
441
713
|
export function createEmptyLaundryData() {
|
|
442
714
|
return {
|
|
443
|
-
stats: { bookingsConfirm: 0,
|
|
444
|
-
packages: 0, sections: 0, avgRevenue: 0, conversionRate: 0 },
|
|
715
|
+
stats: { bookingsConfirm: 0, revenueConfirm: 0 },
|
|
445
716
|
charts: { dailyTrends: [], statusDistribution: [], audienceDistribution: [], topPackages: [] },
|
|
446
717
|
table: { recentBookings: [] },
|
|
447
718
|
};
|
|
448
719
|
}
|
|
449
720
|
|
|
721
|
+
// (2) Fungsi adapter utama.
|
|
450
722
|
export function adaptLaundryData({ raw, filters, range, dateLocale, labels }) {
|
|
451
723
|
if (!raw) return createEmptyLaundryData();
|
|
452
724
|
|
|
453
|
-
const orders = raw.bookings || [];
|
|
725
|
+
const orders = raw.bookings || [];
|
|
454
726
|
const recent = raw.recent || [];
|
|
455
727
|
|
|
456
|
-
//
|
|
728
|
+
// Siapkan bucket harian kosong untuk chart tren (1 bucket = 1 hari)
|
|
457
729
|
const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
|
|
458
|
-
const dayLookup = new Map(dailyBuckets.map(b => [b.dateKey, b]));
|
|
730
|
+
const dayLookup = new Map(dailyBuckets.map((b) => [b.dateKey, b]));
|
|
459
731
|
|
|
460
|
-
let confirmed = 0, pending = 0, revenue = 0;
|
|
461
732
|
const statusMap = new Map();
|
|
733
|
+
let confirmed = 0;
|
|
734
|
+
let revenue = 0;
|
|
462
735
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
const
|
|
736
|
+
// Loop semua order: hitung total & isi bucket harian
|
|
737
|
+
orders.forEach((row) => {
|
|
738
|
+
const status = String(row.status || "pending").toLowerCase();
|
|
739
|
+
const amount = toNumber(row.total_price); // ← kolom DB kamu
|
|
740
|
+
const dayKey = String(row.created_at || "").slice(0, 10);
|
|
467
741
|
|
|
468
742
|
statusMap.set(status, (statusMap.get(status) || 0) + 1);
|
|
469
743
|
|
|
@@ -473,98 +747,211 @@ export function adaptLaundryData({ raw, filters, range, dateLocale, labels }) {
|
|
|
473
747
|
bucket.revenue += amount;
|
|
474
748
|
}
|
|
475
749
|
|
|
476
|
-
if (status === "confirmed") { confirmed
|
|
477
|
-
else if (status === "pending") pending++;
|
|
750
|
+
if (status === "confirmed") { confirmed += 1; revenue += amount; }
|
|
478
751
|
});
|
|
479
752
|
|
|
480
753
|
return {
|
|
754
|
+
// stats: key di sini HARUS cocok dengan valueKey di widget config
|
|
481
755
|
stats: {
|
|
482
756
|
bookingsConfirm: confirmed,
|
|
483
|
-
bookingsPending: pending,
|
|
484
757
|
revenueConfirm: revenue,
|
|
485
|
-
packages: toNumber(raw.staticCounts?.packages),
|
|
486
|
-
sections: 0,
|
|
487
|
-
avgRevenue: confirmed > 0 ? Math.round(revenue / confirmed) : 0,
|
|
488
|
-
conversionRate: confirmed + pending > 0 ? Math.round(confirmed / (confirmed + pending) * 100) : 0,
|
|
489
758
|
},
|
|
490
759
|
charts: {
|
|
491
760
|
dailyTrends: dailyBuckets,
|
|
492
761
|
statusDistribution: Array.from(statusMap.entries()).map(([status, count]) => ({
|
|
493
|
-
status,
|
|
762
|
+
status,
|
|
763
|
+
label: labels?.formatStatusLabel?.(status) || status,
|
|
764
|
+
count,
|
|
494
765
|
})),
|
|
495
|
-
audienceDistribution: [],
|
|
496
|
-
topPackages: [],
|
|
766
|
+
audienceDistribution: [], // tidak dipakai laundry → kosong
|
|
767
|
+
topPackages: [], // tidak dipakai laundry → kosong
|
|
497
768
|
},
|
|
498
769
|
table: {
|
|
499
|
-
recentBookings: recent.map(row =>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
770
|
+
recentBookings: recent.map((row) => {
|
|
771
|
+
const status = String(row.status || "pending").toLowerCase();
|
|
772
|
+
return {
|
|
773
|
+
id: row.id,
|
|
774
|
+
createdAt: row.created_at,
|
|
775
|
+
customerName: row.customer_name || "-",
|
|
776
|
+
packageName: row.service_type || "-", // ← kolom DB kamu
|
|
777
|
+
audienceLabel: "-",
|
|
778
|
+
totalIDR: toNumber(row.total_price),
|
|
779
|
+
status,
|
|
780
|
+
statusLabel: labels?.formatStatusLabel?.(status) || status,
|
|
781
|
+
};
|
|
782
|
+
}),
|
|
509
783
|
},
|
|
510
784
|
};
|
|
511
785
|
}
|
|
512
786
|
```
|
|
513
787
|
|
|
514
|
-
|
|
788
|
+
> Catatan: `audienceDistribution` dan `topPackages` boleh `[]` jika domainmu
|
|
789
|
+
> tidak butuh. Lebih baik lagi: **jangan daftarkan** chart itu di widget config
|
|
790
|
+
> (Langkah 3), supaya tidak tampil sama sekali.
|
|
515
791
|
|
|
516
|
-
|
|
517
|
-
// src/datasources/laundryDataSource.js
|
|
518
|
-
export function createLaundrySupabaseSource(supabase) {
|
|
519
|
-
return {
|
|
520
|
-
async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
|
|
521
|
-
// Query utama
|
|
522
|
-
const { data: orders = [] } = await supabase
|
|
523
|
-
.from("laundry_orders") // sesuaikan nama tabel
|
|
524
|
-
.select("id, created_at, customer_name, total_price, status, service_type")
|
|
525
|
-
.gte("created_at", fromISO)
|
|
526
|
-
.lte("created_at", toISO)
|
|
527
|
-
.order("created_at", { ascending: true });
|
|
792
|
+
---
|
|
528
793
|
|
|
529
|
-
|
|
530
|
-
const recentQuery = supabase
|
|
531
|
-
.from("laundry_orders")
|
|
532
|
-
.select("id, created_at, customer_name, total_price, status, service_type")
|
|
533
|
-
.gte("created_at", fromISO)
|
|
534
|
-
.lte("created_at", toISO)
|
|
535
|
-
.order("created_at", { ascending: false })
|
|
536
|
-
.limit(10);
|
|
537
|
-
if (statusScope && statusScope !== "all") recentQuery.eq("status", statusScope);
|
|
538
|
-
const { data: recent = [] } = await recentQuery;
|
|
794
|
+
#### Langkah 3 — Tulis WIDGET CONFIG
|
|
539
795
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
.select("*", { count: "exact", head: true });
|
|
796
|
+
Buat file `src/config/laundryConfig.js`. Ini murni deklaratif — menentukan
|
|
797
|
+
widget apa yang muncul. `valueKey` harus cocok dengan key `stats` dari adapter,
|
|
798
|
+
dan `label` adalah key dari objek labels (Langkah 4).
|
|
544
799
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
800
|
+
```javascript
|
|
801
|
+
// src/config/laundryConfig.js
|
|
802
|
+
export const laundryConfig = {
|
|
803
|
+
id: "laundry.dashboard",
|
|
804
|
+
defaultFilters: {
|
|
805
|
+
statusScope: "confirmed", // filter awal saat dibuka
|
|
806
|
+
daysPreset: 30, // tampilkan 30 hari terakhir
|
|
807
|
+
sortPkgBy: "bookings",
|
|
808
|
+
sortPkgDir: "desc",
|
|
809
|
+
},
|
|
810
|
+
widgets: {
|
|
811
|
+
// 2 stat card → valueKey cocok dgn stats di adapter
|
|
812
|
+
stats: [
|
|
813
|
+
{ id: "orders", label: "confirmedBookings", icon: "TrendingUp",
|
|
814
|
+
valueKey: "bookingsConfirm", format: "number", accentColor: "blue" },
|
|
815
|
+
{ id: "revenue", label: "confirmedRevenue", icon: "DollarSign",
|
|
816
|
+
valueKey: "revenueConfirm", format: "currency", accentColor: "green" },
|
|
817
|
+
],
|
|
818
|
+
// 2 chart → hanya yang ada datanya (tren & status)
|
|
819
|
+
charts: [
|
|
820
|
+
{ id: "trend", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
|
|
821
|
+
{ id: "status", type: "statusPie", label: "statusDistribution", icon: "PieChart" },
|
|
822
|
+
],
|
|
823
|
+
// 1 tabel → kolom mengacu ke field recentBookings dari adapter
|
|
824
|
+
table: {
|
|
825
|
+
id: "recentOrders",
|
|
826
|
+
label: "recentBookings",
|
|
827
|
+
icon: "Calendar",
|
|
828
|
+
emptyLabel: "noRecentBookings",
|
|
829
|
+
columns: [
|
|
830
|
+
{ id: "date", label: "date", accessor: "createdAt", type: "date" },
|
|
831
|
+
{ id: "customer", label: "customer", accessor: "customerName" },
|
|
832
|
+
{ id: "service", label: "package", accessor: "packageName" },
|
|
833
|
+
{ id: "total", label: "total", accessor: "totalIDR", type: "currency" },
|
|
834
|
+
{ id: "status", label: "status", accessor: "statusLabel",
|
|
835
|
+
type: "statusBadge", statusAccessor: "status" },
|
|
836
|
+
],
|
|
554
837
|
},
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
```
|
|
555
841
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
842
|
+
> Detail tiap field config (tipe kolom, nama ikon, format) ada di
|
|
843
|
+
> [Bab 8 — Konfigurasi Widget](#8-konfigurasi-widget).
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
#### Langkah 4 — Rangkai semuanya di halaman Dashboard
|
|
848
|
+
|
|
849
|
+
Sekarang gabungkan ke-3 file di atas + objek labels, lalu serahkan ke
|
|
850
|
+
`useReusableDashboard` dan render `ReusableDashboardView`.
|
|
851
|
+
|
|
852
|
+
```jsx
|
|
853
|
+
// src/pages/LaundryDashboard.jsx
|
|
854
|
+
import React, { useMemo } from "react";
|
|
855
|
+
import { supabase } from "../lib/supabaseClient";
|
|
856
|
+
import {
|
|
857
|
+
ReusableDashboardView,
|
|
858
|
+
useReusableDashboard,
|
|
859
|
+
} from "@rozaqi02/reusable-dashboard";
|
|
860
|
+
|
|
861
|
+
// 3 file yang baru kamu buat:
|
|
862
|
+
import { laundryConfig } from "../config/laundryConfig";
|
|
863
|
+
import { createLaundrySupabaseSource } from "../datasources/laundryDataSource";
|
|
864
|
+
import { adaptLaundryData, createEmptyLaundryData } from "../adapters/laundryAdapter";
|
|
865
|
+
|
|
866
|
+
// Data source dibuat sekali di luar komponen (anti re-render)
|
|
867
|
+
const source = createLaundrySupabaseSource(supabase);
|
|
868
|
+
|
|
869
|
+
// Label UI — semua teks yang tampil. Sesuaikan bahasa/konteks bisnismu.
|
|
870
|
+
const labels = {
|
|
871
|
+
title: "Dashboard Laundry",
|
|
872
|
+
refresh: "Refresh",
|
|
873
|
+
liveUpdate: "Live",
|
|
874
|
+
loadFailed: "Gagal memuat data.",
|
|
875
|
+
retry: "Coba Lagi",
|
|
876
|
+
allStatus: "Semua Status",
|
|
877
|
+
confirmedOnly: "Selesai",
|
|
878
|
+
pendingOnly: "Proses",
|
|
879
|
+
reset: "Reset",
|
|
880
|
+
confirmedBookings: "Total Order",
|
|
881
|
+
confirmedRevenue: "Total Pendapatan",
|
|
882
|
+
dailyTrends: "Tren Harian",
|
|
883
|
+
statusDistribution: "Distribusi Status",
|
|
884
|
+
recentBookings: "Order Terbaru",
|
|
885
|
+
noRecentBookings: "Belum ada order",
|
|
886
|
+
date: "Tanggal",
|
|
887
|
+
customer: "Pelanggan",
|
|
888
|
+
package: "Layanan",
|
|
889
|
+
total: "Total",
|
|
890
|
+
status: "Status",
|
|
891
|
+
bookingsMetric: "Order",
|
|
892
|
+
revenueMetric: "Pendapatan",
|
|
893
|
+
confirmedBookingMetric: "Order (Selesai)",
|
|
894
|
+
confirmedRevenueMetric: "Pendapatan (Selesai)",
|
|
895
|
+
dayLabel: (n) => `${n} hari`,
|
|
896
|
+
formatStatusLabel: (s) =>
|
|
897
|
+
({ confirmed: "Selesai", pending: "Proses", cancelled: "Batal" })[s] || s,
|
|
898
|
+
formatAudienceLabel: (v) => v || "-",
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
export default function LaundryDashboard() {
|
|
902
|
+
const state = useReusableDashboard({
|
|
903
|
+
config: laundryConfig, // ← Langkah 3
|
|
904
|
+
dataSource: source, // ← Langkah 1
|
|
905
|
+
adapter: adaptLaundryData, // ← Langkah 2
|
|
906
|
+
createEmptyState: createEmptyLaundryData, // ← Langkah 2
|
|
907
|
+
languageCode: "id",
|
|
908
|
+
dateLocale: "id-ID",
|
|
909
|
+
labels,
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
return (
|
|
913
|
+
<ReusableDashboardView
|
|
914
|
+
config={laundryConfig}
|
|
915
|
+
labels={labels}
|
|
916
|
+
loading={state.loading}
|
|
917
|
+
error={state.error}
|
|
918
|
+
filters={state.filters}
|
|
919
|
+
onFilterChange={state.updateFilter}
|
|
920
|
+
onResetFilters={state.resetFilters}
|
|
921
|
+
onRefresh={state.refresh}
|
|
922
|
+
data={state.data}
|
|
923
|
+
dateLocale="id-ID"
|
|
924
|
+
liveUpdateEnabled={state.liveUpdateEnabled}
|
|
925
|
+
/>
|
|
926
|
+
);
|
|
565
927
|
}
|
|
566
928
|
```
|
|
567
929
|
|
|
930
|
+
Selesai. Buka halaman tersebut → dashboard laundry tampil lengkap dengan data
|
|
931
|
+
dari Supabase, filter tanggal, search, sort, dan pagination — semua sudah
|
|
932
|
+
ditangani modul.
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
### 7.3 Checklist & troubleshooting domain baru
|
|
937
|
+
|
|
938
|
+
| Gejala | Penyebab umum | Solusi |
|
|
939
|
+
|--------|---------------|--------|
|
|
940
|
+
| Stat card menampilkan `0` terus | `valueKey` di config tidak cocok dengan key `stats` adapter | Samakan nama, mis. `valueKey: "revenueConfirm"` ↔ `stats.revenueConfirm` |
|
|
941
|
+
| Chart kosong | Data source tidak mengembalikan `bookings`, atau status bukan `"confirmed"` | Pastikan field bernama `bookings` & status lowercase |
|
|
942
|
+
| Tabel kosong padahal ada data | Data source tidak mengembalikan `recent` | Tambahkan query `recent` (10 terbaru) |
|
|
943
|
+
| Error "is not a function" | Lupa pasang `createEmptyState` | Wajib kirim `createEmptyXxxData` ke hook |
|
|
944
|
+
| Label tampil sebagai key mentah | Key di config (`label`) tidak ada di objek `labels` | Tambahkan key tersebut di objek labels |
|
|
945
|
+
| Badge "Live" tidak muncul | `subscribeLiveUpdate` tidak ada / Realtime belum aktif | Tambahkan fungsi + aktifkan Realtime di Supabase |
|
|
946
|
+
|
|
947
|
+
Checklist final sebelum rilis ke client:
|
|
948
|
+
- [ ] Data source mengembalikan `{ bookings, recent }` (nama persis).
|
|
949
|
+
- [ ] Adapter mengembalikan `{ stats, charts, table }` lengkap.
|
|
950
|
+
- [ ] `createEmptyXxxData` punya struktur sama dengan output adapter.
|
|
951
|
+
- [ ] Semua `valueKey` di config ada di `stats`.
|
|
952
|
+
- [ ] Semua `label`/`emptyLabel` di config ada di objek `labels`.
|
|
953
|
+
- [ ] RLS Supabase mengizinkan `SELECT` untuk role `anon` (lihat [Bab 6](#6-setup-database-supabase)).
|
|
954
|
+
|
|
568
955
|
---
|
|
569
956
|
|
|
570
957
|
## 8. Konfigurasi Widget
|
|
@@ -762,6 +1149,20 @@ function Dashboard() {
|
|
|
762
1149
|
| `cidikaWidgetConfig` | Object | Config dashboard Cidika Travel |
|
|
763
1150
|
| `tokoSepatuWidgetConfig` | Object | Config dashboard Toko Sepatu |
|
|
764
1151
|
| `dummyUmkmWidgetConfig` | Object | Config dashboard UMKM generik |
|
|
1152
|
+
| `createDashboardConfig(options)` | Function | Factory: kemas semua config jadi 1 objek (v1.1.3+) |
|
|
1153
|
+
| `validateDashboardConfig(cfg)` | Function | Validasi config, kembalikan `{ valid, issues }` (v1.1.3+) |
|
|
1154
|
+
|
|
1155
|
+
**`createDashboardConfig(options)` — parameter:**
|
|
1156
|
+
|
|
1157
|
+
| Parameter | Tipe | Wajib | Keterangan |
|
|
1158
|
+
|-----------|------|-------|------------|
|
|
1159
|
+
| `widgetConfig` | Object | ✅ | Konfigurasi widget deklaratif |
|
|
1160
|
+
| `dataSource` | Object | ✅ | Hasil `createXxxSupabaseSource(supabase)` |
|
|
1161
|
+
| `adapter` | Function | ✅ | Fungsi `adaptXxxData` |
|
|
1162
|
+
| `createEmptyState` | Function | ✅ | Fungsi `createEmptyXxxData` |
|
|
1163
|
+
| `labels` | Object | — | Objek label UI (bisa di-override saat render) |
|
|
1164
|
+
| `languageCode` | string | — | Default `"id"` |
|
|
1165
|
+
| `dateLocale` | string | — | Default `"id-ID"` |
|
|
765
1166
|
|
|
766
1167
|
### Data Source
|
|
767
1168
|
|
|
@@ -812,6 +1213,7 @@ state.range // { fromISO, toISO, daysWindow }
|
|
|
812
1213
|
| Komponen | Deskripsi |
|
|
813
1214
|
|----------|-----------|
|
|
814
1215
|
| `ReusableDashboardView` | Halaman dashboard lengkap — gunakan ini untuk integrasi cepat |
|
|
1216
|
+
| `SetupWizard` | Pop-up wizard panduan konfigurasi interaktif (v1.1.3+) |
|
|
815
1217
|
| `StatCard` | Kartu metrik dengan warna aksen |
|
|
816
1218
|
| `ChartCard` | Kartu grafik (area/pie/bar) |
|
|
817
1219
|
| `DataTable` | Tabel dengan search, sort, pagination |
|
|
@@ -828,6 +1230,24 @@ state.range // { fromISO, toISO, daysWindow }
|
|
|
828
1230
|
| `SidebarNavigation` | Navigasi sidebar |
|
|
829
1231
|
| `TopbarHeader` | Header atas |
|
|
830
1232
|
|
|
1233
|
+
**Props `ReusableDashboardView` (lengkap):**
|
|
1234
|
+
|
|
1235
|
+
| Prop | Tipe | Wajib | Keterangan |
|
|
1236
|
+
|------|------|-------|------------|
|
|
1237
|
+
| `config` | Object | ✅ | Widget configuration |
|
|
1238
|
+
| `labels` | Object | ✅ | Objek label UI |
|
|
1239
|
+
| `loading` | boolean | — | Status loading data |
|
|
1240
|
+
| `error` | string | — | Pesan error |
|
|
1241
|
+
| `filters` | Object | — | State filter aktif |
|
|
1242
|
+
| `onFilterChange` | Function | ✅ | `(field, value) => void` |
|
|
1243
|
+
| `onResetFilters` | Function | ✅ | `() => void` |
|
|
1244
|
+
| `onRefresh` | Function | ✅ | `() => void` |
|
|
1245
|
+
| `data` | Object | — | Data `{ stats, charts, table }` |
|
|
1246
|
+
| `dateLocale` | string | — | Default `"id-ID"` |
|
|
1247
|
+
| `liveUpdateEnabled` | boolean | — | Badge "Live" aktif |
|
|
1248
|
+
| `supabase` | Object | — | Supabase client — aktifkan fitur baca tabel di wizard (v1.1.3+) |
|
|
1249
|
+
| `dashboardConfig` | Object | — | Hasil `createDashboardConfig()` — aktifkan validasi otomatis (v1.1.3+) |
|
|
1250
|
+
|
|
831
1251
|
### Utility
|
|
832
1252
|
|
|
833
1253
|
| Function | Deskripsi |
|
|
@@ -845,7 +1265,91 @@ state.range // { fromISO, toISO, daysWindow }
|
|
|
845
1265
|
|
|
846
1266
|
---
|
|
847
1267
|
|
|
848
|
-
## 11.
|
|
1268
|
+
## 11. Setup Wizard — Panduan Konfigurasi Interaktif
|
|
1269
|
+
|
|
1270
|
+
Mulai versi **1.1.3**, modul menyertakan **Setup Wizard**: pop-up panduan interaktif yang
|
|
1271
|
+
muncul **otomatis** saat `ReusableDashboardView` mendeteksi konfigurasi belum lengkap.
|
|
1272
|
+
|
|
1273
|
+
Ini menjawab kebutuhan: *"developer baru yang belum pernah pakai modul ini tahu harus
|
|
1274
|
+
mulai dari mana tanpa harus baca README dari awal."*
|
|
1275
|
+
|
|
1276
|
+
### Cara kerja
|
|
1277
|
+
|
|
1278
|
+
Wizard aktif ketika salah satu kondisi terpenuhi:
|
|
1279
|
+
- Prop `dashboardConfig` (dari `createDashboardConfig`) punya `_meta.isValid === false`
|
|
1280
|
+
- Prop `config`, `labels`, atau `labels.formatStatusLabel` tidak ditemukan
|
|
1281
|
+
|
|
1282
|
+
Wizard **tidak** muncul jika semua konfigurasi sudah valid.
|
|
1283
|
+
|
|
1284
|
+
### Fitur wizard
|
|
1285
|
+
|
|
1286
|
+
**Step 0 — Overview:**
|
|
1287
|
+
- Checklist masalah yang ditemukan (mis. `"adapter belum diisi"`)
|
|
1288
|
+
- Diagram alur konfigurasi: Data Source → Adapter → Widget Config
|
|
1289
|
+
|
|
1290
|
+
**Step 1 — Data Source:**
|
|
1291
|
+
- Contoh kode `fetchDashboardSnapshot` yang siap di-copy
|
|
1292
|
+
- Tombol **"Baca tabel Supabase langsung"** — membaca daftar tabel dari project
|
|
1293
|
+
Supabase kamu secara realtime via `information_schema.tables`
|
|
1294
|
+
|
|
1295
|
+
**Step 2 — Adapter:**
|
|
1296
|
+
- Contoh kode `adaptMyData` dengan `buildDayBuckets` + `toNumber`
|
|
1297
|
+
- Penjelasan bahwa key `stats` harus cocok dengan `valueKey` di widget config
|
|
1298
|
+
|
|
1299
|
+
**Step 3 — Widget Config & Rangkaian Akhir:**
|
|
1300
|
+
- Contoh kode lengkap dengan `createDashboardConfig` dari langkah 1–3
|
|
1301
|
+
|
|
1302
|
+
### Cara mengaktifkan fitur baca tabel Supabase
|
|
1303
|
+
|
|
1304
|
+
Tambahkan prop `supabase` ke `ReusableDashboardView`:
|
|
1305
|
+
|
|
1306
|
+
```jsx
|
|
1307
|
+
<ReusableDashboardView
|
|
1308
|
+
config={dashboardConfig.config}
|
|
1309
|
+
labels={labels}
|
|
1310
|
+
// ... props lain
|
|
1311
|
+
supabase={supabase} // ← aktifkan fitur baca tabel
|
|
1312
|
+
dashboardConfig={dashboardConfig} // ← aktifkan validasi otomatis
|
|
1313
|
+
/>
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
Saat wizard terbuka di Step 1, pengguna bisa klik tombol untuk melihat
|
|
1317
|
+
daftar tabel yang tersedia di project Supabase mereka:
|
|
1318
|
+
|
|
1319
|
+
```
|
|
1320
|
+
Tabel ditemukan (4):
|
|
1321
|
+
bookings packages package_locales page_sections
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
> Fitur ini membutuhkan RLS Supabase mengizinkan `SELECT` pada
|
|
1325
|
+
> `information_schema.tables` untuk role `anon`, atau fallback ke RPC `get_tables`.
|
|
1326
|
+
> Jika tidak bisa diakses, wizard tetap berfungsi — hanya tombol baca tabel yang
|
|
1327
|
+
> menampilkan pesan error.
|
|
1328
|
+
|
|
1329
|
+
### Menggunakan wizard tanpa `createDashboardConfig`
|
|
1330
|
+
|
|
1331
|
+
Wizard juga bisa dipakai standalone (misalnya saat testing atau demo):
|
|
1332
|
+
|
|
1333
|
+
```jsx
|
|
1334
|
+
import { SetupWizard } from "@rozaqi02/reusable-dashboard";
|
|
1335
|
+
|
|
1336
|
+
// Tampilkan wizard dengan isu kustom
|
|
1337
|
+
<SetupWizard
|
|
1338
|
+
issues={["Widget config belum diisi", "Data source tidak ditemukan"]}
|
|
1339
|
+
supabase={supabase}
|
|
1340
|
+
onDismiss={() => console.log("wizard ditutup")}
|
|
1341
|
+
/>
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
### Menutup wizard
|
|
1345
|
+
|
|
1346
|
+
Klik tombol **"Lanjutkan tanpa wizard"** atau **"Tutup & lanjutkan ✓"** di Step 3.
|
|
1347
|
+
Wizard tidak muncul lagi selama sesi berlangsung (state `wizardDismissed` disimpan
|
|
1348
|
+
di level komponen).
|
|
1349
|
+
|
|
1350
|
+
---
|
|
1351
|
+
|
|
1352
|
+
## 12. Pengembangan & Kontribusi
|
|
849
1353
|
|
|
850
1354
|
```bash
|
|
851
1355
|
# Clone repository
|
|
@@ -861,13 +1365,48 @@ npm run build # Output ke dist/
|
|
|
861
1365
|
# Test
|
|
862
1366
|
npm test # Jest + React Testing Library
|
|
863
1367
|
npm test -- --coverage # Lihat code coverage
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
### Cara Publish Versi Baru ke npm
|
|
1371
|
+
|
|
1372
|
+
Modul ini publik di npm: `@rozaqi02/reusable-dashboard`. Setiap kali kode modul
|
|
1373
|
+
berubah, kamu wajib publish versi baru agar perubahan sampai ke client.
|
|
1374
|
+
|
|
1375
|
+
```bash
|
|
1376
|
+
# 1. Pastikan sudah login (sekali saja per device)
|
|
1377
|
+
npm login # buka browser untuk autentikasi
|
|
1378
|
+
npm whoami # verifikasi → harus muncul "rozaqi02"
|
|
864
1379
|
|
|
865
|
-
#
|
|
866
|
-
npm version patch # 1.
|
|
867
|
-
npm version minor # 1.
|
|
868
|
-
npm
|
|
1380
|
+
# 2. Naikkan versi (pilih salah satu sesuai jenis perubahan)
|
|
1381
|
+
npm version patch # 1.1.2 → 1.1.3 (bug fix / perbaikan kecil)
|
|
1382
|
+
npm version minor # 1.1.2 → 1.2.0 (fitur baru, tanpa breaking change)
|
|
1383
|
+
npm version major # 1.1.2 → 2.0.0 (breaking change)
|
|
1384
|
+
|
|
1385
|
+
# 3. Publish (build otomatis jalan dulu lewat prepublishOnly)
|
|
1386
|
+
npm publish # akan meminta OTP (One-Time Password)
|
|
869
1387
|
```
|
|
870
1388
|
|
|
1389
|
+
> **Catatan OTP:** karena akun npm kamu mengaktifkan 2FA, `npm publish` akan
|
|
1390
|
+
> meminta one-time password. Masukkan kode dari aplikasi authenticator kamu
|
|
1391
|
+
> (atau gunakan `npm publish --otp=123456`). Langkah ini **harus dijalankan
|
|
1392
|
+
> manual di terminal kamu** — tidak bisa diotomasi.
|
|
1393
|
+
|
|
1394
|
+
> **Publish ≠ git push.** `npm publish` hanya mengunggah isi folder `dist/`
|
|
1395
|
+
> (lihat field `files` di `package.json`) ke registry npm. Tidak bergantung pada
|
|
1396
|
+
> commit/push git terakhir. Jadi kamu tidak perlu push ke GitHub dulu agar
|
|
1397
|
+
> bisa publish — keduanya independen. (Tetap disarankan commit & push agar
|
|
1398
|
+
> source code di GitHub sinkron dengan versi yang dirilis.)
|
|
1399
|
+
|
|
1400
|
+
### Update Modul di Project Client
|
|
1401
|
+
|
|
1402
|
+
Setelah versi baru terbit, di setiap project yang memakai modul:
|
|
1403
|
+
|
|
1404
|
+
```bash
|
|
1405
|
+
npm install @rozaqi02/reusable-dashboard@latest
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
Lalu restart dev server (`npm start` / `npm run dev`) agar perubahan termuat.
|
|
1409
|
+
|
|
871
1410
|
---
|
|
872
1411
|
|
|
873
1412
|
## License
|