@rozaqi02/reusable-dashboard 1.0.1 → 1.1.2
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 +622 -314
- package/dist/index.cjs +200 -479
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +742 -0
- package/dist/index.css.map +1 -0
- package/dist/index.js +200 -479
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -1,85 +1,181 @@
|
|
|
1
1
|
# @rozaqi02/reusable-dashboard
|
|
2
2
|
|
|
3
|
-
Modul dashboard admin reusable untuk UMKM berbasis React +
|
|
4
|
-
Mendukung berbagai domain bisnis (travel, toko online,
|
|
5
|
-
|
|
3
|
+
Modul dashboard admin reusable untuk aplikasi UMKM berbasis React + Supabase.
|
|
4
|
+
Mendukung berbagai domain bisnis (travel, toko online, UMKM generik) tanpa menulis ulang komponen.
|
|
5
|
+
CSS sudah terbundle — tidak perlu konfigurasi Tailwind.
|
|
6
6
|
|
|
7
|
-
[](./CHANGELOG.md)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
[](https://www.npmjs.com/package/@rozaqi02/reusable-dashboard)
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Daftar Isi
|
|
14
|
+
|
|
15
|
+
1. [Instalasi](#1-instalasi)
|
|
16
|
+
2. [Prasyarat Teknis](#2-prasyarat-teknis)
|
|
17
|
+
3. [Preset yang Tersedia](#3-preset-yang-tersedia)
|
|
18
|
+
4. [Quick Start — Toko Sepatu (Contoh Lengkap)](#4-quick-start--toko-sepatu)
|
|
19
|
+
5. [Quick Start — Cidika Travel](#5-quick-start--cidika-travel)
|
|
20
|
+
6. [Setup Database Supabase](#6-setup-database-supabase)
|
|
21
|
+
7. [Cara Membuat Adapter untuk Domain Bisnis Baru](#7-cara-membuat-adapter-untuk-domain-bisnis-baru)
|
|
22
|
+
8. [Konfigurasi Widget](#8-konfigurasi-widget)
|
|
23
|
+
9. [Label & Internasionalisasi](#9-label--internasionalisasi)
|
|
24
|
+
10. [API Reference Lengkap](#10-api-reference-lengkap)
|
|
25
|
+
11. [Pengembangan & Kontribusi](#11-pengembangan--kontribusi)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 1. Instalasi
|
|
14
30
|
|
|
15
31
|
```bash
|
|
16
32
|
npm install @rozaqi02/reusable-dashboard
|
|
33
|
+
npm install recharts @supabase/supabase-js
|
|
17
34
|
```
|
|
18
35
|
|
|
19
|
-
|
|
36
|
+
> **Catatan:** CSS sudah otomatis terbundle di dalam paket ini (`dist/index.css`).
|
|
37
|
+
> Tidak perlu menginstall atau mengkonfigurasi Tailwind CSS.
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
- **React** >= 18.0.0
|
|
23
|
-
- **Supabase** account dan project yang sudah dibuat
|
|
24
|
-
- **Tailwind CSS** >= 3.0 dikonfigurasi pada project konsumen
|
|
39
|
+
---
|
|
25
40
|
|
|
26
|
-
##
|
|
41
|
+
## 2. Prasyarat Teknis
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
| Prasyarat | Versi Minimum | Keterangan |
|
|
44
|
+
|-----------|---------------|------------|
|
|
45
|
+
| Node.js | 16.x | Runtime JavaScript |
|
|
46
|
+
| React | 18.0.0 | UI framework |
|
|
47
|
+
| recharts | 2.0.0 | Library chart (AreaChart, PieChart, BarChart) |
|
|
48
|
+
| @supabase/supabase-js | 2.0.0 | Client Supabase untuk koneksi database |
|
|
49
|
+
| Akun Supabase | — | Gratis di [supabase.com](https://supabase.com) |
|
|
29
50
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```
|
|
51
|
+
Modul ini dirancang khusus untuk aplikasi React yang menggunakan Supabase sebagai backend.
|
|
52
|
+
Belum mendukung backend lain (Firebase, REST API, GraphQL) tanpa menulis data source sendiri.
|
|
33
53
|
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3. Preset yang Tersedia
|
|
57
|
+
|
|
58
|
+
Modul menyediakan **3 preset siap pakai** untuk domain bisnis yang berbeda:
|
|
59
|
+
|
|
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 | Bisa pakai struktur toko sepatu atau buat sendiri |
|
|
65
|
+
|
|
66
|
+
Setiap preset sudah termasuk: konfigurasi widget, data adapter, dan data source Supabase.
|
|
36
67
|
|
|
37
68
|
---
|
|
38
69
|
|
|
39
|
-
##
|
|
70
|
+
## 4. Quick Start — Toko Sepatu
|
|
40
71
|
|
|
41
|
-
|
|
72
|
+
Ini adalah contoh paling lengkap. Ikuti step by step dari nol.
|
|
42
73
|
|
|
43
|
-
|
|
74
|
+
### Step 1 — Setup Supabase
|
|
44
75
|
|
|
45
|
-
|
|
46
|
-
import { createClient } from '@supabase/supabase-js';
|
|
76
|
+
**A. Buat project Supabase** di [app.supabase.com](https://app.supabase.com)
|
|
47
77
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
78
|
+
**B. Jalankan SQL schema** berikut di Supabase SQL Editor
|
|
79
|
+
(Settings → SQL Editor → New Query):
|
|
80
|
+
|
|
81
|
+
```sql
|
|
82
|
+
-- Tabel pelanggan
|
|
83
|
+
CREATE TABLE public.customers (
|
|
84
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
85
|
+
name text NOT NULL,
|
|
86
|
+
email text,
|
|
87
|
+
phone text,
|
|
88
|
+
city text,
|
|
89
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
90
|
+
CONSTRAINT customers_pkey PRIMARY KEY (id)
|
|
52
91
|
);
|
|
53
|
-
```
|
|
54
92
|
|
|
55
|
-
|
|
93
|
+
-- Tabel produk
|
|
94
|
+
CREATE TABLE public.products (
|
|
95
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
96
|
+
name text NOT NULL,
|
|
97
|
+
brand text NOT NULL DEFAULT 'Generic',
|
|
98
|
+
category text NOT NULL DEFAULT 'sneakers',
|
|
99
|
+
price_idr integer NOT NULL,
|
|
100
|
+
stock integer NOT NULL DEFAULT 0,
|
|
101
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
102
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
103
|
+
CONSTRAINT products_pkey PRIMARY KEY (id)
|
|
104
|
+
);
|
|
56
105
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
106
|
+
-- Tabel pesanan
|
|
107
|
+
CREATE TABLE public.orders (
|
|
108
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
109
|
+
customer_id uuid NOT NULL,
|
|
110
|
+
total_amount integer NOT NULL,
|
|
111
|
+
status text NOT NULL DEFAULT 'pending',
|
|
112
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
113
|
+
CONSTRAINT orders_pkey PRIMARY KEY (id),
|
|
114
|
+
CONSTRAINT orders_customer_id_fkey FOREIGN KEY (customer_id)
|
|
115
|
+
REFERENCES public.customers(id)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
-- Tabel detail item pesanan
|
|
119
|
+
CREATE TABLE public.order_items (
|
|
120
|
+
id bigint GENERATED ALWAYS AS IDENTITY,
|
|
121
|
+
order_id uuid NOT NULL,
|
|
122
|
+
product_id uuid NOT NULL,
|
|
123
|
+
qty integer NOT NULL DEFAULT 1,
|
|
124
|
+
price_idr integer NOT NULL,
|
|
125
|
+
subtotal integer NOT NULL,
|
|
126
|
+
CONSTRAINT order_items_pkey PRIMARY KEY (id),
|
|
127
|
+
CONSTRAINT order_items_order_id_fkey FOREIGN KEY (order_id)
|
|
128
|
+
REFERENCES public.orders(id),
|
|
129
|
+
CONSTRAINT order_items_product_id_fkey FOREIGN KEY (product_id)
|
|
130
|
+
REFERENCES public.products(id)
|
|
131
|
+
);
|
|
61
132
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
133
|
+
-- Aktifkan Realtime (untuk fitur live update)
|
|
134
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.orders;
|
|
135
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.products;
|
|
136
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.customers;
|
|
65
137
|
```
|
|
66
138
|
|
|
67
|
-
|
|
139
|
+
**C. Isi data contoh** (opsional, untuk testing):
|
|
68
140
|
|
|
69
|
-
|
|
141
|
+
File seed data lengkap tersedia di:
|
|
142
|
+
`examples/toko-sepatu-example/seed-data.sql` dalam repositori ini.
|
|
70
143
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
| Cidika Travel | `examples/cidika-travel-example/supabase-schema.sql` |
|
|
74
|
-
| Toko Sepatu | `examples/toko-sepatu-example/supabase-schema.sql` |
|
|
144
|
+
**D. Aktifkan Realtime** di Supabase Dashboard:
|
|
145
|
+
Database → Replication → centang tabel `orders`, `products`, `customers`.
|
|
75
146
|
|
|
76
|
-
### Step
|
|
147
|
+
### Step 2 — Buat Supabase client
|
|
77
148
|
|
|
78
|
-
|
|
149
|
+
```js
|
|
150
|
+
// src/lib/supabaseClient.js
|
|
151
|
+
import { createClient } from '@supabase/supabase-js';
|
|
152
|
+
|
|
153
|
+
export const supabase = createClient(
|
|
154
|
+
process.env.REACT_APP_SUPABASE_URL, // CRA
|
|
155
|
+
process.env.REACT_APP_SUPABASE_ANON_KEY
|
|
156
|
+
// Jika pakai Vite: import.meta.env.VITE_SUPABASE_URL
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```env
|
|
161
|
+
# .env (di root project)
|
|
162
|
+
REACT_APP_SUPABASE_URL=https://xxxxxx.supabase.co
|
|
163
|
+
REACT_APP_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
164
|
+
|
|
165
|
+
# Jika pakai Vite:
|
|
166
|
+
# VITE_SUPABASE_URL=https://xxxxxx.supabase.co
|
|
167
|
+
# VITE_SUPABASE_ANON_KEY=eyJ...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Nilai URL dan ANON KEY didapat dari Supabase Dashboard:
|
|
171
|
+
**Settings → API → Project URL & anon key**.
|
|
172
|
+
|
|
173
|
+
### Step 3 — Implementasi halaman Dashboard
|
|
79
174
|
|
|
80
175
|
```jsx
|
|
176
|
+
// src/pages/Dashboard.jsx
|
|
81
177
|
import React, { useMemo } from "react";
|
|
82
|
-
import { supabase } from "
|
|
178
|
+
import { supabase } from "../lib/supabaseClient";
|
|
83
179
|
import {
|
|
84
180
|
ReusableDashboardView,
|
|
85
181
|
tokoSepatuWidgetConfig,
|
|
@@ -89,12 +185,13 @@ import {
|
|
|
89
185
|
useReusableDashboard,
|
|
90
186
|
} from "@rozaqi02/reusable-dashboard";
|
|
91
187
|
|
|
92
|
-
// Inisialisasi data source
|
|
188
|
+
// Inisialisasi data source — dibuat sekali di luar komponen
|
|
93
189
|
const source = createTokoSepatuSupabaseSource(supabase);
|
|
94
190
|
|
|
95
|
-
// Label
|
|
191
|
+
// Label UI dalam Bahasa Indonesia
|
|
192
|
+
// Ganti nilai-nilai ini sesuai bahasa/konteks bisnis kamu
|
|
96
193
|
const labels = {
|
|
97
|
-
title: "Dashboard Toko
|
|
194
|
+
title: "Dashboard Toko Saya",
|
|
98
195
|
refresh: "Refresh",
|
|
99
196
|
liveUpdate: "Live",
|
|
100
197
|
loadFailed: "Gagal memuat data.",
|
|
@@ -104,10 +201,12 @@ const labels = {
|
|
|
104
201
|
allStatus: "Semua Status",
|
|
105
202
|
showPendingOverlay: "Tampilkan pending",
|
|
106
203
|
allAudience: "Semua",
|
|
204
|
+
audienceDomestic: "Domestic",
|
|
205
|
+
audienceForeign: "Foreign",
|
|
107
206
|
customDate: "Kustom",
|
|
108
207
|
reset: "Reset",
|
|
109
208
|
topSort: "Urutkan",
|
|
110
|
-
sortBookings: "Qty",
|
|
209
|
+
sortBookings: "Qty Terjual",
|
|
111
210
|
sortRevenue: "Revenue",
|
|
112
211
|
sortDesc: "Turun",
|
|
113
212
|
sortAsc: "Naik",
|
|
@@ -123,6 +222,7 @@ const labels = {
|
|
|
123
222
|
date: "Tanggal",
|
|
124
223
|
customer: "Pelanggan",
|
|
125
224
|
package: "Produk",
|
|
225
|
+
audience: "Audiens",
|
|
126
226
|
total: "Total",
|
|
127
227
|
status: "Status",
|
|
128
228
|
noRecentBookings: "Belum ada pesanan",
|
|
@@ -132,7 +232,7 @@ const labels = {
|
|
|
132
232
|
confirmedBookingMetric: "Pesanan (Confirmed)",
|
|
133
233
|
confirmedRevenueMetric: "Pendapatan (Confirmed)",
|
|
134
234
|
unknownAudience: "Unknown",
|
|
135
|
-
dayLabel: (
|
|
235
|
+
dayLabel: (n) => `${n} hari`,
|
|
136
236
|
formatStatusLabel: (s) => ({ confirmed: "Confirmed", pending: "Pending", cancelled: "Cancelled" })[s] || s,
|
|
137
237
|
formatAudienceLabel: (v) => v || "Unknown",
|
|
138
238
|
};
|
|
@@ -166,91 +266,336 @@ export default function Dashboard() {
|
|
|
166
266
|
}
|
|
167
267
|
```
|
|
168
268
|
|
|
169
|
-
|
|
170
|
-
> sebagai pengganti objek labels di atas. Lihat [bagian i18n](#-label--i18n).
|
|
171
|
-
|
|
172
|
-
### Step 4: Jalankan Aplikasi
|
|
269
|
+
### Step 4 — Jalankan
|
|
173
270
|
|
|
174
271
|
```bash
|
|
175
|
-
npm
|
|
272
|
+
npm start # Create React App
|
|
176
273
|
# atau
|
|
177
|
-
npm
|
|
274
|
+
npm run dev # Vite
|
|
178
275
|
```
|
|
179
276
|
|
|
277
|
+
Buka browser → dashboard tampil dengan data dari Supabase.
|
|
278
|
+
|
|
180
279
|
---
|
|
181
280
|
|
|
182
|
-
##
|
|
281
|
+
## 5. Quick Start — Cidika Travel
|
|
183
282
|
|
|
184
|
-
|
|
283
|
+
Ganti hanya 4 baris dari contoh Toko Sepatu di atas:
|
|
185
284
|
|
|
186
|
-
```
|
|
285
|
+
```jsx
|
|
187
286
|
import {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
287
|
+
ReusableDashboardView,
|
|
288
|
+
cidikaWidgetConfig, // ← ganti ini
|
|
289
|
+
createCidikaSupabaseSource, // ← ganti ini
|
|
290
|
+
adaptCidikaDashboardData, // ← ganti ini
|
|
291
|
+
createEmptyDashboardData, // ← ganti ini
|
|
292
|
+
createDashboardLabels,
|
|
293
|
+
useReusableDashboard,
|
|
192
294
|
} from "@rozaqi02/reusable-dashboard";
|
|
193
295
|
|
|
296
|
+
// Jika pakai react-i18next:
|
|
297
|
+
const { t } = useTranslation();
|
|
298
|
+
const labels = useMemo(() => createDashboardLabels(t), [t]);
|
|
299
|
+
|
|
300
|
+
// Jika tidak pakai i18n, gunakan objek labels manual seperti contoh Toko Sepatu
|
|
301
|
+
|
|
194
302
|
const source = createCidikaSupabaseSource(supabase);
|
|
303
|
+
|
|
304
|
+
const state = useReusableDashboard({
|
|
305
|
+
config: cidikaWidgetConfig,
|
|
306
|
+
dataSource: source,
|
|
307
|
+
adapter: adaptCidikaDashboardData,
|
|
308
|
+
createEmptyState: createEmptyDashboardData,
|
|
309
|
+
languageCode: "id",
|
|
310
|
+
dateLocale: "id-ID",
|
|
311
|
+
labels,
|
|
312
|
+
});
|
|
195
313
|
```
|
|
196
314
|
|
|
197
|
-
Tabel Supabase yang dibutuhkan
|
|
315
|
+
**Tabel Supabase yang dibutuhkan untuk Cidika Travel:**
|
|
198
316
|
|
|
199
|
-
|
|
317
|
+
```sql
|
|
318
|
+
-- Tabel utama
|
|
319
|
+
bookings (id, created_at, total_idr, status, package_id, audience, customer_name)
|
|
320
|
+
packages (id, slug, price, is_active)
|
|
321
|
+
package_locales (package_id, lang, title) -- untuk nama paket multi-bahasa
|
|
322
|
+
page_sections (id, page_id, type, order) -- untuk menghitung jumlah section
|
|
323
|
+
```
|
|
200
324
|
|
|
201
|
-
|
|
202
|
-
import {
|
|
203
|
-
tokoSepatuWidgetConfig,
|
|
204
|
-
createTokoSepatuSupabaseSource,
|
|
205
|
-
adaptTokoSepatuData,
|
|
206
|
-
createEmptyTokoSepatuData,
|
|
207
|
-
} from "@rozaqi02/reusable-dashboard";
|
|
325
|
+
---
|
|
208
326
|
|
|
209
|
-
|
|
327
|
+
## 6. Setup Database Supabase
|
|
328
|
+
|
|
329
|
+
### Cara mendapatkan URL dan API Key
|
|
330
|
+
|
|
331
|
+
1. Login ke [app.supabase.com](https://app.supabase.com)
|
|
332
|
+
2. Buka project kamu
|
|
333
|
+
3. Klik **Settings** (ikon gear) di sidebar kiri
|
|
334
|
+
4. Klik **API**
|
|
335
|
+
5. Salin:
|
|
336
|
+
- **Project URL** → `REACT_APP_SUPABASE_URL`
|
|
337
|
+
- **anon public** key → `REACT_APP_SUPABASE_ANON_KEY`
|
|
338
|
+
|
|
339
|
+
### Mengaktifkan Realtime (wajib untuk live update)
|
|
340
|
+
|
|
341
|
+
1. Di Supabase Dashboard, klik **Database** di sidebar
|
|
342
|
+
2. Klik **Replication**
|
|
343
|
+
3. Di bagian **Source**, aktifkan toggle untuk tabel yang ingin di-listen
|
|
344
|
+
4. Atau jalankan SQL berikut:
|
|
345
|
+
|
|
346
|
+
```sql
|
|
347
|
+
-- Untuk Toko Sepatu:
|
|
348
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.orders;
|
|
349
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.products;
|
|
350
|
+
|
|
351
|
+
-- Untuk Cidika Travel:
|
|
352
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.bookings;
|
|
353
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.packages;
|
|
210
354
|
```
|
|
211
355
|
|
|
212
|
-
|
|
356
|
+
### Row Level Security (RLS)
|
|
213
357
|
|
|
214
|
-
|
|
358
|
+
Jika dashboard tidak menampilkan data padahal data sudah ada, kemungkinan
|
|
359
|
+
RLS (Row Level Security) memblokir query. Solusi sementara untuk development:
|
|
215
360
|
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
361
|
+
```sql
|
|
362
|
+
-- PERHATIAN: Hanya untuk development. Jangan gunakan di production
|
|
363
|
+
-- tanpa menambahkan policy yang benar.
|
|
364
|
+
ALTER TABLE public.orders DISABLE ROW LEVEL SECURITY;
|
|
365
|
+
ALTER TABLE public.customers DISABLE ROW LEVEL SECURITY;
|
|
366
|
+
ALTER TABLE public.products DISABLE ROW LEVEL SECURITY;
|
|
222
367
|
```
|
|
223
368
|
|
|
224
|
-
|
|
225
|
-
|
|
369
|
+
Untuk production, tambahkan policy yang sesuai:
|
|
370
|
+
|
|
371
|
+
```sql
|
|
372
|
+
-- Contoh policy: izinkan semua operasi SELECT untuk role anon
|
|
373
|
+
CREATE POLICY "Allow read for anon" ON public.orders
|
|
374
|
+
FOR SELECT TO anon USING (true);
|
|
375
|
+
```
|
|
226
376
|
|
|
227
377
|
---
|
|
228
378
|
|
|
229
|
-
##
|
|
379
|
+
## 7. Cara Membuat Adapter untuk Domain Bisnis Baru
|
|
380
|
+
|
|
381
|
+
Jika bisnis kamu tidak cocok dengan preset yang ada, kamu bisa membuat
|
|
382
|
+
adapter sendiri. Adapter adalah fungsi JavaScript yang mengubah data mentah
|
|
383
|
+
dari Supabase ke format yang dipahami oleh komponen dashboard.
|
|
384
|
+
|
|
385
|
+
### Kontrak data yang harus dikembalikan adapter
|
|
386
|
+
|
|
387
|
+
```javascript
|
|
388
|
+
// Format yang harus dikembalikan oleh adapter
|
|
389
|
+
{
|
|
390
|
+
stats: {
|
|
391
|
+
bookingsConfirm: number, // jumlah transaksi confirmed
|
|
392
|
+
bookingsPending: number, // jumlah transaksi pending
|
|
393
|
+
revenueConfirm: number, // total pendapatan (dalam Rupiah, integer)
|
|
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)
|
|
398
|
+
},
|
|
399
|
+
charts: {
|
|
400
|
+
dailyTrends: [ // untuk area chart tren harian
|
|
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
|
+
}
|
|
408
|
+
],
|
|
409
|
+
statusDistribution: [ // untuk pie chart distribusi status
|
|
410
|
+
{ status: "confirmed", label: "Confirmed", count: 30 },
|
|
411
|
+
{ status: "pending", label: "Pending", count: 10 },
|
|
412
|
+
],
|
|
413
|
+
audienceDistribution: [], // untuk pie chart distribusi audiens (boleh kosong [])
|
|
414
|
+
topPackages: [ // untuk bar chart produk/paket terlaris
|
|
415
|
+
{ packageId: "uuid", name: "Nama Produk", value: 10 },
|
|
416
|
+
],
|
|
417
|
+
},
|
|
418
|
+
table: {
|
|
419
|
+
recentBookings: [ // untuk tabel transaksi terbaru
|
|
420
|
+
{
|
|
421
|
+
id: "uuid",
|
|
422
|
+
createdAt: "2026-05-01T10:00:00Z", // ISO string
|
|
423
|
+
customerName: "Budi Santoso",
|
|
424
|
+
packageName: "Nama Produk",
|
|
425
|
+
audienceLabel: "Domestic", // label audiens (boleh "-")
|
|
426
|
+
totalIDR: 500000, // dalam Rupiah (integer)
|
|
427
|
+
status: "confirmed", // lowercase
|
|
428
|
+
statusLabel: "Confirmed", // label untuk ditampilkan
|
|
429
|
+
}
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Contoh adapter sederhana
|
|
436
|
+
|
|
437
|
+
```javascript
|
|
438
|
+
// src/adapters/laundryAdapter.js
|
|
439
|
+
import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
|
|
440
|
+
|
|
441
|
+
export function createEmptyLaundryData() {
|
|
442
|
+
return {
|
|
443
|
+
stats: { bookingsConfirm: 0, bookingsPending: 0, revenueConfirm: 0,
|
|
444
|
+
packages: 0, sections: 0, avgRevenue: 0, conversionRate: 0 },
|
|
445
|
+
charts: { dailyTrends: [], statusDistribution: [], audienceDistribution: [], topPackages: [] },
|
|
446
|
+
table: { recentBookings: [] },
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function adaptLaundryData({ raw, filters, range, dateLocale, labels }) {
|
|
451
|
+
if (!raw) return createEmptyLaundryData();
|
|
452
|
+
|
|
453
|
+
const orders = raw.bookings || []; // nama field bisa disesuaikan di data source
|
|
454
|
+
const recent = raw.recent || [];
|
|
455
|
+
|
|
456
|
+
// Bangun bucket harian
|
|
457
|
+
const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
|
|
458
|
+
const dayLookup = new Map(dailyBuckets.map(b => [b.dateKey, b]));
|
|
459
|
+
|
|
460
|
+
let confirmed = 0, pending = 0, revenue = 0;
|
|
461
|
+
const statusMap = new Map();
|
|
462
|
+
|
|
463
|
+
orders.forEach(row => {
|
|
464
|
+
const status = (row.status || "pending").toLowerCase();
|
|
465
|
+
const amount = toNumber(row.total_price); // sesuaikan nama kolom
|
|
466
|
+
const dayKey = row.created_at?.slice(0, 10);
|
|
467
|
+
|
|
468
|
+
statusMap.set(status, (statusMap.get(status) || 0) + 1);
|
|
469
|
+
|
|
470
|
+
const bucket = dayLookup.get(dayKey);
|
|
471
|
+
if (bucket && status === "confirmed") {
|
|
472
|
+
bucket.count += 1;
|
|
473
|
+
bucket.revenue += amount;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (status === "confirmed") { confirmed++; revenue += amount; }
|
|
477
|
+
else if (status === "pending") pending++;
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
stats: {
|
|
482
|
+
bookingsConfirm: confirmed,
|
|
483
|
+
bookingsPending: pending,
|
|
484
|
+
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
|
+
},
|
|
490
|
+
charts: {
|
|
491
|
+
dailyTrends: dailyBuckets,
|
|
492
|
+
statusDistribution: Array.from(statusMap.entries()).map(([status, count]) => ({
|
|
493
|
+
status, label: labels?.formatStatusLabel?.(status) || status, count
|
|
494
|
+
})),
|
|
495
|
+
audienceDistribution: [],
|
|
496
|
+
topPackages: [],
|
|
497
|
+
},
|
|
498
|
+
table: {
|
|
499
|
+
recentBookings: recent.map(row => ({
|
|
500
|
+
id: row.id,
|
|
501
|
+
createdAt: row.created_at,
|
|
502
|
+
customerName: row.customer_name || "-",
|
|
503
|
+
packageName: row.service_type || "-", // sesuaikan nama kolom
|
|
504
|
+
audienceLabel: "-",
|
|
505
|
+
totalIDR: toNumber(row.total_price),
|
|
506
|
+
status: (row.status || "pending").toLowerCase(),
|
|
507
|
+
statusLabel: labels?.formatStatusLabel?.(row.status) || row.status,
|
|
508
|
+
})),
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Contoh data source sederhana
|
|
515
|
+
|
|
516
|
+
```javascript
|
|
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 });
|
|
528
|
+
|
|
529
|
+
// Query recent (10 terbaru)
|
|
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;
|
|
539
|
+
|
|
540
|
+
// Hitung jumlah layanan (opsional)
|
|
541
|
+
const { count: serviceCount } = await supabase
|
|
542
|
+
.from("services")
|
|
543
|
+
.select("*", { count: "exact", head: true });
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
bookings: orders, // wajib, nama harus "bookings"
|
|
547
|
+
recent, // wajib, nama harus "recent"
|
|
548
|
+
packageLocales: [], // kosong jika tidak ada multi-bahasa
|
|
549
|
+
staticCounts: {
|
|
550
|
+
packages: serviceCount || 0,
|
|
551
|
+
sections: 0,
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
},
|
|
230
555
|
|
|
231
|
-
|
|
556
|
+
// Opsional: subscribe ke perubahan realtime
|
|
557
|
+
subscribeLiveUpdate(onEvent) {
|
|
558
|
+
const channel = supabase
|
|
559
|
+
.channel("laundry-dashboard-live")
|
|
560
|
+
.on("postgres_changes", { event: "*", schema: "public", table: "laundry_orders" }, onEvent)
|
|
561
|
+
.subscribe();
|
|
562
|
+
return () => supabase.removeChannel(channel);
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
232
569
|
|
|
233
|
-
|
|
570
|
+
## 8. Konfigurasi Widget
|
|
571
|
+
|
|
572
|
+
Widget configuration menentukan apa yang ditampilkan di dashboard.
|
|
573
|
+
Kamu bisa gunakan preset yang ada atau buat konfigurasi baru.
|
|
234
574
|
|
|
235
575
|
```javascript
|
|
236
576
|
export const myConfig = {
|
|
237
|
-
id: "my
|
|
577
|
+
id: "my.dashboard",
|
|
578
|
+
|
|
579
|
+
// Filter default saat pertama kali dibuka
|
|
238
580
|
defaultFilters: {
|
|
239
|
-
statusScope: "
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
581
|
+
statusScope: "confirmed", // "confirmed" | "pending" | "all"
|
|
582
|
+
includePendingOverlay: false, // tampilkan overlay pending di chart?
|
|
583
|
+
audience: "", // "" | "domestic" | "foreign"
|
|
584
|
+
daysPreset: 30, // 7 | 30 | 90 | 0 (0 = custom range)
|
|
585
|
+
sortPkgBy: "revenue", // "bookings" | "revenue"
|
|
586
|
+
sortPkgDir: "desc", // "desc" | "asc"
|
|
245
587
|
},
|
|
588
|
+
|
|
246
589
|
widgets: {
|
|
590
|
+
// Stat cards (1–4 kartu)
|
|
247
591
|
stats: [
|
|
248
592
|
{
|
|
249
593
|
id: "totalOrders",
|
|
250
|
-
label: "confirmedBookings",
|
|
251
|
-
icon: "TrendingUp",
|
|
252
|
-
valueKey: "bookingsConfirm",
|
|
253
|
-
format: "number",
|
|
594
|
+
label: "confirmedBookings", // key dari objek labels
|
|
595
|
+
icon: "TrendingUp", // nama ikon Lucide
|
|
596
|
+
valueKey: "bookingsConfirm", // key dari data.stats yang dikembalikan adapter
|
|
597
|
+
format: "number", // "number" | "currency" | "percent"
|
|
598
|
+
accentColor: "blue", // "blue" | "green" | "violet" | "orange" | "sky" | "rose"
|
|
254
599
|
},
|
|
255
600
|
{
|
|
256
601
|
id: "totalRevenue",
|
|
@@ -258,34 +603,41 @@ export const myConfig = {
|
|
|
258
603
|
icon: "DollarSign",
|
|
259
604
|
valueKey: "revenueConfirm",
|
|
260
605
|
format: "currency",
|
|
606
|
+
accentColor: "green",
|
|
261
607
|
},
|
|
262
608
|
],
|
|
609
|
+
|
|
610
|
+
// Chart cards (1–4 chart)
|
|
263
611
|
charts: [
|
|
264
612
|
{
|
|
265
|
-
id: "
|
|
266
|
-
type: "dailyArea",
|
|
267
|
-
label: "dailyTrends",
|
|
613
|
+
id: "trendHarian",
|
|
614
|
+
type: "dailyArea", // "dailyArea" | "statusPie" | "audiencePie" | "topPackagesBar"
|
|
615
|
+
label: "dailyTrends", // key dari objek labels
|
|
268
616
|
icon: "BarChart3",
|
|
269
617
|
},
|
|
270
618
|
{
|
|
271
|
-
id: "
|
|
619
|
+
id: "distribusiStatus",
|
|
272
620
|
type: "statusPie",
|
|
273
621
|
label: "statusDistribution",
|
|
274
622
|
icon: "PieChart",
|
|
275
623
|
},
|
|
276
624
|
],
|
|
625
|
+
|
|
626
|
+
// Tabel (hanya 1)
|
|
277
627
|
table: {
|
|
278
628
|
id: "recentOrders",
|
|
279
|
-
label: "recentBookings",
|
|
629
|
+
label: "recentBookings", // key dari objek labels
|
|
280
630
|
icon: "Calendar",
|
|
281
|
-
emptyLabel: "noRecentBookings",
|
|
631
|
+
emptyLabel: "noRecentBookings", // key dari objek labels, untuk empty state
|
|
282
632
|
columns: [
|
|
283
633
|
{ id: "date", label: "date", accessor: "createdAt", type: "date" },
|
|
284
634
|
{ id: "customer", label: "customer", accessor: "customerName" },
|
|
635
|
+
{ id: "product", label: "package", accessor: "packageName" },
|
|
285
636
|
{ id: "total", label: "total", accessor: "totalIDR", type: "currency" },
|
|
286
637
|
{
|
|
287
638
|
id: "status", label: "status", accessor: "statusLabel",
|
|
288
|
-
type: "statusBadge",
|
|
639
|
+
type: "statusBadge",
|
|
640
|
+
statusAccessor: "status", // kolom yang berisi status lowercase (confirmed/pending/cancelled)
|
|
289
641
|
},
|
|
290
642
|
],
|
|
291
643
|
},
|
|
@@ -293,97 +645,97 @@ export const myConfig = {
|
|
|
293
645
|
};
|
|
294
646
|
```
|
|
295
647
|
|
|
296
|
-
|
|
648
|
+
**Tipe kolom tabel:**
|
|
649
|
+
| `type` | Format Output |
|
|
650
|
+
|--------|---------------|
|
|
651
|
+
| `date` | Diformat dengan `dateLocale` (contoh: "01 Mei 2026") |
|
|
652
|
+
| `currency` | Diformat dengan prefix "Rp" dan titik ribuan |
|
|
653
|
+
| `statusBadge` | Badge berwarna sesuai status |
|
|
654
|
+
| _(tanpa type)_ | Ditampilkan apa adanya (string) |
|
|
297
655
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
// Wajib: mengambil snapshot data dashboard
|
|
304
|
-
async fetchDashboardSnapshot({ fromISO, toISO, audience, statusScope, languageCode }) {
|
|
305
|
-
const { data: orders, error } = await supabase
|
|
306
|
-
.from("orders")
|
|
307
|
-
.select("id, created_at, total_amount, status, customer_id")
|
|
308
|
-
.gte("created_at", fromISO)
|
|
309
|
-
.lte("created_at", toISO);
|
|
656
|
+
**Nama ikon yang tersedia:**
|
|
657
|
+
`TrendingUp`, `TrendingDown`, `DollarSign`, `Users`, `PieChart`, `BarChart3`,
|
|
658
|
+
`Calendar`, `RotateCcw`, `Search`, `ChevronLeft`, `ChevronRight`, `ArrowUp`,
|
|
659
|
+
`ArrowDown`, `AlertCircle`
|
|
310
660
|
|
|
311
|
-
|
|
312
|
-
return { bookings: orders, recent: [], packageLocales: [], staticCounts: {} };
|
|
313
|
-
},
|
|
661
|
+
---
|
|
314
662
|
|
|
315
|
-
|
|
316
|
-
subscribeLiveUpdate(onEvent) {
|
|
317
|
-
const channel = supabase
|
|
318
|
-
.channel("my-dashboard-live")
|
|
319
|
-
.on("postgres_changes", { event: "*", schema: "public", table: "orders" }, onEvent)
|
|
320
|
-
.subscribe();
|
|
321
|
-
return () => { supabase.removeChannel(channel); };
|
|
322
|
-
},
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
```
|
|
663
|
+
## 9. Label & Internasionalisasi
|
|
326
664
|
|
|
327
|
-
###
|
|
665
|
+
### Tanpa i18n (objek manual)
|
|
328
666
|
|
|
329
|
-
|
|
667
|
+
Buat objek labels dengan semua key yang diperlukan:
|
|
330
668
|
|
|
331
669
|
```javascript
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
orders.forEach((row) => {
|
|
340
|
-
if (row.status === "confirmed") {
|
|
341
|
-
totalConfirmed += 1;
|
|
342
|
-
totalRevenue += Number(row.total_amount) || 0;
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
stats: {
|
|
348
|
-
bookingsConfirm: totalConfirmed,
|
|
349
|
-
revenueConfirm: totalRevenue,
|
|
350
|
-
avgRevenue: totalConfirmed > 0 ? Math.round(totalRevenue / totalConfirmed) : 0,
|
|
351
|
-
conversionRate: 0,
|
|
352
|
-
packages: 0,
|
|
353
|
-
sections: 0,
|
|
354
|
-
},
|
|
355
|
-
charts: {
|
|
356
|
-
dailyTrends: [],
|
|
357
|
-
statusDistribution: [],
|
|
358
|
-
audienceDistribution: [],
|
|
359
|
-
topPackages: [],
|
|
360
|
-
},
|
|
361
|
-
table: { recentBookings: [] },
|
|
362
|
-
};
|
|
363
|
-
}
|
|
670
|
+
const labels = {
|
|
671
|
+
// Header
|
|
672
|
+
title: "Dashboard",
|
|
673
|
+
refresh: "Refresh",
|
|
674
|
+
liveUpdate: "Live",
|
|
675
|
+
loadFailed: "Gagal memuat data.",
|
|
676
|
+
retry: "Coba Lagi",
|
|
364
677
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
678
|
+
// Filter
|
|
679
|
+
confirmedOnly: "Confirmed",
|
|
680
|
+
pendingOnly: "Pending",
|
|
681
|
+
allStatus: "Semua Status",
|
|
682
|
+
showPendingOverlay: "Tampilkan pending",
|
|
683
|
+
allAudience: "Semua",
|
|
684
|
+
audienceDomestic: "Domestic",
|
|
685
|
+
audienceForeign: "Foreign",
|
|
686
|
+
customDate: "Kustom",
|
|
687
|
+
reset: "Reset",
|
|
688
|
+
topSort: "Urutkan",
|
|
689
|
+
sortBookings: "Qty Terjual",
|
|
690
|
+
sortRevenue: "Revenue",
|
|
691
|
+
sortDesc: "Turun",
|
|
692
|
+
sortAsc: "Naik",
|
|
375
693
|
|
|
376
|
-
|
|
694
|
+
// Stat cards (sesuaikan dengan valueKey di config)
|
|
695
|
+
confirmedBookings: "Total Pesanan",
|
|
696
|
+
confirmedRevenue: "Total Pendapatan",
|
|
697
|
+
avgRevenue: "Rata-rata / Pesanan",
|
|
698
|
+
conversionRate: "Conversion Rate",
|
|
699
|
+
totalProducts: "Total Produk",
|
|
377
700
|
|
|
378
|
-
|
|
701
|
+
// Chart & tabel
|
|
702
|
+
dailyTrends: "Tren Harian",
|
|
703
|
+
statusDistribution: "Distribusi Status",
|
|
704
|
+
audienceDistribution: "Distribusi Audiens",
|
|
705
|
+
topPackages: "Produk Terlaris",
|
|
706
|
+
recentBookings: "Pesanan Terbaru",
|
|
707
|
+
date: "Tanggal",
|
|
708
|
+
customer: "Pelanggan",
|
|
709
|
+
package: "Produk",
|
|
710
|
+
audience: "Audiens",
|
|
711
|
+
total: "Total",
|
|
712
|
+
status: "Status",
|
|
713
|
+
noRecentBookings: "Belum ada pesanan",
|
|
379
714
|
|
|
380
|
-
|
|
715
|
+
// Chart metrics
|
|
716
|
+
bookingsMetric: "Pesanan",
|
|
717
|
+
revenueMetric: "Pendapatan",
|
|
718
|
+
pendingMetric: "Pesanan (Pending)",
|
|
719
|
+
confirmedBookingMetric: "Pesanan (Confirmed)",
|
|
720
|
+
confirmedRevenueMetric: "Pendapatan (Confirmed)",
|
|
721
|
+
unknownAudience: "Unknown",
|
|
381
722
|
|
|
382
|
-
|
|
723
|
+
// Fungsi formatter (wajib ada)
|
|
724
|
+
dayLabel: (n) => `${n} hari`,
|
|
725
|
+
formatStatusLabel: (status) => ({
|
|
726
|
+
confirmed: "Confirmed",
|
|
727
|
+
pending: "Pending",
|
|
728
|
+
cancelled: "Cancelled",
|
|
729
|
+
shipped: "Dikirim",
|
|
730
|
+
delivered: "Terkirim",
|
|
731
|
+
})[status] || status,
|
|
732
|
+
formatAudienceLabel: (value) => value || "Unknown",
|
|
733
|
+
};
|
|
734
|
+
```
|
|
383
735
|
|
|
384
736
|
### Dengan react-i18next
|
|
385
737
|
|
|
386
|
-
```
|
|
738
|
+
```jsx
|
|
387
739
|
import { useTranslation } from "react-i18next";
|
|
388
740
|
import { createDashboardLabels } from "@rozaqi02/reusable-dashboard";
|
|
389
741
|
|
|
@@ -391,180 +743,136 @@ function Dashboard() {
|
|
|
391
743
|
const { t, i18n } = useTranslation();
|
|
392
744
|
const languageCode = (i18n.language || "id").slice(0, 2);
|
|
393
745
|
const dateLocale = languageCode === "id" ? "id-ID" : "en-US";
|
|
394
|
-
|
|
395
|
-
// createDashboardLabels menerima fungsi t dari react-i18next
|
|
396
746
|
const labels = useMemo(() => createDashboardLabels(t), [t]);
|
|
397
747
|
// ...
|
|
398
748
|
}
|
|
399
749
|
```
|
|
400
750
|
|
|
751
|
+
> `createDashboardLabels(t)` menggunakan key i18n dari namespace default.
|
|
752
|
+
> Pastikan file terjemahan kamu memiliki key `admin.dashboard.*`.
|
|
753
|
+
|
|
401
754
|
---
|
|
402
755
|
|
|
403
|
-
##
|
|
756
|
+
## 10. API Reference Lengkap
|
|
404
757
|
|
|
405
|
-
###
|
|
758
|
+
### Config
|
|
406
759
|
|
|
407
|
-
| Export | Deskripsi |
|
|
408
|
-
|
|
409
|
-
| `cidikaWidgetConfig` | Config dashboard
|
|
410
|
-
| `tokoSepatuWidgetConfig` | Config dashboard
|
|
411
|
-
| `dummyUmkmWidgetConfig` | Config dashboard
|
|
760
|
+
| Export | Tipe | Deskripsi |
|
|
761
|
+
|--------|------|-----------|
|
|
762
|
+
| `cidikaWidgetConfig` | Object | Config dashboard Cidika Travel |
|
|
763
|
+
| `tokoSepatuWidgetConfig` | Object | Config dashboard Toko Sepatu |
|
|
764
|
+
| `dummyUmkmWidgetConfig` | Object | Config dashboard UMKM generik |
|
|
412
765
|
|
|
413
|
-
### Data
|
|
766
|
+
### Data Source
|
|
414
767
|
|
|
415
768
|
| Export | Parameter | Deskripsi |
|
|
416
769
|
|--------|-----------|-----------|
|
|
417
770
|
| `createCidikaSupabaseSource(supabase)` | Supabase client | Data source Cidika Travel |
|
|
418
771
|
| `createTokoSepatuSupabaseSource(supabase)` | Supabase client | Data source Toko Sepatu |
|
|
419
772
|
|
|
420
|
-
### Data
|
|
421
|
-
|
|
422
|
-
| Export | Deskripsi |
|
|
423
|
-
|--------|-----------|
|
|
424
|
-
| `adaptCidikaDashboardData({ raw, filters, range, dateLocale, languageCode, labels })` | Adapter Cidika Travel |
|
|
425
|
-
| `createEmptyDashboardData()` | Empty state Cidika Travel |
|
|
426
|
-
| `adaptTokoSepatuData({ raw, filters, range, dateLocale, languageCode, labels })` | Adapter Toko Sepatu |
|
|
427
|
-
| `createEmptyTokoSepatuData()` | Empty state Toko Sepatu |
|
|
428
|
-
| `adaptDummyUmkmData({ raw, filters, range, dateLocale, languageCode, labels })` | Adapter Dummy UMKM |
|
|
429
|
-
| `createEmptyDummyUmkmData()` | Empty state Dummy UMKM |
|
|
773
|
+
### Data Adapter
|
|
430
774
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
|
434
|
-
|
|
435
|
-
| `
|
|
436
|
-
| `
|
|
775
|
+
| Export | Parameter | Deskripsi |
|
|
776
|
+
|--------|-----------|-----------|
|
|
777
|
+
| `adaptCidikaDashboardData({ raw, filters, range, dateLocale, languageCode, labels })` | — | Adapter Cidika |
|
|
778
|
+
| `createEmptyDashboardData()` | — | Empty state Cidika |
|
|
779
|
+
| `adaptTokoSepatuData({ raw, filters, range, dateLocale, labels })` | — | Adapter Toko Sepatu |
|
|
780
|
+
| `createEmptyTokoSepatuData()` | — | Empty state Toko Sepatu |
|
|
781
|
+
| `adaptDummyUmkmData({ raw, filters, range, dateLocale, labels })` | — | Adapter UMKM generik |
|
|
782
|
+
| `createEmptyDummyUmkmData()` | — | Empty state UMKM generik |
|
|
437
783
|
|
|
438
|
-
|
|
784
|
+
### Hook Utama
|
|
439
785
|
|
|
440
786
|
```typescript
|
|
441
|
-
{
|
|
787
|
+
const state = useReusableDashboard({
|
|
442
788
|
config: object, // Widget configuration
|
|
443
|
-
dataSource: object, // Objek
|
|
444
|
-
adapter: Function, //
|
|
445
|
-
createEmptyState: Function, //
|
|
446
|
-
languageCode:
|
|
447
|
-
dateLocale:
|
|
448
|
-
labels: object,
|
|
449
|
-
}
|
|
789
|
+
dataSource: object, // Objek dari createXxxSupabaseSource()
|
|
790
|
+
adapter: Function, // Fungsi adaptXxxData
|
|
791
|
+
createEmptyState: Function, // Fungsi createEmptyXxxData
|
|
792
|
+
languageCode: "id" | "en",
|
|
793
|
+
dateLocale: "id-ID" | "en-US",
|
|
794
|
+
labels: object,
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// state yang dikembalikan:
|
|
798
|
+
state.data // Data dashboard terproses
|
|
799
|
+
state.loading // boolean — sedang loading?
|
|
800
|
+
state.error // string — pesan error (kosong jika tidak ada)
|
|
801
|
+
state.filters // State filter aktif saat ini
|
|
802
|
+
state.updateFilter // (field: string, value: any) => void
|
|
803
|
+
state.resetFilters // () => void
|
|
804
|
+
state.refresh // ({ silent?: boolean }) => void
|
|
805
|
+
state.liveUpdateEnabled // boolean — realtime subscription aktif?
|
|
806
|
+
state.lastUpdatedAt // Date | null
|
|
807
|
+
state.range // { fromISO, toISO, daysWindow }
|
|
450
808
|
```
|
|
451
809
|
|
|
452
|
-
|
|
810
|
+
### Komponen
|
|
453
811
|
|
|
454
|
-
```typescript
|
|
455
|
-
{
|
|
456
|
-
data: object, // Data dashboard terproses
|
|
457
|
-
loading: boolean, // Status loading
|
|
458
|
-
error: string, // Pesan error (kosong jika tidak ada)
|
|
459
|
-
filters: object, // State filter aktif
|
|
460
|
-
updateFilter: (field, value) => void, // Update satu filter
|
|
461
|
-
resetFilters: () => void, // Reset semua filter ke default
|
|
462
|
-
refresh: ({ silent?: boolean }) => void, // Muat ulang data
|
|
463
|
-
range: { fromISO, toISO, daysWindow }, // Rentang tanggal aktif
|
|
464
|
-
liveUpdateEnabled: boolean, // Apakah realtime subscription aktif
|
|
465
|
-
lastUpdatedAt: Date | null, // Waktu terakhir data diperbarui
|
|
466
|
-
}
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### Components
|
|
470
|
-
|
|
471
|
-
#### Atoms
|
|
472
|
-
| Komponen | Props Utama | Deskripsi |
|
|
473
|
-
|----------|-------------|-----------|
|
|
474
|
-
| `Button` | `variant` (primary\|secondary\|ghost), `size` (sm\|md\|lg), `onClick`, `disabled` | Tombol aksi |
|
|
475
|
-
| `Input` | `type`, `value`, `onChange`, `placeholder`, `label`, `disabled` | Field teks |
|
|
476
|
-
| `Icon` | `name` (nama Lucide), `size`, `className` | Ikon SVG via registry |
|
|
477
|
-
| `Typography` | `variant` (h1\|h2\|h3\|subheading\|body\|caption\|metric) | Teks hierarki |
|
|
478
|
-
| `Badge` | `status` (confirmed\|pending\|cancelled\|success\|info\|default) | Label status |
|
|
479
|
-
| `SkeletonLoader` | `className` | Placeholder loading |
|
|
480
|
-
|
|
481
|
-
#### Molecules
|
|
482
|
-
| Komponen | Props Utama | Deskripsi |
|
|
483
|
-
|----------|-------------|-----------|
|
|
484
|
-
| `StatCard` | `label`, `value`, `icon`, `format` (number\|currency\|percent), `trend` (up\|down) | Kartu metrik |
|
|
485
|
-
| `SearchBar` | `value`, `onSearch`, `placeholder` | Input pencarian |
|
|
486
|
-
| `DateRangeFilter` | `value`, `onChange` | Filter tanggal |
|
|
487
|
-
| `ChartHeader` | `title`, `icon`, `actions` | Header grafik |
|
|
488
|
-
|
|
489
|
-
#### Organisms
|
|
490
|
-
| Komponen | Props Utama | Deskripsi |
|
|
491
|
-
|----------|-------------|-----------|
|
|
492
|
-
| `DataTable` | `columns`, `data`, `labels`, `dateLocale`, `searchable`, `sortable`, `pageSize`, `emptyLabel` | Tabel interaktif |
|
|
493
|
-
| `ChartCard` | `widget`, `labels`, `loading`, `filters`, `chartData` | Kartu grafik |
|
|
494
|
-
| `FilterPanel` | `filters`, `labels`, `onFilterChange`, `onResetFilters` | Panel filter |
|
|
495
|
-
| `SidebarNavigation` | `items`, `activeId`, `onSelect` | Sidebar nav |
|
|
496
|
-
| `TopbarHeader` | `title`, `onRefresh`, `liveUpdateEnabled` | Header atas |
|
|
497
|
-
|
|
498
|
-
#### Templates & Pages
|
|
499
812
|
| Komponen | Deskripsi |
|
|
500
813
|
|----------|-----------|
|
|
501
|
-
| `
|
|
502
|
-
| `
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
|
507
|
-
|
|
508
|
-
| `
|
|
509
|
-
| `
|
|
510
|
-
| `
|
|
511
|
-
| `
|
|
512
|
-
| `
|
|
513
|
-
| `
|
|
514
|
-
| `
|
|
515
|
-
| `
|
|
516
|
-
| `
|
|
517
|
-
|
|
518
|
-
|
|
814
|
+
| `ReusableDashboardView` | Halaman dashboard lengkap — gunakan ini untuk integrasi cepat |
|
|
815
|
+
| `StatCard` | Kartu metrik dengan warna aksen |
|
|
816
|
+
| `ChartCard` | Kartu grafik (area/pie/bar) |
|
|
817
|
+
| `DataTable` | Tabel dengan search, sort, pagination |
|
|
818
|
+
| `FilterPanel` | Panel filter |
|
|
819
|
+
| `SearchBar` | Input pencarian |
|
|
820
|
+
| `DateRangeFilter` | Filter rentang tanggal (dropdown picker) |
|
|
821
|
+
| `Badge` | Label status berwarna |
|
|
822
|
+
| `Button` | Tombol |
|
|
823
|
+
| `Input` | Input field |
|
|
824
|
+
| `Typography` | Komponen teks |
|
|
825
|
+
| `Icon` | Ikon (Lucide) |
|
|
826
|
+
| `SkeletonLoader` | Placeholder loading |
|
|
827
|
+
| `DashboardLayout` | Template layout dengan slot sidebar dan content |
|
|
828
|
+
| `SidebarNavigation` | Navigasi sidebar |
|
|
829
|
+
| `TopbarHeader` | Header atas |
|
|
830
|
+
|
|
831
|
+
### Utility
|
|
832
|
+
|
|
833
|
+
| Function | Deskripsi |
|
|
834
|
+
|----------|-----------|
|
|
835
|
+
| `createDashboardLabels(t)` | Buat labels dari fungsi `t` react-i18next |
|
|
836
|
+
| `createDefaultFilters(base?)` | Buat state filter default |
|
|
837
|
+
| `resolveDateRange({ daysPreset, dateFrom, dateTo })` | Resolve ke `{ fromISO, toISO, daysWindow }` |
|
|
838
|
+
| `formatYYYYMMDD(date)` | Format `Date` ke `"YYYY-MM-DD"` |
|
|
839
|
+
| `formatIDR(value)` | Format angka ke format Rupiah (`1.500.000`) |
|
|
840
|
+
| `formatDate(value, locale)` | Format ISO date string dengan locale |
|
|
841
|
+
| `shortId(value, length?)` | Potong string (default 8 karakter) |
|
|
842
|
+
| `toNumber(value)` | Konversi ke angka, fallback ke 0 |
|
|
843
|
+
| `buildDayBuckets(daysWindow, fromISO, dateLocale)` | Bangun array bucket harian untuk chart |
|
|
844
|
+
| `sortMapEntries(map, direction)` | Urutkan entri Map berdasarkan nilai |
|
|
519
845
|
|
|
520
846
|
---
|
|
521
847
|
|
|
522
|
-
##
|
|
848
|
+
## 11. Pengembangan & Kontribusi
|
|
523
849
|
|
|
524
850
|
```bash
|
|
525
|
-
# Clone
|
|
526
|
-
git clone
|
|
851
|
+
# Clone repository
|
|
852
|
+
git clone https://github.com/rozaqi02/reusable-dashboard-umkm.git
|
|
527
853
|
cd reusable-dashboard-umkm
|
|
854
|
+
|
|
855
|
+
# Install dependencies
|
|
528
856
|
npm install
|
|
529
857
|
|
|
530
|
-
# Build
|
|
531
|
-
npm run build
|
|
858
|
+
# Build
|
|
859
|
+
npm run build # Output ke dist/
|
|
532
860
|
|
|
533
|
-
#
|
|
534
|
-
npm test
|
|
861
|
+
# Test
|
|
862
|
+
npm test # Jest + React Testing Library
|
|
863
|
+
npm test -- --coverage # Lihat code coverage
|
|
535
864
|
|
|
536
|
-
#
|
|
537
|
-
npm
|
|
538
|
-
|
|
539
|
-
npm
|
|
865
|
+
# Publish versi baru
|
|
866
|
+
npm version patch # 1.0.0 → 1.0.1
|
|
867
|
+
npm version minor # 1.0.0 → 1.1.0
|
|
868
|
+
npm publish # Build otomatis sebelum publish
|
|
540
869
|
```
|
|
541
870
|
|
|
542
|
-
**Output build** (`dist/`):
|
|
543
|
-
- `dist/index.js` — ESM (untuk Vite, modern bundler)
|
|
544
|
-
- `dist/index.cjs` — CommonJS (untuk CRA, Node.js)
|
|
545
|
-
- `dist/index.js.map` + `dist/index.cjs.map` — Source maps
|
|
546
|
-
|
|
547
|
-
---
|
|
548
|
-
|
|
549
|
-
## 📄 Examples
|
|
550
|
-
|
|
551
|
-
| Folder | Domain | Konten |
|
|
552
|
-
|--------|--------|--------|
|
|
553
|
-
| `examples/cidika-travel-example/` | Travel agency | `AdminDashboard.jsx`, schema SQL |
|
|
554
|
-
| `examples/toko-sepatu-example/` | Toko online | `TokoSepatuDashboard.jsx`, schema SQL, seed data |
|
|
555
|
-
| `examples/dummy-umkm-example/` | UMKM generik | `DummyUmkmDashboard.jsx` |
|
|
556
|
-
|
|
557
|
-
---
|
|
558
|
-
|
|
559
|
-
## 🔒 License
|
|
560
|
-
|
|
561
|
-
Proprietary — hanya untuk penggunaan oleh pihak yang berwenang.
|
|
562
|
-
|
|
563
871
|
---
|
|
564
872
|
|
|
565
|
-
##
|
|
873
|
+
## License
|
|
566
874
|
|
|
567
|
-
|
|
875
|
+
MIT — Ahmad Abror Rozaqi Fatoni
|
|
568
876
|
|
|
569
877
|
---
|
|
570
878
|
|