@rozaqi02/reusable-dashboard 1.1.2 → 1.1.3
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 +447 -121
- package/dist/index.cjs +437 -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 +434 -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
|
|
|
@@ -33,8 +33,29 @@ npm install @rozaqi02/reusable-dashboard
|
|
|
33
33
|
npm install recharts @supabase/supabase-js
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
### Import CSS
|
|
37
|
+
|
|
38
|
+
CSS modul harus di-import secara eksplisit di file CSS utama project kamu.
|
|
39
|
+
|
|
40
|
+
**Create React App** — tambahkan di `src/index.css`:
|
|
41
|
+
```css
|
|
42
|
+
@import "../node_modules/@rozaqi02/reusable-dashboard/dist/index.css";
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Vite** — tambahkan di `src/index.css` atau `src/main.jsx`:
|
|
46
|
+
```css
|
|
47
|
+
/* index.css */
|
|
48
|
+
@import "@rozaqi02/reusable-dashboard/dist/index.css";
|
|
49
|
+
```
|
|
50
|
+
```jsx
|
|
51
|
+
// atau di main.jsx
|
|
52
|
+
import "@rozaqi02/reusable-dashboard/dist/index.css";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Next.js** — tambahkan di `app/globals.css` atau `pages/_app.jsx`:
|
|
56
|
+
```css
|
|
57
|
+
@import "@rozaqi02/reusable-dashboard/dist/index.css";
|
|
58
|
+
```
|
|
38
59
|
|
|
39
60
|
---
|
|
40
61
|
|
|
@@ -42,8 +63,8 @@ npm install recharts @supabase/supabase-js
|
|
|
42
63
|
|
|
43
64
|
| Prasyarat | Versi Minimum | Keterangan |
|
|
44
65
|
|-----------|---------------|------------|
|
|
45
|
-
| Node.js |
|
|
46
|
-
| React | 18.0.0 | UI framework |
|
|
66
|
+
| Node.js | 18.x | Runtime JavaScript (18 atau lebih baru) |
|
|
67
|
+
| React | 18.0.0 | UI framework (mendukung React 19) |
|
|
47
68
|
| recharts | 2.0.0 | Library chart (AreaChart, PieChart, BarChart) |
|
|
48
69
|
| @supabase/supabase-js | 2.0.0 | Client Supabase untuk koneksi database |
|
|
49
70
|
| Akun Supabase | — | Gratis di [supabase.com](https://supabase.com) |
|
|
@@ -57,13 +78,23 @@ Belum mendukung backend lain (Firebase, REST API, GraphQL) tanpa menulis data so
|
|
|
57
78
|
|
|
58
79
|
Modul menyediakan **3 preset siap pakai** untuk domain bisnis yang berbeda:
|
|
59
80
|
|
|
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 |
|
|
81
|
+
| Preset | Domain | Tabel Supabase yang Dibutuhkan | Siap Pakai? |
|
|
82
|
+
|--------|--------|-------------------------------|-------------|
|
|
83
|
+
| **Cidika Travel** | Travel agency / paket wisata | `bookings`, `packages`, `package_locales`, `page_sections` | Config + Adapter + Source |
|
|
84
|
+
| **Toko Sepatu** | Toko online / e-commerce | `orders`, `order_items`, `products`, `customers` | Config + Adapter + Source |
|
|
85
|
+
| **Dummy UMKM** | UMKM generik (contoh domain baru) | `orders`, `products`, `customers` | Config + Adapter saja* |
|
|
86
|
+
|
|
87
|
+
> *Preset **Dummy UMKM** menyediakan widget config (`dummyUmkmWidgetConfig`) dan
|
|
88
|
+
> adapter (`adaptDummyUmkmData`), tetapi **tidak** menyertakan data source siap pakai.
|
|
89
|
+
> Data source-nya ditulis manual — lihat contoh di
|
|
90
|
+
> `examples/dummy-umkm-example/DummyUmkmDashboard.jsx` (repo GitHub).
|
|
91
|
+
> Ini sengaja dijadikan **template** untuk kamu yang ingin membuat domain baru
|
|
92
|
+
> (lihat [Bab 7](#7-cara-membuat-adapter-untuk-domain-bisnis-baru)).
|
|
93
|
+
|
|
94
|
+
Hanya **2 data source siap pakai** yang diekspor modul:
|
|
95
|
+
`createCidikaSupabaseSource` dan `createTokoSepatuSupabaseSource`.
|
|
65
96
|
|
|
66
|
-
Setiap preset sudah termasuk: konfigurasi widget, data adapter, dan data source Supabase.
|
|
97
|
+
Setiap preset siap pakai sudah termasuk: konfigurasi widget, data adapter, dan data source Supabase.
|
|
67
98
|
|
|
68
99
|
---
|
|
69
100
|
|
|
@@ -138,8 +169,12 @@ ALTER PUBLICATION supabase_realtime ADD TABLE public.customers;
|
|
|
138
169
|
|
|
139
170
|
**C. Isi data contoh** (opsional, untuk testing):
|
|
140
171
|
|
|
141
|
-
File seed data lengkap tersedia di:
|
|
142
|
-
`examples/toko-sepatu-example/seed-data.sql
|
|
172
|
+
File seed data lengkap tersedia di repo GitHub:
|
|
173
|
+
`examples/toko-sepatu-example/seed-data.sql`.
|
|
174
|
+
|
|
175
|
+
> Catatan: folder `examples/` **tidak ikut** saat `npm install` (paket npm hanya
|
|
176
|
+
> berisi `dist/`). Untuk mengambil file SQL contoh, kloning repo GitHub atau
|
|
177
|
+
> salin manual dari halaman repositori.
|
|
143
178
|
|
|
144
179
|
**D. Aktifkan Realtime** di Supabase Dashboard:
|
|
145
180
|
Database → Replication → centang tabel `orders`, `products`, `customers`.
|
|
@@ -378,92 +413,235 @@ CREATE POLICY "Allow read for anon" ON public.orders
|
|
|
378
413
|
|
|
379
414
|
## 7. Cara Membuat Adapter untuk Domain Bisnis Baru
|
|
380
415
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
416
|
+
Bab ini adalah panduan **dari nol** untuk client yang domainnya **bukan** travel
|
|
417
|
+
atau toko sepatu (misal: laundry, klinik, rental mobil, kursus, dll). Kamu hanya
|
|
418
|
+
perlu menulis **3 file kecil**, tanpa pernah menyentuh kode inti modul.
|
|
419
|
+
|
|
420
|
+
### 7.0 Pahami dulu alurnya (WAJIB dibaca)
|
|
384
421
|
|
|
385
|
-
|
|
422
|
+
Modul ini memisahkan 3 tanggung jawab. Memahami peran masing-masing membuat
|
|
423
|
+
sisanya gampang:
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
DATABASE Supabase
|
|
427
|
+
│
|
|
428
|
+
▼
|
|
429
|
+
┌───────────────┐ "Ambil data mentah dari Supabase"
|
|
430
|
+
│ 1. DATA SOURCE │ → tahu nama tabel & kolom kamu
|
|
431
|
+
└───────────────┘ → output: objek { bookings, recent, staticCounts }
|
|
432
|
+
│
|
|
433
|
+
▼
|
|
434
|
+
┌───────────────┐ "Ubah data mentah → format standar dashboard"
|
|
435
|
+
│ 2. ADAPTER │ → hitung stats, susun data chart & tabel
|
|
436
|
+
└───────────────┘ → output: objek { stats, charts, table }
|
|
437
|
+
│
|
|
438
|
+
▼
|
|
439
|
+
┌───────────────┐ "Tentukan WIDGET apa yang tampil & labelnya"
|
|
440
|
+
│ 3. WIDGET │ → kartu mana, chart mana, kolom tabel mana
|
|
441
|
+
│ CONFIG │
|
|
442
|
+
└───────────────┘
|
|
443
|
+
│
|
|
444
|
+
▼
|
|
445
|
+
useReusableDashboard(...) → <ReusableDashboardView /> (UI jadi)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Aturan emas:
|
|
449
|
+
- **Data Source** = satu-satunya bagian yang tahu nama tabel/kolom database kamu.
|
|
450
|
+
- **Adapter** = satu-satunya bagian yang berisi logika hitung (revenue, konversi, dll).
|
|
451
|
+
- **Widget Config** = satu-satunya bagian yang menentukan tampilan (deklaratif, tanpa logika).
|
|
452
|
+
|
|
453
|
+
Urutan mengerjakan: **Data Source → Adapter → Widget Config → rangkai di halaman**.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### 7.1 Kontrak antar-lapisan (referensi cepat)
|
|
458
|
+
|
|
459
|
+
**Output Data Source** (yang masuk ke adapter sebagai `raw`):
|
|
460
|
+
|
|
461
|
+
```javascript
|
|
462
|
+
{
|
|
463
|
+
bookings: [...], // WAJIB. Array semua transaksi dalam rentang tanggal.
|
|
464
|
+
recent: [...], // WAJIB. Array 10 transaksi terbaru (untuk tabel).
|
|
465
|
+
packageLocales: [], // Opsional. Untuk nama multi-bahasa. Boleh [].
|
|
466
|
+
staticCounts: { // Opsional. Angka agregat (mis. jumlah produk).
|
|
467
|
+
packages: 0,
|
|
468
|
+
sections: 0,
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
> Nama field **harus** `bookings` dan `recent` walau domainmu bukan booking —
|
|
474
|
+
> ini istilah internal modul. Isi datanya bebas (order, transaksi, reservasi, dll).
|
|
475
|
+
|
|
476
|
+
**Output Adapter** (yang dirender oleh komponen):
|
|
386
477
|
|
|
387
478
|
```javascript
|
|
388
|
-
// Format yang harus dikembalikan oleh adapter
|
|
389
479
|
{
|
|
390
480
|
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)
|
|
481
|
+
bookingsConfirm: number, // angka untuk stat card (key bebas, asal cocok config)
|
|
482
|
+
revenueConfirm: number,
|
|
483
|
+
// ...tambahkan key lain sesuai kebutuhan
|
|
398
484
|
},
|
|
399
485
|
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
|
-
}
|
|
486
|
+
dailyTrends: [ // area chart tren harian
|
|
487
|
+
{ dateKey: "2026-05-01", label: "01 Mei", count: 5, revenue: 2500000, pendingCount: 2 }
|
|
408
488
|
],
|
|
409
|
-
statusDistribution: [ //
|
|
489
|
+
statusDistribution: [ // pie chart distribusi status
|
|
410
490
|
{ status: "confirmed", label: "Confirmed", count: 30 },
|
|
411
|
-
{ status: "pending", label: "Pending", count: 10 },
|
|
412
491
|
],
|
|
413
|
-
audienceDistribution: [], //
|
|
414
|
-
topPackages: [ //
|
|
492
|
+
audienceDistribution: [], // pie chart kedua (boleh kosong [])
|
|
493
|
+
topPackages: [ // bar chart terlaris (boleh kosong [])
|
|
415
494
|
{ packageId: "uuid", name: "Nama Produk", value: 10 },
|
|
416
495
|
],
|
|
417
496
|
},
|
|
418
497
|
table: {
|
|
419
|
-
recentBookings: [ //
|
|
498
|
+
recentBookings: [ // baris tabel transaksi terbaru
|
|
420
499
|
{
|
|
421
500
|
id: "uuid",
|
|
422
501
|
createdAt: "2026-05-01T10:00:00Z", // ISO string
|
|
423
502
|
customerName: "Budi Santoso",
|
|
424
503
|
packageName: "Nama Produk",
|
|
425
|
-
audienceLabel: "Domestic", //
|
|
426
|
-
totalIDR: 500000, //
|
|
504
|
+
audienceLabel: "Domestic", // boleh "-"
|
|
505
|
+
totalIDR: 500000, // integer Rupiah
|
|
427
506
|
status: "confirmed", // lowercase
|
|
428
|
-
statusLabel: "Confirmed",
|
|
507
|
+
statusLabel: "Confirmed",
|
|
429
508
|
}
|
|
430
509
|
],
|
|
431
510
|
},
|
|
432
511
|
}
|
|
433
512
|
```
|
|
434
513
|
|
|
435
|
-
|
|
514
|
+
> Penting: `valueKey` di widget config (Bab 8) harus cocok dengan key di `stats`.
|
|
515
|
+
> Contoh: jika config punya `valueKey: "revenueConfirm"`, adapter wajib
|
|
516
|
+
> mengembalikan `stats.revenueConfirm`.
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### 7.2 STUDI KASUS: Aplikasi Laundry (dari nol)
|
|
521
|
+
|
|
522
|
+
Misalkan client punya 1 tabel Supabase bernama `laundry_orders`:
|
|
523
|
+
|
|
524
|
+
| Kolom | Tipe | Contoh |
|
|
525
|
+
|-------|------|--------|
|
|
526
|
+
| `id` | uuid | `a1b2...` |
|
|
527
|
+
| `created_at` | timestamptz | `2026-05-01T10:00:00Z` |
|
|
528
|
+
| `customer_name` | text | `Budi Santoso` |
|
|
529
|
+
| `total_price` | integer | `50000` |
|
|
530
|
+
| `status` | text | `confirmed` / `pending` |
|
|
531
|
+
| `service_type` | text | `Cuci Kering` |
|
|
532
|
+
|
|
533
|
+
Target dashboard: 2 stat card (total order, total pendapatan), 2 chart
|
|
534
|
+
(tren harian + distribusi status), dan 1 tabel order terbaru.
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
#### Langkah 1 — Tulis DATA SOURCE
|
|
539
|
+
|
|
540
|
+
Buat file `src/datasources/laundryDataSource.js`. Tugasnya: query Supabase,
|
|
541
|
+
kembalikan dalam bentuk `{ bookings, recent, staticCounts }`.
|
|
542
|
+
|
|
543
|
+
```javascript
|
|
544
|
+
// src/datasources/laundryDataSource.js
|
|
545
|
+
export function createLaundrySupabaseSource(supabase) {
|
|
546
|
+
return {
|
|
547
|
+
// Dipanggil otomatis oleh useReusableDashboard setiap filter berubah.
|
|
548
|
+
// Parameter fromISO/toISO/statusScope sudah dihitung oleh modul.
|
|
549
|
+
async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
|
|
550
|
+
// (a) Semua order dalam rentang tanggal → untuk chart & stats
|
|
551
|
+
const { data: orders = [] } = await supabase
|
|
552
|
+
.from("laundry_orders")
|
|
553
|
+
.select("id, created_at, customer_name, total_price, status, service_type")
|
|
554
|
+
.gte("created_at", fromISO)
|
|
555
|
+
.lte("created_at", toISO)
|
|
556
|
+
.order("created_at", { ascending: true });
|
|
557
|
+
|
|
558
|
+
// (b) 10 order terbaru → untuk tabel "Order Terbaru"
|
|
559
|
+
const recentQuery = supabase
|
|
560
|
+
.from("laundry_orders")
|
|
561
|
+
.select("id, created_at, customer_name, total_price, status, service_type")
|
|
562
|
+
.gte("created_at", fromISO)
|
|
563
|
+
.lte("created_at", toISO)
|
|
564
|
+
.order("created_at", { ascending: false })
|
|
565
|
+
.limit(10);
|
|
566
|
+
|
|
567
|
+
// Hormati filter status dari UI (confirmed/pending/all)
|
|
568
|
+
if (statusScope && statusScope !== "all") {
|
|
569
|
+
recentQuery.eq("status", statusScope);
|
|
570
|
+
}
|
|
571
|
+
const { data: recent = [] } = await recentQuery;
|
|
572
|
+
|
|
573
|
+
// (c) Angka agregat opsional, mis. jumlah jenis layanan
|
|
574
|
+
const { count: serviceCount } = await supabase
|
|
575
|
+
.from("services")
|
|
576
|
+
.select("*", { count: "exact", head: true });
|
|
577
|
+
|
|
578
|
+
// Kembalikan SESUAI KONTRAK (nama bookings & recent wajib)
|
|
579
|
+
return {
|
|
580
|
+
bookings: orders,
|
|
581
|
+
recent,
|
|
582
|
+
packageLocales: [],
|
|
583
|
+
staticCounts: { packages: serviceCount || 0, sections: 0 },
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
// OPSIONAL — aktifkan live update realtime. Hapus jika tak perlu.
|
|
588
|
+
subscribeLiveUpdate(onEvent) {
|
|
589
|
+
const channel = supabase
|
|
590
|
+
.channel("laundry-dashboard-live")
|
|
591
|
+
.on("postgres_changes",
|
|
592
|
+
{ event: "*", schema: "public", table: "laundry_orders" }, onEvent)
|
|
593
|
+
.subscribe();
|
|
594
|
+
return () => supabase.removeChannel(channel);
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
> Tips: kalau kamu **tidak** butuh realtime, cukup hapus fungsi
|
|
601
|
+
> `subscribeLiveUpdate`. Badge "Live" otomatis tidak muncul.
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
#### Langkah 2 — Tulis ADAPTER
|
|
606
|
+
|
|
607
|
+
Buat file `src/adapters/laundryAdapter.js`. Tugasnya: ubah `raw` dari data source
|
|
608
|
+
menjadi `{ stats, charts, table }`. Pakai helper `toNumber` & `buildDayBuckets`
|
|
609
|
+
dari modul agar tidak menulis ulang logika tanggal.
|
|
436
610
|
|
|
437
611
|
```javascript
|
|
438
612
|
// src/adapters/laundryAdapter.js
|
|
439
613
|
import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
|
|
440
614
|
|
|
615
|
+
// (1) State kosong — dipakai modul sebagai initial state & saat error.
|
|
616
|
+
// Strukturnya HARUS sama dengan output adaptLaundryData di bawah.
|
|
441
617
|
export function createEmptyLaundryData() {
|
|
442
618
|
return {
|
|
443
|
-
stats: { bookingsConfirm: 0,
|
|
444
|
-
packages: 0, sections: 0, avgRevenue: 0, conversionRate: 0 },
|
|
619
|
+
stats: { bookingsConfirm: 0, revenueConfirm: 0 },
|
|
445
620
|
charts: { dailyTrends: [], statusDistribution: [], audienceDistribution: [], topPackages: [] },
|
|
446
621
|
table: { recentBookings: [] },
|
|
447
622
|
};
|
|
448
623
|
}
|
|
449
624
|
|
|
625
|
+
// (2) Fungsi adapter utama.
|
|
450
626
|
export function adaptLaundryData({ raw, filters, range, dateLocale, labels }) {
|
|
451
627
|
if (!raw) return createEmptyLaundryData();
|
|
452
628
|
|
|
453
|
-
const orders = raw.bookings || [];
|
|
629
|
+
const orders = raw.bookings || [];
|
|
454
630
|
const recent = raw.recent || [];
|
|
455
631
|
|
|
456
|
-
//
|
|
632
|
+
// Siapkan bucket harian kosong untuk chart tren (1 bucket = 1 hari)
|
|
457
633
|
const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
|
|
458
|
-
const dayLookup = new Map(dailyBuckets.map(b => [b.dateKey, b]));
|
|
634
|
+
const dayLookup = new Map(dailyBuckets.map((b) => [b.dateKey, b]));
|
|
459
635
|
|
|
460
|
-
let confirmed = 0, pending = 0, revenue = 0;
|
|
461
636
|
const statusMap = new Map();
|
|
637
|
+
let confirmed = 0;
|
|
638
|
+
let revenue = 0;
|
|
462
639
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const
|
|
466
|
-
const
|
|
640
|
+
// Loop semua order: hitung total & isi bucket harian
|
|
641
|
+
orders.forEach((row) => {
|
|
642
|
+
const status = String(row.status || "pending").toLowerCase();
|
|
643
|
+
const amount = toNumber(row.total_price); // ← kolom DB kamu
|
|
644
|
+
const dayKey = String(row.created_at || "").slice(0, 10);
|
|
467
645
|
|
|
468
646
|
statusMap.set(status, (statusMap.get(status) || 0) + 1);
|
|
469
647
|
|
|
@@ -473,98 +651,211 @@ export function adaptLaundryData({ raw, filters, range, dateLocale, labels }) {
|
|
|
473
651
|
bucket.revenue += amount;
|
|
474
652
|
}
|
|
475
653
|
|
|
476
|
-
if (status === "confirmed") { confirmed
|
|
477
|
-
else if (status === "pending") pending++;
|
|
654
|
+
if (status === "confirmed") { confirmed += 1; revenue += amount; }
|
|
478
655
|
});
|
|
479
656
|
|
|
480
657
|
return {
|
|
658
|
+
// stats: key di sini HARUS cocok dengan valueKey di widget config
|
|
481
659
|
stats: {
|
|
482
660
|
bookingsConfirm: confirmed,
|
|
483
|
-
bookingsPending: pending,
|
|
484
661
|
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
662
|
},
|
|
490
663
|
charts: {
|
|
491
664
|
dailyTrends: dailyBuckets,
|
|
492
665
|
statusDistribution: Array.from(statusMap.entries()).map(([status, count]) => ({
|
|
493
|
-
status,
|
|
666
|
+
status,
|
|
667
|
+
label: labels?.formatStatusLabel?.(status) || status,
|
|
668
|
+
count,
|
|
494
669
|
})),
|
|
495
|
-
audienceDistribution: [],
|
|
496
|
-
topPackages: [],
|
|
670
|
+
audienceDistribution: [], // tidak dipakai laundry → kosong
|
|
671
|
+
topPackages: [], // tidak dipakai laundry → kosong
|
|
497
672
|
},
|
|
498
673
|
table: {
|
|
499
|
-
recentBookings: recent.map(row =>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
674
|
+
recentBookings: recent.map((row) => {
|
|
675
|
+
const status = String(row.status || "pending").toLowerCase();
|
|
676
|
+
return {
|
|
677
|
+
id: row.id,
|
|
678
|
+
createdAt: row.created_at,
|
|
679
|
+
customerName: row.customer_name || "-",
|
|
680
|
+
packageName: row.service_type || "-", // ← kolom DB kamu
|
|
681
|
+
audienceLabel: "-",
|
|
682
|
+
totalIDR: toNumber(row.total_price),
|
|
683
|
+
status,
|
|
684
|
+
statusLabel: labels?.formatStatusLabel?.(status) || status,
|
|
685
|
+
};
|
|
686
|
+
}),
|
|
509
687
|
},
|
|
510
688
|
};
|
|
511
689
|
}
|
|
512
690
|
```
|
|
513
691
|
|
|
514
|
-
|
|
692
|
+
> Catatan: `audienceDistribution` dan `topPackages` boleh `[]` jika domainmu
|
|
693
|
+
> tidak butuh. Lebih baik lagi: **jangan daftarkan** chart itu di widget config
|
|
694
|
+
> (Langkah 3), supaya tidak tampil sama sekali.
|
|
515
695
|
|
|
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 });
|
|
696
|
+
---
|
|
528
697
|
|
|
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;
|
|
698
|
+
#### Langkah 3 — Tulis WIDGET CONFIG
|
|
539
699
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
.select("*", { count: "exact", head: true });
|
|
700
|
+
Buat file `src/config/laundryConfig.js`. Ini murni deklaratif — menentukan
|
|
701
|
+
widget apa yang muncul. `valueKey` harus cocok dengan key `stats` dari adapter,
|
|
702
|
+
dan `label` adalah key dari objek labels (Langkah 4).
|
|
544
703
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
704
|
+
```javascript
|
|
705
|
+
// src/config/laundryConfig.js
|
|
706
|
+
export const laundryConfig = {
|
|
707
|
+
id: "laundry.dashboard",
|
|
708
|
+
defaultFilters: {
|
|
709
|
+
statusScope: "confirmed", // filter awal saat dibuka
|
|
710
|
+
daysPreset: 30, // tampilkan 30 hari terakhir
|
|
711
|
+
sortPkgBy: "bookings",
|
|
712
|
+
sortPkgDir: "desc",
|
|
713
|
+
},
|
|
714
|
+
widgets: {
|
|
715
|
+
// 2 stat card → valueKey cocok dgn stats di adapter
|
|
716
|
+
stats: [
|
|
717
|
+
{ id: "orders", label: "confirmedBookings", icon: "TrendingUp",
|
|
718
|
+
valueKey: "bookingsConfirm", format: "number", accentColor: "blue" },
|
|
719
|
+
{ id: "revenue", label: "confirmedRevenue", icon: "DollarSign",
|
|
720
|
+
valueKey: "revenueConfirm", format: "currency", accentColor: "green" },
|
|
721
|
+
],
|
|
722
|
+
// 2 chart → hanya yang ada datanya (tren & status)
|
|
723
|
+
charts: [
|
|
724
|
+
{ id: "trend", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
|
|
725
|
+
{ id: "status", type: "statusPie", label: "statusDistribution", icon: "PieChart" },
|
|
726
|
+
],
|
|
727
|
+
// 1 tabel → kolom mengacu ke field recentBookings dari adapter
|
|
728
|
+
table: {
|
|
729
|
+
id: "recentOrders",
|
|
730
|
+
label: "recentBookings",
|
|
731
|
+
icon: "Calendar",
|
|
732
|
+
emptyLabel: "noRecentBookings",
|
|
733
|
+
columns: [
|
|
734
|
+
{ id: "date", label: "date", accessor: "createdAt", type: "date" },
|
|
735
|
+
{ id: "customer", label: "customer", accessor: "customerName" },
|
|
736
|
+
{ id: "service", label: "package", accessor: "packageName" },
|
|
737
|
+
{ id: "total", label: "total", accessor: "totalIDR", type: "currency" },
|
|
738
|
+
{ id: "status", label: "status", accessor: "statusLabel",
|
|
739
|
+
type: "statusBadge", statusAccessor: "status" },
|
|
740
|
+
],
|
|
554
741
|
},
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
```
|
|
555
745
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
746
|
+
> Detail tiap field config (tipe kolom, nama ikon, format) ada di
|
|
747
|
+
> [Bab 8 — Konfigurasi Widget](#8-konfigurasi-widget).
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
#### Langkah 4 — Rangkai semuanya di halaman Dashboard
|
|
752
|
+
|
|
753
|
+
Sekarang gabungkan ke-3 file di atas + objek labels, lalu serahkan ke
|
|
754
|
+
`useReusableDashboard` dan render `ReusableDashboardView`.
|
|
755
|
+
|
|
756
|
+
```jsx
|
|
757
|
+
// src/pages/LaundryDashboard.jsx
|
|
758
|
+
import React, { useMemo } from "react";
|
|
759
|
+
import { supabase } from "../lib/supabaseClient";
|
|
760
|
+
import {
|
|
761
|
+
ReusableDashboardView,
|
|
762
|
+
useReusableDashboard,
|
|
763
|
+
} from "@rozaqi02/reusable-dashboard";
|
|
764
|
+
|
|
765
|
+
// 3 file yang baru kamu buat:
|
|
766
|
+
import { laundryConfig } from "../config/laundryConfig";
|
|
767
|
+
import { createLaundrySupabaseSource } from "../datasources/laundryDataSource";
|
|
768
|
+
import { adaptLaundryData, createEmptyLaundryData } from "../adapters/laundryAdapter";
|
|
769
|
+
|
|
770
|
+
// Data source dibuat sekali di luar komponen (anti re-render)
|
|
771
|
+
const source = createLaundrySupabaseSource(supabase);
|
|
772
|
+
|
|
773
|
+
// Label UI — semua teks yang tampil. Sesuaikan bahasa/konteks bisnismu.
|
|
774
|
+
const labels = {
|
|
775
|
+
title: "Dashboard Laundry",
|
|
776
|
+
refresh: "Refresh",
|
|
777
|
+
liveUpdate: "Live",
|
|
778
|
+
loadFailed: "Gagal memuat data.",
|
|
779
|
+
retry: "Coba Lagi",
|
|
780
|
+
allStatus: "Semua Status",
|
|
781
|
+
confirmedOnly: "Selesai",
|
|
782
|
+
pendingOnly: "Proses",
|
|
783
|
+
reset: "Reset",
|
|
784
|
+
confirmedBookings: "Total Order",
|
|
785
|
+
confirmedRevenue: "Total Pendapatan",
|
|
786
|
+
dailyTrends: "Tren Harian",
|
|
787
|
+
statusDistribution: "Distribusi Status",
|
|
788
|
+
recentBookings: "Order Terbaru",
|
|
789
|
+
noRecentBookings: "Belum ada order",
|
|
790
|
+
date: "Tanggal",
|
|
791
|
+
customer: "Pelanggan",
|
|
792
|
+
package: "Layanan",
|
|
793
|
+
total: "Total",
|
|
794
|
+
status: "Status",
|
|
795
|
+
bookingsMetric: "Order",
|
|
796
|
+
revenueMetric: "Pendapatan",
|
|
797
|
+
confirmedBookingMetric: "Order (Selesai)",
|
|
798
|
+
confirmedRevenueMetric: "Pendapatan (Selesai)",
|
|
799
|
+
dayLabel: (n) => `${n} hari`,
|
|
800
|
+
formatStatusLabel: (s) =>
|
|
801
|
+
({ confirmed: "Selesai", pending: "Proses", cancelled: "Batal" })[s] || s,
|
|
802
|
+
formatAudienceLabel: (v) => v || "-",
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
export default function LaundryDashboard() {
|
|
806
|
+
const state = useReusableDashboard({
|
|
807
|
+
config: laundryConfig, // ← Langkah 3
|
|
808
|
+
dataSource: source, // ← Langkah 1
|
|
809
|
+
adapter: adaptLaundryData, // ← Langkah 2
|
|
810
|
+
createEmptyState: createEmptyLaundryData, // ← Langkah 2
|
|
811
|
+
languageCode: "id",
|
|
812
|
+
dateLocale: "id-ID",
|
|
813
|
+
labels,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
return (
|
|
817
|
+
<ReusableDashboardView
|
|
818
|
+
config={laundryConfig}
|
|
819
|
+
labels={labels}
|
|
820
|
+
loading={state.loading}
|
|
821
|
+
error={state.error}
|
|
822
|
+
filters={state.filters}
|
|
823
|
+
onFilterChange={state.updateFilter}
|
|
824
|
+
onResetFilters={state.resetFilters}
|
|
825
|
+
onRefresh={state.refresh}
|
|
826
|
+
data={state.data}
|
|
827
|
+
dateLocale="id-ID"
|
|
828
|
+
liveUpdateEnabled={state.liveUpdateEnabled}
|
|
829
|
+
/>
|
|
830
|
+
);
|
|
565
831
|
}
|
|
566
832
|
```
|
|
567
833
|
|
|
834
|
+
Selesai. Buka halaman tersebut → dashboard laundry tampil lengkap dengan data
|
|
835
|
+
dari Supabase, filter tanggal, search, sort, dan pagination — semua sudah
|
|
836
|
+
ditangani modul.
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
### 7.3 Checklist & troubleshooting domain baru
|
|
841
|
+
|
|
842
|
+
| Gejala | Penyebab umum | Solusi |
|
|
843
|
+
|--------|---------------|--------|
|
|
844
|
+
| Stat card menampilkan `0` terus | `valueKey` di config tidak cocok dengan key `stats` adapter | Samakan nama, mis. `valueKey: "revenueConfirm"` ↔ `stats.revenueConfirm` |
|
|
845
|
+
| Chart kosong | Data source tidak mengembalikan `bookings`, atau status bukan `"confirmed"` | Pastikan field bernama `bookings` & status lowercase |
|
|
846
|
+
| Tabel kosong padahal ada data | Data source tidak mengembalikan `recent` | Tambahkan query `recent` (10 terbaru) |
|
|
847
|
+
| Error "is not a function" | Lupa pasang `createEmptyState` | Wajib kirim `createEmptyXxxData` ke hook |
|
|
848
|
+
| Label tampil sebagai key mentah | Key di config (`label`) tidak ada di objek `labels` | Tambahkan key tersebut di objek labels |
|
|
849
|
+
| Badge "Live" tidak muncul | `subscribeLiveUpdate` tidak ada / Realtime belum aktif | Tambahkan fungsi + aktifkan Realtime di Supabase |
|
|
850
|
+
|
|
851
|
+
Checklist final sebelum rilis ke client:
|
|
852
|
+
- [ ] Data source mengembalikan `{ bookings, recent }` (nama persis).
|
|
853
|
+
- [ ] Adapter mengembalikan `{ stats, charts, table }` lengkap.
|
|
854
|
+
- [ ] `createEmptyXxxData` punya struktur sama dengan output adapter.
|
|
855
|
+
- [ ] Semua `valueKey` di config ada di `stats`.
|
|
856
|
+
- [ ] Semua `label`/`emptyLabel` di config ada di objek `labels`.
|
|
857
|
+
- [ ] RLS Supabase mengizinkan `SELECT` untuk role `anon` (lihat [Bab 6](#6-setup-database-supabase)).
|
|
858
|
+
|
|
568
859
|
---
|
|
569
860
|
|
|
570
861
|
## 8. Konfigurasi Widget
|
|
@@ -861,13 +1152,48 @@ npm run build # Output ke dist/
|
|
|
861
1152
|
# Test
|
|
862
1153
|
npm test # Jest + React Testing Library
|
|
863
1154
|
npm test -- --coverage # Lihat code coverage
|
|
1155
|
+
```
|
|
864
1156
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
npm
|
|
868
|
-
|
|
1157
|
+
### Cara Publish Versi Baru ke npm
|
|
1158
|
+
|
|
1159
|
+
Modul ini publik di npm: `@rozaqi02/reusable-dashboard`. Setiap kali kode modul
|
|
1160
|
+
berubah, kamu wajib publish versi baru agar perubahan sampai ke client.
|
|
1161
|
+
|
|
1162
|
+
```bash
|
|
1163
|
+
# 1. Pastikan sudah login (sekali saja per device)
|
|
1164
|
+
npm login # buka browser untuk autentikasi
|
|
1165
|
+
npm whoami # verifikasi → harus muncul "rozaqi02"
|
|
1166
|
+
|
|
1167
|
+
# 2. Naikkan versi (pilih salah satu sesuai jenis perubahan)
|
|
1168
|
+
npm version patch # 1.1.2 → 1.1.3 (bug fix / perbaikan kecil)
|
|
1169
|
+
npm version minor # 1.1.2 → 1.2.0 (fitur baru, tanpa breaking change)
|
|
1170
|
+
npm version major # 1.1.2 → 2.0.0 (breaking change)
|
|
1171
|
+
|
|
1172
|
+
# 3. Publish (build otomatis jalan dulu lewat prepublishOnly)
|
|
1173
|
+
npm publish # akan meminta OTP (One-Time Password)
|
|
869
1174
|
```
|
|
870
1175
|
|
|
1176
|
+
> **Catatan OTP:** karena akun npm kamu mengaktifkan 2FA, `npm publish` akan
|
|
1177
|
+
> meminta one-time password. Masukkan kode dari aplikasi authenticator kamu
|
|
1178
|
+
> (atau gunakan `npm publish --otp=123456`). Langkah ini **harus dijalankan
|
|
1179
|
+
> manual di terminal kamu** — tidak bisa diotomasi.
|
|
1180
|
+
|
|
1181
|
+
> **Publish ≠ git push.** `npm publish` hanya mengunggah isi folder `dist/`
|
|
1182
|
+
> (lihat field `files` di `package.json`) ke registry npm. Tidak bergantung pada
|
|
1183
|
+
> commit/push git terakhir. Jadi kamu tidak perlu push ke GitHub dulu agar
|
|
1184
|
+
> bisa publish — keduanya independen. (Tetap disarankan commit & push agar
|
|
1185
|
+
> source code di GitHub sinkron dengan versi yang dirilis.)
|
|
1186
|
+
|
|
1187
|
+
### Update Modul di Project Client
|
|
1188
|
+
|
|
1189
|
+
Setelah versi baru terbit, di setiap project yang memakai modul:
|
|
1190
|
+
|
|
1191
|
+
```bash
|
|
1192
|
+
npm install @rozaqi02/reusable-dashboard@latest
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
Lalu restart dev server (`npm start` / `npm run dev`) agar perubahan termuat.
|
|
1196
|
+
|
|
871
1197
|
---
|
|
872
1198
|
|
|
873
1199
|
## License
|