@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 CHANGED
@@ -1,85 +1,181 @@
1
1
  # @rozaqi02/reusable-dashboard
2
2
 
3
- Modul dashboard admin reusable untuk UMKM berbasis React + Atomic Design + Supabase.
4
- Mendukung berbagai domain bisnis (travel, toko online, dll.) tanpa mengubah komponen inti —
5
- cukup definisikan widget configuration dan data adapter yang sesuai.
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
- [![version](https://img.shields.io/badge/version-1.0.0-blue)](./CHANGELOG.md)
8
- [![license](https://img.shields.io/badge/license-Proprietary-red)](./LICENSE)
9
- [![build](https://img.shields.io/badge/build-passing-brightgreen)](#)
7
+ [![version](https://img.shields.io/badge/version-1.1.1-blue)](./CHANGELOG.md)
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)
10
10
 
11
11
  ---
12
12
 
13
- ## 📦 Installation
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
- ## Prerequisites
36
+ > **Catatan:** CSS sudah otomatis terbundle di dalam paket ini (`dist/index.css`).
37
+ > Tidak perlu menginstall atau mengkonfigurasi Tailwind CSS.
20
38
 
21
- - **Node.js** >= 16.x
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
- ## 🔧 Peer Dependencies
41
+ ## 2. Prasyarat Teknis
27
42
 
28
- Install peer dependencies yang dibutuhkan:
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
- ```bash
31
- npm install react react-dom recharts @supabase/supabase-js lucide-react prop-types tailwindcss
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
- > **Tailwind CSS wajib** karena komponen menggunakan utility classes Tailwind untuk styling.
35
- > Pastikan Tailwind sudah dikonfigurasi di project kamu (lihat [panduan Tailwind](https://tailwindcss.com/docs/installation)).
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
- ## 🚀 Quick Start
70
+ ## 4. Quick Start — Toko Sepatu
40
71
 
41
- ### Step 1: Setup Supabase Client
72
+ Ini adalah contoh paling lengkap. Ikuti step by step dari nol.
42
73
 
43
- Buat file `src/lib/supabaseClient.js`:
74
+ ### Step 1 — Setup Supabase
44
75
 
45
- ```javascript
46
- import { createClient } from '@supabase/supabase-js';
76
+ **A. Buat project Supabase** di [app.supabase.com](https://app.supabase.com)
47
77
 
48
- export const supabase = createClient(
49
- import.meta.env.VITE_SUPABASE_URL, // Vite
50
- import.meta.env.VITE_SUPABASE_ANON_KEY // Vite
51
- // atau process.env.REACT_APP_SUPABASE_URL jika pakai Create React App
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
- Buat file `.env` di root project:
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
- ```env
58
- # Vite
59
- VITE_SUPABASE_URL=https://your-project.supabase.co
60
- VITE_SUPABASE_ANON_KEY=your-anon-key
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
- # Create React App
63
- # REACT_APP_SUPABASE_URL=https://your-project.supabase.co
64
- # REACT_APP_SUPABASE_ANON_KEY=your-anon-key
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
- ### Step 2: Setup Database Schema
139
+ **C. Isi data contoh** (opsional, untuk testing):
68
140
 
69
- Jalankan SQL schema di Supabase SQL Editor sesuai domain bisnis:
141
+ File seed data lengkap tersedia di:
142
+ `examples/toko-sepatu-example/seed-data.sql` dalam repositori ini.
70
143
 
71
- | Domain | File SQL |
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 3: Implementasi Dashboard
147
+ ### Step 2 Buat Supabase client
77
148
 
78
- Contoh menggunakan preset **Toko Sepatu**:
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 "./lib/supabaseClient";
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 satu kali di luar komponen
188
+ // Inisialisasi data source dibuat sekali di luar komponen
93
189
  const source = createTokoSepatuSupabaseSource(supabase);
94
190
 
95
- // Label minimal tanpa i18n — override semua key yang diperlukan
191
+ // Label UI dalam Bahasa Indonesia
192
+ // Ganti nilai-nilai ini sesuai bahasa/konteks bisnis kamu
96
193
  const labels = {
97
- title: "Dashboard Toko Sepatu",
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: (count) => `${count} hari`,
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
- > **Menggunakan react-i18next?** Gunakan `createDashboardLabels(t)` dari export modul
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 run dev # Vite
272
+ npm start # Create React App
176
273
  # atau
177
- npm start # Create React App
274
+ npm run dev # Vite
178
275
  ```
179
276
 
277
+ Buka browser → dashboard tampil dengan data dari Supabase.
278
+
180
279
  ---
181
280
 
182
- ## 📚 Available Presets
281
+ ## 5. Quick Start — Cidika Travel
183
282
 
184
- ### 1. Cidika Travel (Travel Agency)
283
+ Ganti hanya 4 baris dari contoh Toko Sepatu di atas:
185
284
 
186
- ```javascript
285
+ ```jsx
187
286
  import {
188
- cidikaWidgetConfig,
189
- createCidikaSupabaseSource,
190
- adaptCidikaDashboardData,
191
- createEmptyDashboardData,
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: `bookings`, `packages`, `package_locales`, `page_sections`
315
+ **Tabel Supabase yang dibutuhkan untuk Cidika Travel:**
198
316
 
199
- ### 2. Toko Sepatu (Online Shop)
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
- ```javascript
202
- import {
203
- tokoSepatuWidgetConfig,
204
- createTokoSepatuSupabaseSource,
205
- adaptTokoSepatuData,
206
- createEmptyTokoSepatuData,
207
- } from "@rozaqi02/reusable-dashboard";
325
+ ---
208
326
 
209
- const source = createTokoSepatuSupabaseSource(supabase);
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
- Tabel Supabase yang dibutuhkan: `orders`, `order_items`, `products`, `customers`
356
+ ### Row Level Security (RLS)
213
357
 
214
- ### 3. Dummy UMKM (Generic)
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
- ```javascript
217
- import {
218
- dummyUmkmWidgetConfig,
219
- adaptDummyUmkmData,
220
- createEmptyDummyUmkmData,
221
- } from "@rozaqi02/reusable-dashboard";
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
- > Dummy UMKM tidak memiliki data source khusus.
225
- > Gunakan data source Toko Sepatu atau buat sendiri.
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
- ## 🎨 Customization
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
- ### Custom Widget Configuration
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
- Struktur konfigurasi yang benar (sesuai kontrak API modul):
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-business.dashboard",
577
+ id: "my.dashboard",
578
+
579
+ // Filter default saat pertama kali dibuka
238
580
  defaultFilters: {
239
- statusScope: "all", // "confirmed" | "pending" | "all"
240
- daysPreset: 30, // preset hari (7 | 30 | 90 | 0 untuk kustom)
241
- sortPkgBy: "revenue", // "bookings" | "revenue"
242
- sortPkgDir: "desc", // "asc" | "desc"
243
- includePendingOverlay: false,
244
- audience: "",
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", // key pada objek labels
251
- icon: "TrendingUp", // nama ikon dari Lucide React
252
- valueKey: "bookingsConfirm", // key pada data.stats dari adapter
253
- format: "number", // "number" | "currency" | "percent"
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: "dailyTrends",
266
- type: "dailyArea", // "dailyArea" | "statusPie" | "audiencePie" | "topPackagesBar"
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: "statusDist",
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", statusAccessor: "status",
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
- ### Custom Data Source
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
- Kontrak yang harus diimplementasikan:
299
-
300
- ```javascript
301
- export function createMySupabaseSource(supabase) {
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
- if (error) throw new Error("Failed to load orders.");
312
- return { bookings: orders, recent: [], packageLocales: [], staticCounts: {} };
313
- },
661
+ ---
314
662
 
315
- // Opsional: subscribe ke perubahan realtime (untuk live update)
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
- ### Custom Data Adapter
665
+ ### Tanpa i18n (objek manual)
328
666
 
329
- Adapter menerima parameter objek, bukan positional argument:
667
+ Buat objek labels dengan semua key yang diperlukan:
330
668
 
331
669
  ```javascript
332
- export function adaptMyBusinessData({ raw, filters, range, dateLocale, languageCode, labels }) {
333
- if (!raw) return createMyEmptyData();
334
-
335
- const orders = raw.bookings || [];
336
- let totalConfirmed = 0;
337
- let totalRevenue = 0;
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
- export function createMyEmptyData() {
366
- return {
367
- stats: { bookingsConfirm: 0, revenueConfirm: 0, avgRevenue: 0,
368
- conversionRate: 0, packages: 0, sections: 0 },
369
- charts: { dailyTrends: [], statusDistribution: [],
370
- audienceDistribution: [], topPackages: [] },
371
- table: { recentBookings: [] },
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
- ## 🌐 Label & i18n
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
- ### Tanpa i18n (plain object)
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
- Buat objek label secara manual (lihat contoh di Quick Start).
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
- ```javascript
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
- ## 📖 API Reference
756
+ ## 10. API Reference Lengkap
404
757
 
405
- ### Configs
758
+ ### Config
406
759
 
407
- | Export | Deskripsi |
408
- |--------|-----------|
409
- | `cidikaWidgetConfig` | Config dashboard travel agency |
410
- | `tokoSepatuWidgetConfig` | Config dashboard toko online |
411
- | `dummyUmkmWidgetConfig` | Config dashboard bisnis generik |
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 Sources
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 Adapters
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
- ### Hooks
432
-
433
- | Hook | Deskripsi |
434
- |------|-----------|
435
- | `useReusableDashboard(options)` | Hook utama orkestrasi dashboard |
436
- | `useRealtimeUpdate(options)` | Hook subscription Supabase Realtime |
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
- **useReusableDashboard options:**
784
+ ### Hook Utama
439
785
 
440
786
  ```typescript
441
- {
787
+ const state = useReusableDashboard({
442
788
  config: object, // Widget configuration
443
- dataSource: object, // Objek dengan fetchDashboardSnapshot + subscribeLiveUpdate
444
- adapter: Function, // ({ raw, filters, range, dateLocale, languageCode, labels }) => data
445
- createEmptyState: Function, // () => data kosong
446
- languageCode: string, // "id" | "en"
447
- dateLocale: string, // "id-ID" | "en-US"
448
- labels: object, // Objek label teks UI
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
- **Return value useReusableDashboard:**
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
- | `DashboardLayout` | Layout dengan slot sidebar dan main content |
502
- | `ReusableDashboardView` | Halaman dashboard lengkap (gunakan ini untuk integrasi cepat) |
503
-
504
- ### Utility Functions
505
-
506
- | Function | Signature | Deskripsi |
507
- |----------|-----------|-----------|
508
- | `createDashboardLabels(t)` | `t: Function` | Buat objek label dari fungsi i18n |
509
- | `createDefaultFilters(base?)` | `base?: object` | Buat filter default |
510
- | `resolveDateRange({ daysPreset, dateFrom, dateTo })` | | Resolve range ke `{ fromISO, toISO, daysWindow }` |
511
- | `formatYYYYMMDD(date)` | `date: Date` | Format tanggal ke `YYYY-MM-DD` |
512
- | `formatIDR(value)` | `value: number` | Format angka ke Rupiah (`1.500.000`) |
513
- | `formatDate(value, locale)` | `value: string, locale: string` | Format tanggal dengan locale |
514
- | `shortId(value, length?)` | `value: string, length?: number` | Potong string (default 8 karakter) |
515
- | `toNumber(value)` | `value: any` | Konversi ke angka finite, fallback 0 |
516
- | `buildDayBuckets(daysWindow, fromISO, dateLocale)` | | Bangun array bucket harian untuk chart |
517
- | `sortMapEntries(map, direction)` | `direction: "asc"\|"desc"` | Urutkan entri Map berdasarkan nilai |
518
- | `resolveIcon(name)` | `name: string` | Resolve nama string ke komponen Lucide |
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
- ## 🛠️ Development
848
+ ## 11. Pengembangan & Kontribusi
523
849
 
524
850
  ```bash
525
- # Clone & install
526
- git clone <repo-url>
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 production
531
- npm run build
858
+ # Build
859
+ npm run build # Output ke dist/
532
860
 
533
- # Run tests
534
- npm test
861
+ # Test
862
+ npm test # Jest + React Testing Library
863
+ npm test -- --coverage # Lihat code coverage
535
864
 
536
- # Test via npm link
537
- npm link
538
- cd /path/to/your-project
539
- npm link @rozaqi02/reusable-dashboard
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
- ## 📞 Support
873
+ ## License
566
874
 
567
- - Email: abrorrozaqi@gmail.com
875
+ MIT Ahmad Abror Rozaqi Fatoni
568
876
 
569
877
  ---
570
878