@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.
- package/README.md +778 -61
- 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
|
|
4
|
-
Mendukung berbagai domain bisnis tanpa
|
|
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
|
-
[](./CHANGELOG.md)
|
|
7
8
|
[](./LICENSE)
|
|
9
|
+
[](https://www.npmjs.com/package/@rozaqi02/reusable-dashboard)
|
|
8
10
|
|
|
9
11
|
---
|
|
10
12
|
|
|
11
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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://
|
|
38
|
-
REACT_APP_SUPABASE_ANON_KEY=
|
|
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
|
-
|
|
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 "
|
|
178
|
+
import { supabase } from "../lib/supabaseClient";
|
|
46
179
|
import {
|
|
47
180
|
ReusableDashboardView,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
createDashboardLabels,
|
|
181
|
+
tokoSepatuWidgetConfig,
|
|
182
|
+
createTokoSepatuSupabaseSource,
|
|
183
|
+
adaptTokoSepatuData,
|
|
184
|
+
createEmptyTokoSepatuData,
|
|
53
185
|
useReusableDashboard,
|
|
54
186
|
} from "@rozaqi02/reusable-dashboard";
|
|
55
187
|
|
|
56
|
-
|
|
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:
|
|
242
|
+
config: tokoSepatuWidgetConfig,
|
|
63
243
|
dataSource: source,
|
|
64
|
-
adapter:
|
|
65
|
-
createEmptyState:
|
|
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={
|
|
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
|
-
###
|
|
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
|
-
|
|
277
|
+
Buka browser → dashboard tampil dengan data dari Supabase.
|
|
96
278
|
|
|
97
279
|
---
|
|
98
280
|
|
|
99
|
-
##
|
|
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 {
|
|
113
|
-
|
|
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
|
-
##
|
|
327
|
+
## 6. Setup Database Supabase
|
|
122
328
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
import { ReusableDashboardView }
|
|
754
|
+
---
|
|
140
755
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
##
|
|
848
|
+
## 11. Pengembangan & Kontribusi
|
|
149
849
|
|
|
150
850
|
```bash
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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**
|