@rozaqi02/reusable-dashboard 1.1.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.
Files changed (2) hide show
  1. package/README.md +778 -61
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,68 +1,248 @@
1
1
  # @rozaqi02/reusable-dashboard
2
2
 
3
- Modul dashboard admin reusable untuk UMKM. Dibangun dengan React + Atomic Design + Supabase.
4
- Mendukung berbagai domain bisnis tanpa mengubah komponen inti.
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.
5
6
 
6
- [![version](https://img.shields.io/badge/version-1.1.0-blue)](./CHANGELOG.md)
7
+ [![version](https://img.shields.io/badge/version-1.1.1-blue)](./CHANGELOG.md)
7
8
  [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
9
+ [![npm](https://img.shields.io/badge/npm-%40rozaqi02%2Freusable--dashboard-red)](https://www.npmjs.com/package/@rozaqi02/reusable-dashboard)
8
10
 
9
11
  ---
10
12
 
11
- ## Install
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
12
30
 
13
31
  ```bash
14
32
  npm install @rozaqi02/reusable-dashboard
15
33
  npm install recharts @supabase/supabase-js
16
34
  ```
17
35
 
18
- Hanya 2 perintah. CSS sudah otomatis terbundle tidak perlu konfigurasi Tailwind.
36
+ > **Catatan:** CSS sudah otomatis terbundle di dalam paket ini (`dist/index.css`).
37
+ > Tidak perlu menginstall atau mengkonfigurasi Tailwind CSS.
38
+
39
+ ---
40
+
41
+ ## 2. Prasyarat Teknis
42
+
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) |
50
+
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.
53
+
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.
19
67
 
20
68
  ---
21
69
 
22
- ## Quick Start (5 menit)
70
+ ## 4. Quick Start Toko Sepatu
71
+
72
+ Ini adalah contoh paling lengkap. Ikuti step by step dari nol.
73
+
74
+ ### Step 1 — Setup Supabase
75
+
76
+ **A. Buat project Supabase** di [app.supabase.com](https://app.supabase.com)
77
+
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)
91
+ );
92
+
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
+ );
105
+
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
+ );
132
+
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;
137
+ ```
138
+
139
+ **C. Isi data contoh** (opsional, untuk testing):
140
+
141
+ File seed data lengkap tersedia di:
142
+ `examples/toko-sepatu-example/seed-data.sql` dalam repositori ini.
23
143
 
24
- ### 1. Buat Supabase client
144
+ **D. Aktifkan Realtime** di Supabase Dashboard:
145
+ Database → Replication → centang tabel `orders`, `products`, `customers`.
146
+
147
+ ### Step 2 — Buat Supabase client
25
148
 
26
149
  ```js
27
150
  // src/lib/supabaseClient.js
28
151
  import { createClient } from '@supabase/supabase-js';
152
+
29
153
  export const supabase = createClient(
30
- process.env.REACT_APP_SUPABASE_URL,
154
+ process.env.REACT_APP_SUPABASE_URL, // CRA
31
155
  process.env.REACT_APP_SUPABASE_ANON_KEY
156
+ // Jika pakai Vite: import.meta.env.VITE_SUPABASE_URL
32
157
  );
33
158
  ```
34
159
 
35
160
  ```env
36
- # .env
37
- REACT_APP_SUPABASE_URL=https://xxx.supabase.co
38
- REACT_APP_SUPABASE_ANON_KEY=eyJ...
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...
39
168
  ```
40
169
 
41
- ### 2. Pakai di halaman dashboard
170
+ Nilai URL dan ANON KEY didapat dari Supabase Dashboard:
171
+ **Settings → API → Project URL & anon key**.
172
+
173
+ ### Step 3 — Implementasi halaman Dashboard
42
174
 
43
175
  ```jsx
176
+ // src/pages/Dashboard.jsx
44
177
  import React, { useMemo } from "react";
45
- import { supabase } from "./lib/supabaseClient";
178
+ import { supabase } from "../lib/supabaseClient";
46
179
  import {
47
180
  ReusableDashboardView,
48
- cidikaWidgetConfig,
49
- createCidikaSupabaseSource,
50
- adaptCidikaDashboardData,
51
- createEmptyDashboardData,
52
- createDashboardLabels,
181
+ tokoSepatuWidgetConfig,
182
+ createTokoSepatuSupabaseSource,
183
+ adaptTokoSepatuData,
184
+ createEmptyTokoSepatuData,
53
185
  useReusableDashboard,
54
186
  } from "@rozaqi02/reusable-dashboard";
55
187
 
56
- const source = createCidikaSupabaseSource(supabase);
188
+ // Inisialisasi data source dibuat sekali di luar komponen
189
+ const source = createTokoSepatuSupabaseSource(supabase);
190
+
191
+ // Label UI dalam Bahasa Indonesia
192
+ // Ganti nilai-nilai ini sesuai bahasa/konteks bisnis kamu
193
+ const labels = {
194
+ title: "Dashboard Toko Saya",
195
+ refresh: "Refresh",
196
+ liveUpdate: "Live",
197
+ loadFailed: "Gagal memuat data.",
198
+ retry: "Coba Lagi",
199
+ confirmedOnly: "Confirmed",
200
+ pendingOnly: "Pending",
201
+ allStatus: "Semua Status",
202
+ showPendingOverlay: "Tampilkan pending",
203
+ allAudience: "Semua",
204
+ audienceDomestic: "Domestic",
205
+ audienceForeign: "Foreign",
206
+ customDate: "Kustom",
207
+ reset: "Reset",
208
+ topSort: "Urutkan",
209
+ sortBookings: "Qty Terjual",
210
+ sortRevenue: "Revenue",
211
+ sortDesc: "Turun",
212
+ sortAsc: "Naik",
213
+ confirmedBookings: "Total Pesanan",
214
+ confirmedRevenue: "Total Pendapatan",
215
+ avgRevenue: "Rata-rata / Pesanan",
216
+ conversionRate: "Conversion Rate",
217
+ totalProducts: "Total Produk",
218
+ dailyTrends: "Tren Harian",
219
+ statusDistribution: "Distribusi Status",
220
+ topPackages: "Produk Terlaris",
221
+ recentBookings: "Pesanan Terbaru",
222
+ date: "Tanggal",
223
+ customer: "Pelanggan",
224
+ package: "Produk",
225
+ audience: "Audiens",
226
+ total: "Total",
227
+ status: "Status",
228
+ noRecentBookings: "Belum ada pesanan",
229
+ bookingsMetric: "Pesanan",
230
+ revenueMetric: "Pendapatan",
231
+ pendingMetric: "Pesanan (Pending)",
232
+ confirmedBookingMetric: "Pesanan (Confirmed)",
233
+ confirmedRevenueMetric: "Pendapatan (Confirmed)",
234
+ unknownAudience: "Unknown",
235
+ dayLabel: (n) => `${n} hari`,
236
+ formatStatusLabel: (s) => ({ confirmed: "Confirmed", pending: "Pending", cancelled: "Cancelled" })[s] || s,
237
+ formatAudienceLabel: (v) => v || "Unknown",
238
+ };
57
239
 
58
240
  export default function Dashboard() {
59
- const labels = useMemo(() => createDashboardLabels((key) => key), []);
60
-
61
241
  const state = useReusableDashboard({
62
- config: cidikaWidgetConfig,
242
+ config: tokoSepatuWidgetConfig,
63
243
  dataSource: source,
64
- adapter: adaptCidikaDashboardData,
65
- createEmptyState: createEmptyDashboardData,
244
+ adapter: adaptTokoSepatuData,
245
+ createEmptyState: createEmptyTokoSepatuData,
66
246
  languageCode: "id",
67
247
  dateLocale: "id-ID",
68
248
  labels,
@@ -70,7 +250,7 @@ export default function Dashboard() {
70
250
 
71
251
  return (
72
252
  <ReusableDashboardView
73
- config={cidikaWidgetConfig}
253
+ config={tokoSepatuWidgetConfig}
74
254
  labels={labels}
75
255
  loading={state.loading}
76
256
  error={state.error}
@@ -86,73 +266,606 @@ export default function Dashboard() {
86
266
  }
87
267
  ```
88
268
 
89
- ### 3. Jalankan
269
+ ### Step 4 — Jalankan
90
270
 
91
271
  ```bash
92
- npm start
272
+ npm start # Create React App
273
+ # atau
274
+ npm run dev # Vite
93
275
  ```
94
276
 
95
- Selesai.
277
+ Buka browser → dashboard tampil dengan data dari Supabase.
96
278
 
97
279
  ---
98
280
 
99
- ## Preset yang tersedia
100
-
101
- | Preset | Import | Tabel Supabase yang dibutuhkan |
102
- |--------|--------|-------------------------------|
103
- | **Cidika Travel** | `cidikaWidgetConfig`, `createCidikaSupabaseSource`, `adaptCidikaDashboardData` | `bookings`, `packages`, `package_locales`, `page_sections` |
104
- | **Toko Sepatu** | `tokoSepatuWidgetConfig`, `createTokoSepatuSupabaseSource`, `adaptTokoSepatuData` | `orders`, `order_items`, `products`, `customers` |
105
- | **Dummy UMKM** | `dummyUmkmWidgetConfig`, `adaptDummyUmkmData` | (gunakan salah satu source di atas) |
281
+ ## 5. Quick Start — Cidika Travel
106
282
 
107
- ---
108
-
109
- ## Pakai dengan i18n (react-i18next)
283
+ Ganti hanya 4 baris dari contoh Toko Sepatu di atas:
110
284
 
111
285
  ```jsx
112
- import { useTranslation } from "react-i18next";
113
- import { createDashboardLabels } from "@rozaqi02/reusable-dashboard";
286
+ import {
287
+ ReusableDashboardView,
288
+ cidikaWidgetConfig, // ← ganti ini
289
+ createCidikaSupabaseSource, // ← ganti ini
290
+ adaptCidikaDashboardData, // ← ganti ini
291
+ createEmptyDashboardData, // ← ganti ini
292
+ createDashboardLabels,
293
+ useReusableDashboard,
294
+ } from "@rozaqi02/reusable-dashboard";
114
295
 
296
+ // Jika pakai react-i18next:
115
297
  const { t } = useTranslation();
116
298
  const labels = useMemo(() => createDashboardLabels(t), [t]);
299
+
300
+ // Jika tidak pakai i18n, gunakan objek labels manual seperti contoh Toko Sepatu
301
+
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
+ });
313
+ ```
314
+
315
+ **Tabel Supabase yang dibutuhkan untuk Cidika Travel:**
316
+
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
117
323
  ```
118
324
 
119
325
  ---
120
326
 
121
- ## API Ringkas
327
+ ## 6. Setup Database Supabase
122
328
 
123
- ```js
124
- // Config
125
- import { cidikaWidgetConfig, tokoSepatuWidgetConfig, dummyUmkmWidgetConfig }
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)
126
340
 
127
- // Data source
128
- import { createCidikaSupabaseSource, createTokoSepatuSupabaseSource }
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:
129
345
 
130
- // Adapter
131
- import { adaptCidikaDashboardData, createEmptyDashboardData }
132
- import { adaptTokoSepatuData, createEmptyTokoSepatuData }
133
- import { adaptDummyUmkmData, createEmptyDummyUmkmData }
346
+ ```sql
347
+ -- Untuk Toko Sepatu:
348
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.orders;
349
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.products;
134
350
 
135
- // Hook
136
- import { useReusableDashboard, useRealtimeUpdate }
351
+ -- Untuk Cidika Travel:
352
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.bookings;
353
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.packages;
354
+ ```
355
+
356
+ ### Row Level Security (RLS)
357
+
358
+ Jika dashboard tidak menampilkan data padahal data sudah ada, kemungkinan
359
+ RLS (Row Level Security) memblokir query. Solusi sementara untuk development:
360
+
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;
367
+ ```
368
+
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
+ ```
376
+
377
+ ---
378
+
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
+ },
555
+
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
+ ---
569
+
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.
574
+
575
+ ```javascript
576
+ export const myConfig = {
577
+ id: "my.dashboard",
578
+
579
+ // Filter default saat pertama kali dibuka
580
+ defaultFilters: {
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"
587
+ },
588
+
589
+ widgets: {
590
+ // Stat cards (1–4 kartu)
591
+ stats: [
592
+ {
593
+ id: "totalOrders",
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"
599
+ },
600
+ {
601
+ id: "totalRevenue",
602
+ label: "confirmedRevenue",
603
+ icon: "DollarSign",
604
+ valueKey: "revenueConfirm",
605
+ format: "currency",
606
+ accentColor: "green",
607
+ },
608
+ ],
609
+
610
+ // Chart cards (1–4 chart)
611
+ charts: [
612
+ {
613
+ id: "trendHarian",
614
+ type: "dailyArea", // "dailyArea" | "statusPie" | "audiencePie" | "topPackagesBar"
615
+ label: "dailyTrends", // key dari objek labels
616
+ icon: "BarChart3",
617
+ },
618
+ {
619
+ id: "distribusiStatus",
620
+ type: "statusPie",
621
+ label: "statusDistribution",
622
+ icon: "PieChart",
623
+ },
624
+ ],
625
+
626
+ // Tabel (hanya 1)
627
+ table: {
628
+ id: "recentOrders",
629
+ label: "recentBookings", // key dari objek labels
630
+ icon: "Calendar",
631
+ emptyLabel: "noRecentBookings", // key dari objek labels, untuk empty state
632
+ columns: [
633
+ { id: "date", label: "date", accessor: "createdAt", type: "date" },
634
+ { id: "customer", label: "customer", accessor: "customerName" },
635
+ { id: "product", label: "package", accessor: "packageName" },
636
+ { id: "total", label: "total", accessor: "totalIDR", type: "currency" },
637
+ {
638
+ id: "status", label: "status", accessor: "statusLabel",
639
+ type: "statusBadge",
640
+ statusAccessor: "status", // kolom yang berisi status lowercase (confirmed/pending/cancelled)
641
+ },
642
+ ],
643
+ },
644
+ },
645
+ };
646
+ ```
647
+
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) |
655
+
656
+ **Nama ikon yang tersedia:**
657
+ `TrendingUp`, `TrendingDown`, `DollarSign`, `Users`, `PieChart`, `BarChart3`,
658
+ `Calendar`, `RotateCcw`, `Search`, `ChevronLeft`, `ChevronRight`, `ArrowUp`,
659
+ `ArrowDown`, `AlertCircle`
660
+
661
+ ---
662
+
663
+ ## 9. Label & Internasionalisasi
664
+
665
+ ### Tanpa i18n (objek manual)
666
+
667
+ Buat objek labels dengan semua key yang diperlukan:
668
+
669
+ ```javascript
670
+ const labels = {
671
+ // Header
672
+ title: "Dashboard",
673
+ refresh: "Refresh",
674
+ liveUpdate: "Live",
675
+ loadFailed: "Gagal memuat data.",
676
+ retry: "Coba Lagi",
677
+
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",
693
+
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",
700
+
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",
714
+
715
+ // Chart metrics
716
+ bookingsMetric: "Pesanan",
717
+ revenueMetric: "Pendapatan",
718
+ pendingMetric: "Pesanan (Pending)",
719
+ confirmedBookingMetric: "Pesanan (Confirmed)",
720
+ confirmedRevenueMetric: "Pendapatan (Confirmed)",
721
+ unknownAudience: "Unknown",
722
+
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
+ ```
735
+
736
+ ### Dengan react-i18next
737
+
738
+ ```jsx
739
+ import { useTranslation } from "react-i18next";
740
+ import { createDashboardLabels } from "@rozaqi02/reusable-dashboard";
741
+
742
+ function Dashboard() {
743
+ const { t, i18n } = useTranslation();
744
+ const languageCode = (i18n.language || "id").slice(0, 2);
745
+ const dateLocale = languageCode === "id" ? "id-ID" : "en-US";
746
+ const labels = useMemo(() => createDashboardLabels(t), [t]);
747
+ // ...
748
+ }
749
+ ```
750
+
751
+ > `createDashboardLabels(t)` menggunakan key i18n dari namespace default.
752
+ > Pastikan file terjemahan kamu memiliki key `admin.dashboard.*`.
137
753
 
138
- // View
139
- import { ReusableDashboardView }
754
+ ---
140
755
 
141
- // Utils
142
- import { createDashboardLabels, createDefaultFilters, resolveDateRange }
143
- import { formatIDR, formatDate, shortId, formatYYYYMMDD }
756
+ ## 10. API Reference Lengkap
757
+
758
+ ### Config
759
+
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 |
765
+
766
+ ### Data Source
767
+
768
+ | Export | Parameter | Deskripsi |
769
+ |--------|-----------|-----------|
770
+ | `createCidikaSupabaseSource(supabase)` | Supabase client | Data source Cidika Travel |
771
+ | `createTokoSepatuSupabaseSource(supabase)` | Supabase client | Data source Toko Sepatu |
772
+
773
+ ### Data Adapter
774
+
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 |
783
+
784
+ ### Hook Utama
785
+
786
+ ```typescript
787
+ const state = useReusableDashboard({
788
+ config: object, // Widget configuration
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 }
144
808
  ```
145
809
 
810
+ ### Komponen
811
+
812
+ | Komponen | Deskripsi |
813
+ |----------|-----------|
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 |
845
+
146
846
  ---
147
847
 
148
- ## Development
848
+ ## 11. Pengembangan & Kontribusi
149
849
 
150
850
  ```bash
151
- git clone <repo>
851
+ # Clone repository
852
+ git clone https://github.com/rozaqi02/reusable-dashboard-umkm.git
152
853
  cd reusable-dashboard-umkm
854
+
855
+ # Install dependencies
153
856
  npm install
154
- npm run build # output ke dist/
155
- npm test
857
+
858
+ # Build
859
+ npm run build # Output ke dist/
860
+
861
+ # Test
862
+ npm test # Jest + React Testing Library
863
+ npm test -- --coverage # Lihat code coverage
864
+
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
156
869
  ```
157
870
 
158
871
  ---
@@ -160,3 +873,7 @@ npm test
160
873
  ## License
161
874
 
162
875
  MIT — Ahmad Abror Rozaqi Fatoni
876
+
877
+ ---
878
+
879
+ **Made with ❤️ for UMKM Indonesia**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rozaqi02/reusable-dashboard",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Reusable dashboard module for admin web apps (UMKM).",
5
5
  "type": "module",
6
6
  "private": false,