@rozaqi02/reusable-dashboard 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1101 -58
- package/dist/index.cjs +437 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +284 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +434 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,68 +1,283 @@
|
|
|
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
|
+
### Import CSS
|
|
37
|
+
|
|
38
|
+
CSS modul harus di-import secara eksplisit di file CSS utama project kamu.
|
|
39
|
+
|
|
40
|
+
**Create React App** — tambahkan di `src/index.css`:
|
|
41
|
+
```css
|
|
42
|
+
@import "../node_modules/@rozaqi02/reusable-dashboard/dist/index.css";
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Vite** — tambahkan di `src/index.css` atau `src/main.jsx`:
|
|
46
|
+
```css
|
|
47
|
+
/* index.css */
|
|
48
|
+
@import "@rozaqi02/reusable-dashboard/dist/index.css";
|
|
49
|
+
```
|
|
50
|
+
```jsx
|
|
51
|
+
// atau di main.jsx
|
|
52
|
+
import "@rozaqi02/reusable-dashboard/dist/index.css";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Next.js** — tambahkan di `app/globals.css` atau `pages/_app.jsx`:
|
|
56
|
+
```css
|
|
57
|
+
@import "@rozaqi02/reusable-dashboard/dist/index.css";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 2. Prasyarat Teknis
|
|
63
|
+
|
|
64
|
+
| Prasyarat | Versi Minimum | Keterangan |
|
|
65
|
+
|-----------|---------------|------------|
|
|
66
|
+
| Node.js | 18.x | Runtime JavaScript (18 atau lebih baru) |
|
|
67
|
+
| React | 18.0.0 | UI framework (mendukung React 19) |
|
|
68
|
+
| recharts | 2.0.0 | Library chart (AreaChart, PieChart, BarChart) |
|
|
69
|
+
| @supabase/supabase-js | 2.0.0 | Client Supabase untuk koneksi database |
|
|
70
|
+
| Akun Supabase | — | Gratis di [supabase.com](https://supabase.com) |
|
|
71
|
+
|
|
72
|
+
Modul ini dirancang khusus untuk aplikasi React yang menggunakan Supabase sebagai backend.
|
|
73
|
+
Belum mendukung backend lain (Firebase, REST API, GraphQL) tanpa menulis data source sendiri.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 3. Preset yang Tersedia
|
|
78
|
+
|
|
79
|
+
Modul menyediakan **3 preset siap pakai** untuk domain bisnis yang berbeda:
|
|
80
|
+
|
|
81
|
+
| Preset | Domain | Tabel Supabase yang Dibutuhkan | Siap Pakai? |
|
|
82
|
+
|--------|--------|-------------------------------|-------------|
|
|
83
|
+
| **Cidika Travel** | Travel agency / paket wisata | `bookings`, `packages`, `package_locales`, `page_sections` | Config + Adapter + Source |
|
|
84
|
+
| **Toko Sepatu** | Toko online / e-commerce | `orders`, `order_items`, `products`, `customers` | Config + Adapter + Source |
|
|
85
|
+
| **Dummy UMKM** | UMKM generik (contoh domain baru) | `orders`, `products`, `customers` | Config + Adapter saja* |
|
|
86
|
+
|
|
87
|
+
> *Preset **Dummy UMKM** menyediakan widget config (`dummyUmkmWidgetConfig`) dan
|
|
88
|
+
> adapter (`adaptDummyUmkmData`), tetapi **tidak** menyertakan data source siap pakai.
|
|
89
|
+
> Data source-nya ditulis manual — lihat contoh di
|
|
90
|
+
> `examples/dummy-umkm-example/DummyUmkmDashboard.jsx` (repo GitHub).
|
|
91
|
+
> Ini sengaja dijadikan **template** untuk kamu yang ingin membuat domain baru
|
|
92
|
+
> (lihat [Bab 7](#7-cara-membuat-adapter-untuk-domain-bisnis-baru)).
|
|
93
|
+
|
|
94
|
+
Hanya **2 data source siap pakai** yang diekspor modul:
|
|
95
|
+
`createCidikaSupabaseSource` dan `createTokoSepatuSupabaseSource`.
|
|
96
|
+
|
|
97
|
+
Setiap preset siap pakai sudah termasuk: konfigurasi widget, data adapter, dan data source Supabase.
|
|
19
98
|
|
|
20
99
|
---
|
|
21
100
|
|
|
22
|
-
## Quick Start
|
|
101
|
+
## 4. Quick Start — Toko Sepatu
|
|
102
|
+
|
|
103
|
+
Ini adalah contoh paling lengkap. Ikuti step by step dari nol.
|
|
104
|
+
|
|
105
|
+
### Step 1 — Setup Supabase
|
|
106
|
+
|
|
107
|
+
**A. Buat project Supabase** di [app.supabase.com](https://app.supabase.com)
|
|
108
|
+
|
|
109
|
+
**B. Jalankan SQL schema** berikut di Supabase SQL Editor
|
|
110
|
+
(Settings → SQL Editor → New Query):
|
|
111
|
+
|
|
112
|
+
```sql
|
|
113
|
+
-- Tabel pelanggan
|
|
114
|
+
CREATE TABLE public.customers (
|
|
115
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
116
|
+
name text NOT NULL,
|
|
117
|
+
email text,
|
|
118
|
+
phone text,
|
|
119
|
+
city text,
|
|
120
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
121
|
+
CONSTRAINT customers_pkey PRIMARY KEY (id)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
-- Tabel produk
|
|
125
|
+
CREATE TABLE public.products (
|
|
126
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
127
|
+
name text NOT NULL,
|
|
128
|
+
brand text NOT NULL DEFAULT 'Generic',
|
|
129
|
+
category text NOT NULL DEFAULT 'sneakers',
|
|
130
|
+
price_idr integer NOT NULL,
|
|
131
|
+
stock integer NOT NULL DEFAULT 0,
|
|
132
|
+
is_active boolean NOT NULL DEFAULT true,
|
|
133
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
134
|
+
CONSTRAINT products_pkey PRIMARY KEY (id)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
-- Tabel pesanan
|
|
138
|
+
CREATE TABLE public.orders (
|
|
139
|
+
id uuid NOT NULL DEFAULT gen_random_uuid(),
|
|
140
|
+
customer_id uuid NOT NULL,
|
|
141
|
+
total_amount integer NOT NULL,
|
|
142
|
+
status text NOT NULL DEFAULT 'pending',
|
|
143
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
144
|
+
CONSTRAINT orders_pkey PRIMARY KEY (id),
|
|
145
|
+
CONSTRAINT orders_customer_id_fkey FOREIGN KEY (customer_id)
|
|
146
|
+
REFERENCES public.customers(id)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
-- Tabel detail item pesanan
|
|
150
|
+
CREATE TABLE public.order_items (
|
|
151
|
+
id bigint GENERATED ALWAYS AS IDENTITY,
|
|
152
|
+
order_id uuid NOT NULL,
|
|
153
|
+
product_id uuid NOT NULL,
|
|
154
|
+
qty integer NOT NULL DEFAULT 1,
|
|
155
|
+
price_idr integer NOT NULL,
|
|
156
|
+
subtotal integer NOT NULL,
|
|
157
|
+
CONSTRAINT order_items_pkey PRIMARY KEY (id),
|
|
158
|
+
CONSTRAINT order_items_order_id_fkey FOREIGN KEY (order_id)
|
|
159
|
+
REFERENCES public.orders(id),
|
|
160
|
+
CONSTRAINT order_items_product_id_fkey FOREIGN KEY (product_id)
|
|
161
|
+
REFERENCES public.products(id)
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
-- Aktifkan Realtime (untuk fitur live update)
|
|
165
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.orders;
|
|
166
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.products;
|
|
167
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.customers;
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**C. Isi data contoh** (opsional, untuk testing):
|
|
171
|
+
|
|
172
|
+
File seed data lengkap tersedia di repo GitHub:
|
|
173
|
+
`examples/toko-sepatu-example/seed-data.sql`.
|
|
174
|
+
|
|
175
|
+
> Catatan: folder `examples/` **tidak ikut** saat `npm install` (paket npm hanya
|
|
176
|
+
> berisi `dist/`). Untuk mengambil file SQL contoh, kloning repo GitHub atau
|
|
177
|
+
> salin manual dari halaman repositori.
|
|
178
|
+
|
|
179
|
+
**D. Aktifkan Realtime** di Supabase Dashboard:
|
|
180
|
+
Database → Replication → centang tabel `orders`, `products`, `customers`.
|
|
23
181
|
|
|
24
|
-
###
|
|
182
|
+
### Step 2 — Buat Supabase client
|
|
25
183
|
|
|
26
184
|
```js
|
|
27
185
|
// src/lib/supabaseClient.js
|
|
28
186
|
import { createClient } from '@supabase/supabase-js';
|
|
187
|
+
|
|
29
188
|
export const supabase = createClient(
|
|
30
|
-
process.env.REACT_APP_SUPABASE_URL,
|
|
189
|
+
process.env.REACT_APP_SUPABASE_URL, // CRA
|
|
31
190
|
process.env.REACT_APP_SUPABASE_ANON_KEY
|
|
191
|
+
// Jika pakai Vite: import.meta.env.VITE_SUPABASE_URL
|
|
32
192
|
);
|
|
33
193
|
```
|
|
34
194
|
|
|
35
195
|
```env
|
|
36
|
-
# .env
|
|
37
|
-
REACT_APP_SUPABASE_URL=https://
|
|
38
|
-
REACT_APP_SUPABASE_ANON_KEY=
|
|
196
|
+
# .env (di root project)
|
|
197
|
+
REACT_APP_SUPABASE_URL=https://xxxxxx.supabase.co
|
|
198
|
+
REACT_APP_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
199
|
+
|
|
200
|
+
# Jika pakai Vite:
|
|
201
|
+
# VITE_SUPABASE_URL=https://xxxxxx.supabase.co
|
|
202
|
+
# VITE_SUPABASE_ANON_KEY=eyJ...
|
|
39
203
|
```
|
|
40
204
|
|
|
41
|
-
|
|
205
|
+
Nilai URL dan ANON KEY didapat dari Supabase Dashboard:
|
|
206
|
+
**Settings → API → Project URL & anon key**.
|
|
207
|
+
|
|
208
|
+
### Step 3 — Implementasi halaman Dashboard
|
|
42
209
|
|
|
43
210
|
```jsx
|
|
211
|
+
// src/pages/Dashboard.jsx
|
|
44
212
|
import React, { useMemo } from "react";
|
|
45
|
-
import { supabase } from "
|
|
213
|
+
import { supabase } from "../lib/supabaseClient";
|
|
46
214
|
import {
|
|
47
215
|
ReusableDashboardView,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
createDashboardLabels,
|
|
216
|
+
tokoSepatuWidgetConfig,
|
|
217
|
+
createTokoSepatuSupabaseSource,
|
|
218
|
+
adaptTokoSepatuData,
|
|
219
|
+
createEmptyTokoSepatuData,
|
|
53
220
|
useReusableDashboard,
|
|
54
221
|
} from "@rozaqi02/reusable-dashboard";
|
|
55
222
|
|
|
56
|
-
|
|
223
|
+
// Inisialisasi data source — dibuat sekali di luar komponen
|
|
224
|
+
const source = createTokoSepatuSupabaseSource(supabase);
|
|
57
225
|
|
|
58
|
-
|
|
59
|
-
|
|
226
|
+
// Label UI dalam Bahasa Indonesia
|
|
227
|
+
// Ganti nilai-nilai ini sesuai bahasa/konteks bisnis kamu
|
|
228
|
+
const labels = {
|
|
229
|
+
title: "Dashboard Toko Saya",
|
|
230
|
+
refresh: "Refresh",
|
|
231
|
+
liveUpdate: "Live",
|
|
232
|
+
loadFailed: "Gagal memuat data.",
|
|
233
|
+
retry: "Coba Lagi",
|
|
234
|
+
confirmedOnly: "Confirmed",
|
|
235
|
+
pendingOnly: "Pending",
|
|
236
|
+
allStatus: "Semua Status",
|
|
237
|
+
showPendingOverlay: "Tampilkan pending",
|
|
238
|
+
allAudience: "Semua",
|
|
239
|
+
audienceDomestic: "Domestic",
|
|
240
|
+
audienceForeign: "Foreign",
|
|
241
|
+
customDate: "Kustom",
|
|
242
|
+
reset: "Reset",
|
|
243
|
+
topSort: "Urutkan",
|
|
244
|
+
sortBookings: "Qty Terjual",
|
|
245
|
+
sortRevenue: "Revenue",
|
|
246
|
+
sortDesc: "Turun",
|
|
247
|
+
sortAsc: "Naik",
|
|
248
|
+
confirmedBookings: "Total Pesanan",
|
|
249
|
+
confirmedRevenue: "Total Pendapatan",
|
|
250
|
+
avgRevenue: "Rata-rata / Pesanan",
|
|
251
|
+
conversionRate: "Conversion Rate",
|
|
252
|
+
totalProducts: "Total Produk",
|
|
253
|
+
dailyTrends: "Tren Harian",
|
|
254
|
+
statusDistribution: "Distribusi Status",
|
|
255
|
+
topPackages: "Produk Terlaris",
|
|
256
|
+
recentBookings: "Pesanan Terbaru",
|
|
257
|
+
date: "Tanggal",
|
|
258
|
+
customer: "Pelanggan",
|
|
259
|
+
package: "Produk",
|
|
260
|
+
audience: "Audiens",
|
|
261
|
+
total: "Total",
|
|
262
|
+
status: "Status",
|
|
263
|
+
noRecentBookings: "Belum ada pesanan",
|
|
264
|
+
bookingsMetric: "Pesanan",
|
|
265
|
+
revenueMetric: "Pendapatan",
|
|
266
|
+
pendingMetric: "Pesanan (Pending)",
|
|
267
|
+
confirmedBookingMetric: "Pesanan (Confirmed)",
|
|
268
|
+
confirmedRevenueMetric: "Pendapatan (Confirmed)",
|
|
269
|
+
unknownAudience: "Unknown",
|
|
270
|
+
dayLabel: (n) => `${n} hari`,
|
|
271
|
+
formatStatusLabel: (s) => ({ confirmed: "Confirmed", pending: "Pending", cancelled: "Cancelled" })[s] || s,
|
|
272
|
+
formatAudienceLabel: (v) => v || "Unknown",
|
|
273
|
+
};
|
|
60
274
|
|
|
275
|
+
export default function Dashboard() {
|
|
61
276
|
const state = useReusableDashboard({
|
|
62
|
-
config:
|
|
277
|
+
config: tokoSepatuWidgetConfig,
|
|
63
278
|
dataSource: source,
|
|
64
|
-
adapter:
|
|
65
|
-
createEmptyState:
|
|
279
|
+
adapter: adaptTokoSepatuData,
|
|
280
|
+
createEmptyState: createEmptyTokoSepatuData,
|
|
66
281
|
languageCode: "id",
|
|
67
282
|
dateLocale: "id-ID",
|
|
68
283
|
labels,
|
|
@@ -70,7 +285,7 @@ export default function Dashboard() {
|
|
|
70
285
|
|
|
71
286
|
return (
|
|
72
287
|
<ReusableDashboardView
|
|
73
|
-
config={
|
|
288
|
+
config={tokoSepatuWidgetConfig}
|
|
74
289
|
labels={labels}
|
|
75
290
|
loading={state.loading}
|
|
76
291
|
error={state.error}
|
|
@@ -86,77 +301,905 @@ export default function Dashboard() {
|
|
|
86
301
|
}
|
|
87
302
|
```
|
|
88
303
|
|
|
89
|
-
###
|
|
304
|
+
### Step 4 — Jalankan
|
|
90
305
|
|
|
91
306
|
```bash
|
|
92
|
-
npm start
|
|
307
|
+
npm start # Create React App
|
|
308
|
+
# atau
|
|
309
|
+
npm run dev # Vite
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Buka browser → dashboard tampil dengan data dari Supabase.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 5. Quick Start — Cidika Travel
|
|
317
|
+
|
|
318
|
+
Ganti hanya 4 baris dari contoh Toko Sepatu di atas:
|
|
319
|
+
|
|
320
|
+
```jsx
|
|
321
|
+
import {
|
|
322
|
+
ReusableDashboardView,
|
|
323
|
+
cidikaWidgetConfig, // ← ganti ini
|
|
324
|
+
createCidikaSupabaseSource, // ← ganti ini
|
|
325
|
+
adaptCidikaDashboardData, // ← ganti ini
|
|
326
|
+
createEmptyDashboardData, // ← ganti ini
|
|
327
|
+
createDashboardLabels,
|
|
328
|
+
useReusableDashboard,
|
|
329
|
+
} from "@rozaqi02/reusable-dashboard";
|
|
330
|
+
|
|
331
|
+
// Jika pakai react-i18next:
|
|
332
|
+
const { t } = useTranslation();
|
|
333
|
+
const labels = useMemo(() => createDashboardLabels(t), [t]);
|
|
334
|
+
|
|
335
|
+
// Jika tidak pakai i18n, gunakan objek labels manual seperti contoh Toko Sepatu
|
|
336
|
+
|
|
337
|
+
const source = createCidikaSupabaseSource(supabase);
|
|
338
|
+
|
|
339
|
+
const state = useReusableDashboard({
|
|
340
|
+
config: cidikaWidgetConfig,
|
|
341
|
+
dataSource: source,
|
|
342
|
+
adapter: adaptCidikaDashboardData,
|
|
343
|
+
createEmptyState: createEmptyDashboardData,
|
|
344
|
+
languageCode: "id",
|
|
345
|
+
dateLocale: "id-ID",
|
|
346
|
+
labels,
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Tabel Supabase yang dibutuhkan untuk Cidika Travel:**
|
|
351
|
+
|
|
352
|
+
```sql
|
|
353
|
+
-- Tabel utama
|
|
354
|
+
bookings (id, created_at, total_idr, status, package_id, audience, customer_name)
|
|
355
|
+
packages (id, slug, price, is_active)
|
|
356
|
+
package_locales (package_id, lang, title) -- untuk nama paket multi-bahasa
|
|
357
|
+
page_sections (id, page_id, type, order) -- untuk menghitung jumlah section
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 6. Setup Database Supabase
|
|
363
|
+
|
|
364
|
+
### Cara mendapatkan URL dan API Key
|
|
365
|
+
|
|
366
|
+
1. Login ke [app.supabase.com](https://app.supabase.com)
|
|
367
|
+
2. Buka project kamu
|
|
368
|
+
3. Klik **Settings** (ikon gear) di sidebar kiri
|
|
369
|
+
4. Klik **API**
|
|
370
|
+
5. Salin:
|
|
371
|
+
- **Project URL** → `REACT_APP_SUPABASE_URL`
|
|
372
|
+
- **anon public** key → `REACT_APP_SUPABASE_ANON_KEY`
|
|
373
|
+
|
|
374
|
+
### Mengaktifkan Realtime (wajib untuk live update)
|
|
375
|
+
|
|
376
|
+
1. Di Supabase Dashboard, klik **Database** di sidebar
|
|
377
|
+
2. Klik **Replication**
|
|
378
|
+
3. Di bagian **Source**, aktifkan toggle untuk tabel yang ingin di-listen
|
|
379
|
+
4. Atau jalankan SQL berikut:
|
|
380
|
+
|
|
381
|
+
```sql
|
|
382
|
+
-- Untuk Toko Sepatu:
|
|
383
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.orders;
|
|
384
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.products;
|
|
385
|
+
|
|
386
|
+
-- Untuk Cidika Travel:
|
|
387
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.bookings;
|
|
388
|
+
ALTER PUBLICATION supabase_realtime ADD TABLE public.packages;
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Row Level Security (RLS)
|
|
392
|
+
|
|
393
|
+
Jika dashboard tidak menampilkan data padahal data sudah ada, kemungkinan
|
|
394
|
+
RLS (Row Level Security) memblokir query. Solusi sementara untuk development:
|
|
395
|
+
|
|
396
|
+
```sql
|
|
397
|
+
-- PERHATIAN: Hanya untuk development. Jangan gunakan di production
|
|
398
|
+
-- tanpa menambahkan policy yang benar.
|
|
399
|
+
ALTER TABLE public.orders DISABLE ROW LEVEL SECURITY;
|
|
400
|
+
ALTER TABLE public.customers DISABLE ROW LEVEL SECURITY;
|
|
401
|
+
ALTER TABLE public.products DISABLE ROW LEVEL SECURITY;
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Untuk production, tambahkan policy yang sesuai:
|
|
405
|
+
|
|
406
|
+
```sql
|
|
407
|
+
-- Contoh policy: izinkan semua operasi SELECT untuk role anon
|
|
408
|
+
CREATE POLICY "Allow read for anon" ON public.orders
|
|
409
|
+
FOR SELECT TO anon USING (true);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## 7. Cara Membuat Adapter untuk Domain Bisnis Baru
|
|
415
|
+
|
|
416
|
+
Bab ini adalah panduan **dari nol** untuk client yang domainnya **bukan** travel
|
|
417
|
+
atau toko sepatu (misal: laundry, klinik, rental mobil, kursus, dll). Kamu hanya
|
|
418
|
+
perlu menulis **3 file kecil**, tanpa pernah menyentuh kode inti modul.
|
|
419
|
+
|
|
420
|
+
### 7.0 Pahami dulu alurnya (WAJIB dibaca)
|
|
421
|
+
|
|
422
|
+
Modul ini memisahkan 3 tanggung jawab. Memahami peran masing-masing membuat
|
|
423
|
+
sisanya gampang:
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
DATABASE Supabase
|
|
427
|
+
│
|
|
428
|
+
▼
|
|
429
|
+
┌───────────────┐ "Ambil data mentah dari Supabase"
|
|
430
|
+
│ 1. DATA SOURCE │ → tahu nama tabel & kolom kamu
|
|
431
|
+
└───────────────┘ → output: objek { bookings, recent, staticCounts }
|
|
432
|
+
│
|
|
433
|
+
▼
|
|
434
|
+
┌───────────────┐ "Ubah data mentah → format standar dashboard"
|
|
435
|
+
│ 2. ADAPTER │ → hitung stats, susun data chart & tabel
|
|
436
|
+
└───────────────┘ → output: objek { stats, charts, table }
|
|
437
|
+
│
|
|
438
|
+
▼
|
|
439
|
+
┌───────────────┐ "Tentukan WIDGET apa yang tampil & labelnya"
|
|
440
|
+
│ 3. WIDGET │ → kartu mana, chart mana, kolom tabel mana
|
|
441
|
+
│ CONFIG │
|
|
442
|
+
└───────────────┘
|
|
443
|
+
│
|
|
444
|
+
▼
|
|
445
|
+
useReusableDashboard(...) → <ReusableDashboardView /> (UI jadi)
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Aturan emas:
|
|
449
|
+
- **Data Source** = satu-satunya bagian yang tahu nama tabel/kolom database kamu.
|
|
450
|
+
- **Adapter** = satu-satunya bagian yang berisi logika hitung (revenue, konversi, dll).
|
|
451
|
+
- **Widget Config** = satu-satunya bagian yang menentukan tampilan (deklaratif, tanpa logika).
|
|
452
|
+
|
|
453
|
+
Urutan mengerjakan: **Data Source → Adapter → Widget Config → rangkai di halaman**.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### 7.1 Kontrak antar-lapisan (referensi cepat)
|
|
458
|
+
|
|
459
|
+
**Output Data Source** (yang masuk ke adapter sebagai `raw`):
|
|
460
|
+
|
|
461
|
+
```javascript
|
|
462
|
+
{
|
|
463
|
+
bookings: [...], // WAJIB. Array semua transaksi dalam rentang tanggal.
|
|
464
|
+
recent: [...], // WAJIB. Array 10 transaksi terbaru (untuk tabel).
|
|
465
|
+
packageLocales: [], // Opsional. Untuk nama multi-bahasa. Boleh [].
|
|
466
|
+
staticCounts: { // Opsional. Angka agregat (mis. jumlah produk).
|
|
467
|
+
packages: 0,
|
|
468
|
+
sections: 0,
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
> Nama field **harus** `bookings` dan `recent` walau domainmu bukan booking —
|
|
474
|
+
> ini istilah internal modul. Isi datanya bebas (order, transaksi, reservasi, dll).
|
|
475
|
+
|
|
476
|
+
**Output Adapter** (yang dirender oleh komponen):
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
{
|
|
480
|
+
stats: {
|
|
481
|
+
bookingsConfirm: number, // angka untuk stat card (key bebas, asal cocok config)
|
|
482
|
+
revenueConfirm: number,
|
|
483
|
+
// ...tambahkan key lain sesuai kebutuhan
|
|
484
|
+
},
|
|
485
|
+
charts: {
|
|
486
|
+
dailyTrends: [ // area chart tren harian
|
|
487
|
+
{ dateKey: "2026-05-01", label: "01 Mei", count: 5, revenue: 2500000, pendingCount: 2 }
|
|
488
|
+
],
|
|
489
|
+
statusDistribution: [ // pie chart distribusi status
|
|
490
|
+
{ status: "confirmed", label: "Confirmed", count: 30 },
|
|
491
|
+
],
|
|
492
|
+
audienceDistribution: [], // pie chart kedua (boleh kosong [])
|
|
493
|
+
topPackages: [ // bar chart terlaris (boleh kosong [])
|
|
494
|
+
{ packageId: "uuid", name: "Nama Produk", value: 10 },
|
|
495
|
+
],
|
|
496
|
+
},
|
|
497
|
+
table: {
|
|
498
|
+
recentBookings: [ // baris tabel transaksi terbaru
|
|
499
|
+
{
|
|
500
|
+
id: "uuid",
|
|
501
|
+
createdAt: "2026-05-01T10:00:00Z", // ISO string
|
|
502
|
+
customerName: "Budi Santoso",
|
|
503
|
+
packageName: "Nama Produk",
|
|
504
|
+
audienceLabel: "Domestic", // boleh "-"
|
|
505
|
+
totalIDR: 500000, // integer Rupiah
|
|
506
|
+
status: "confirmed", // lowercase
|
|
507
|
+
statusLabel: "Confirmed",
|
|
508
|
+
}
|
|
509
|
+
],
|
|
510
|
+
},
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
> Penting: `valueKey` di widget config (Bab 8) harus cocok dengan key di `stats`.
|
|
515
|
+
> Contoh: jika config punya `valueKey: "revenueConfirm"`, adapter wajib
|
|
516
|
+
> mengembalikan `stats.revenueConfirm`.
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### 7.2 STUDI KASUS: Aplikasi Laundry (dari nol)
|
|
521
|
+
|
|
522
|
+
Misalkan client punya 1 tabel Supabase bernama `laundry_orders`:
|
|
523
|
+
|
|
524
|
+
| Kolom | Tipe | Contoh |
|
|
525
|
+
|-------|------|--------|
|
|
526
|
+
| `id` | uuid | `a1b2...` |
|
|
527
|
+
| `created_at` | timestamptz | `2026-05-01T10:00:00Z` |
|
|
528
|
+
| `customer_name` | text | `Budi Santoso` |
|
|
529
|
+
| `total_price` | integer | `50000` |
|
|
530
|
+
| `status` | text | `confirmed` / `pending` |
|
|
531
|
+
| `service_type` | text | `Cuci Kering` |
|
|
532
|
+
|
|
533
|
+
Target dashboard: 2 stat card (total order, total pendapatan), 2 chart
|
|
534
|
+
(tren harian + distribusi status), dan 1 tabel order terbaru.
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
#### Langkah 1 — Tulis DATA SOURCE
|
|
539
|
+
|
|
540
|
+
Buat file `src/datasources/laundryDataSource.js`. Tugasnya: query Supabase,
|
|
541
|
+
kembalikan dalam bentuk `{ bookings, recent, staticCounts }`.
|
|
542
|
+
|
|
543
|
+
```javascript
|
|
544
|
+
// src/datasources/laundryDataSource.js
|
|
545
|
+
export function createLaundrySupabaseSource(supabase) {
|
|
546
|
+
return {
|
|
547
|
+
// Dipanggil otomatis oleh useReusableDashboard setiap filter berubah.
|
|
548
|
+
// Parameter fromISO/toISO/statusScope sudah dihitung oleh modul.
|
|
549
|
+
async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
|
|
550
|
+
// (a) Semua order dalam rentang tanggal → untuk chart & stats
|
|
551
|
+
const { data: orders = [] } = await supabase
|
|
552
|
+
.from("laundry_orders")
|
|
553
|
+
.select("id, created_at, customer_name, total_price, status, service_type")
|
|
554
|
+
.gte("created_at", fromISO)
|
|
555
|
+
.lte("created_at", toISO)
|
|
556
|
+
.order("created_at", { ascending: true });
|
|
557
|
+
|
|
558
|
+
// (b) 10 order terbaru → untuk tabel "Order Terbaru"
|
|
559
|
+
const recentQuery = supabase
|
|
560
|
+
.from("laundry_orders")
|
|
561
|
+
.select("id, created_at, customer_name, total_price, status, service_type")
|
|
562
|
+
.gte("created_at", fromISO)
|
|
563
|
+
.lte("created_at", toISO)
|
|
564
|
+
.order("created_at", { ascending: false })
|
|
565
|
+
.limit(10);
|
|
566
|
+
|
|
567
|
+
// Hormati filter status dari UI (confirmed/pending/all)
|
|
568
|
+
if (statusScope && statusScope !== "all") {
|
|
569
|
+
recentQuery.eq("status", statusScope);
|
|
570
|
+
}
|
|
571
|
+
const { data: recent = [] } = await recentQuery;
|
|
572
|
+
|
|
573
|
+
// (c) Angka agregat opsional, mis. jumlah jenis layanan
|
|
574
|
+
const { count: serviceCount } = await supabase
|
|
575
|
+
.from("services")
|
|
576
|
+
.select("*", { count: "exact", head: true });
|
|
577
|
+
|
|
578
|
+
// Kembalikan SESUAI KONTRAK (nama bookings & recent wajib)
|
|
579
|
+
return {
|
|
580
|
+
bookings: orders,
|
|
581
|
+
recent,
|
|
582
|
+
packageLocales: [],
|
|
583
|
+
staticCounts: { packages: serviceCount || 0, sections: 0 },
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
// OPSIONAL — aktifkan live update realtime. Hapus jika tak perlu.
|
|
588
|
+
subscribeLiveUpdate(onEvent) {
|
|
589
|
+
const channel = supabase
|
|
590
|
+
.channel("laundry-dashboard-live")
|
|
591
|
+
.on("postgres_changes",
|
|
592
|
+
{ event: "*", schema: "public", table: "laundry_orders" }, onEvent)
|
|
593
|
+
.subscribe();
|
|
594
|
+
return () => supabase.removeChannel(channel);
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
> Tips: kalau kamu **tidak** butuh realtime, cukup hapus fungsi
|
|
601
|
+
> `subscribeLiveUpdate`. Badge "Live" otomatis tidak muncul.
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
#### Langkah 2 — Tulis ADAPTER
|
|
606
|
+
|
|
607
|
+
Buat file `src/adapters/laundryAdapter.js`. Tugasnya: ubah `raw` dari data source
|
|
608
|
+
menjadi `{ stats, charts, table }`. Pakai helper `toNumber` & `buildDayBuckets`
|
|
609
|
+
dari modul agar tidak menulis ulang logika tanggal.
|
|
610
|
+
|
|
611
|
+
```javascript
|
|
612
|
+
// src/adapters/laundryAdapter.js
|
|
613
|
+
import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
|
|
614
|
+
|
|
615
|
+
// (1) State kosong — dipakai modul sebagai initial state & saat error.
|
|
616
|
+
// Strukturnya HARUS sama dengan output adaptLaundryData di bawah.
|
|
617
|
+
export function createEmptyLaundryData() {
|
|
618
|
+
return {
|
|
619
|
+
stats: { bookingsConfirm: 0, revenueConfirm: 0 },
|
|
620
|
+
charts: { dailyTrends: [], statusDistribution: [], audienceDistribution: [], topPackages: [] },
|
|
621
|
+
table: { recentBookings: [] },
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// (2) Fungsi adapter utama.
|
|
626
|
+
export function adaptLaundryData({ raw, filters, range, dateLocale, labels }) {
|
|
627
|
+
if (!raw) return createEmptyLaundryData();
|
|
628
|
+
|
|
629
|
+
const orders = raw.bookings || [];
|
|
630
|
+
const recent = raw.recent || [];
|
|
631
|
+
|
|
632
|
+
// Siapkan bucket harian kosong untuk chart tren (1 bucket = 1 hari)
|
|
633
|
+
const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
|
|
634
|
+
const dayLookup = new Map(dailyBuckets.map((b) => [b.dateKey, b]));
|
|
635
|
+
|
|
636
|
+
const statusMap = new Map();
|
|
637
|
+
let confirmed = 0;
|
|
638
|
+
let revenue = 0;
|
|
639
|
+
|
|
640
|
+
// Loop semua order: hitung total & isi bucket harian
|
|
641
|
+
orders.forEach((row) => {
|
|
642
|
+
const status = String(row.status || "pending").toLowerCase();
|
|
643
|
+
const amount = toNumber(row.total_price); // ← kolom DB kamu
|
|
644
|
+
const dayKey = String(row.created_at || "").slice(0, 10);
|
|
645
|
+
|
|
646
|
+
statusMap.set(status, (statusMap.get(status) || 0) + 1);
|
|
647
|
+
|
|
648
|
+
const bucket = dayLookup.get(dayKey);
|
|
649
|
+
if (bucket && status === "confirmed") {
|
|
650
|
+
bucket.count += 1;
|
|
651
|
+
bucket.revenue += amount;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (status === "confirmed") { confirmed += 1; revenue += amount; }
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
// stats: key di sini HARUS cocok dengan valueKey di widget config
|
|
659
|
+
stats: {
|
|
660
|
+
bookingsConfirm: confirmed,
|
|
661
|
+
revenueConfirm: revenue,
|
|
662
|
+
},
|
|
663
|
+
charts: {
|
|
664
|
+
dailyTrends: dailyBuckets,
|
|
665
|
+
statusDistribution: Array.from(statusMap.entries()).map(([status, count]) => ({
|
|
666
|
+
status,
|
|
667
|
+
label: labels?.formatStatusLabel?.(status) || status,
|
|
668
|
+
count,
|
|
669
|
+
})),
|
|
670
|
+
audienceDistribution: [], // tidak dipakai laundry → kosong
|
|
671
|
+
topPackages: [], // tidak dipakai laundry → kosong
|
|
672
|
+
},
|
|
673
|
+
table: {
|
|
674
|
+
recentBookings: recent.map((row) => {
|
|
675
|
+
const status = String(row.status || "pending").toLowerCase();
|
|
676
|
+
return {
|
|
677
|
+
id: row.id,
|
|
678
|
+
createdAt: row.created_at,
|
|
679
|
+
customerName: row.customer_name || "-",
|
|
680
|
+
packageName: row.service_type || "-", // ← kolom DB kamu
|
|
681
|
+
audienceLabel: "-",
|
|
682
|
+
totalIDR: toNumber(row.total_price),
|
|
683
|
+
status,
|
|
684
|
+
statusLabel: labels?.formatStatusLabel?.(status) || status,
|
|
685
|
+
};
|
|
686
|
+
}),
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
}
|
|
93
690
|
```
|
|
94
691
|
|
|
95
|
-
|
|
692
|
+
> Catatan: `audienceDistribution` dan `topPackages` boleh `[]` jika domainmu
|
|
693
|
+
> tidak butuh. Lebih baik lagi: **jangan daftarkan** chart itu di widget config
|
|
694
|
+
> (Langkah 3), supaya tidak tampil sama sekali.
|
|
96
695
|
|
|
97
696
|
---
|
|
98
697
|
|
|
99
|
-
|
|
698
|
+
#### Langkah 3 — Tulis WIDGET CONFIG
|
|
699
|
+
|
|
700
|
+
Buat file `src/config/laundryConfig.js`. Ini murni deklaratif — menentukan
|
|
701
|
+
widget apa yang muncul. `valueKey` harus cocok dengan key `stats` dari adapter,
|
|
702
|
+
dan `label` adalah key dari objek labels (Langkah 4).
|
|
703
|
+
|
|
704
|
+
```javascript
|
|
705
|
+
// src/config/laundryConfig.js
|
|
706
|
+
export const laundryConfig = {
|
|
707
|
+
id: "laundry.dashboard",
|
|
708
|
+
defaultFilters: {
|
|
709
|
+
statusScope: "confirmed", // filter awal saat dibuka
|
|
710
|
+
daysPreset: 30, // tampilkan 30 hari terakhir
|
|
711
|
+
sortPkgBy: "bookings",
|
|
712
|
+
sortPkgDir: "desc",
|
|
713
|
+
},
|
|
714
|
+
widgets: {
|
|
715
|
+
// 2 stat card → valueKey cocok dgn stats di adapter
|
|
716
|
+
stats: [
|
|
717
|
+
{ id: "orders", label: "confirmedBookings", icon: "TrendingUp",
|
|
718
|
+
valueKey: "bookingsConfirm", format: "number", accentColor: "blue" },
|
|
719
|
+
{ id: "revenue", label: "confirmedRevenue", icon: "DollarSign",
|
|
720
|
+
valueKey: "revenueConfirm", format: "currency", accentColor: "green" },
|
|
721
|
+
],
|
|
722
|
+
// 2 chart → hanya yang ada datanya (tren & status)
|
|
723
|
+
charts: [
|
|
724
|
+
{ id: "trend", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
|
|
725
|
+
{ id: "status", type: "statusPie", label: "statusDistribution", icon: "PieChart" },
|
|
726
|
+
],
|
|
727
|
+
// 1 tabel → kolom mengacu ke field recentBookings dari adapter
|
|
728
|
+
table: {
|
|
729
|
+
id: "recentOrders",
|
|
730
|
+
label: "recentBookings",
|
|
731
|
+
icon: "Calendar",
|
|
732
|
+
emptyLabel: "noRecentBookings",
|
|
733
|
+
columns: [
|
|
734
|
+
{ id: "date", label: "date", accessor: "createdAt", type: "date" },
|
|
735
|
+
{ id: "customer", label: "customer", accessor: "customerName" },
|
|
736
|
+
{ id: "service", label: "package", accessor: "packageName" },
|
|
737
|
+
{ id: "total", label: "total", accessor: "totalIDR", type: "currency" },
|
|
738
|
+
{ id: "status", label: "status", accessor: "statusLabel",
|
|
739
|
+
type: "statusBadge", statusAccessor: "status" },
|
|
740
|
+
],
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
```
|
|
100
745
|
|
|
101
|
-
|
|
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) |
|
|
746
|
+
> Detail tiap field config (tipe kolom, nama ikon, format) ada di
|
|
747
|
+
> [Bab 8 — Konfigurasi Widget](#8-konfigurasi-widget).
|
|
106
748
|
|
|
107
749
|
---
|
|
108
750
|
|
|
109
|
-
|
|
751
|
+
#### Langkah 4 — Rangkai semuanya di halaman Dashboard
|
|
752
|
+
|
|
753
|
+
Sekarang gabungkan ke-3 file di atas + objek labels, lalu serahkan ke
|
|
754
|
+
`useReusableDashboard` dan render `ReusableDashboardView`.
|
|
755
|
+
|
|
756
|
+
```jsx
|
|
757
|
+
// src/pages/LaundryDashboard.jsx
|
|
758
|
+
import React, { useMemo } from "react";
|
|
759
|
+
import { supabase } from "../lib/supabaseClient";
|
|
760
|
+
import {
|
|
761
|
+
ReusableDashboardView,
|
|
762
|
+
useReusableDashboard,
|
|
763
|
+
} from "@rozaqi02/reusable-dashboard";
|
|
764
|
+
|
|
765
|
+
// 3 file yang baru kamu buat:
|
|
766
|
+
import { laundryConfig } from "../config/laundryConfig";
|
|
767
|
+
import { createLaundrySupabaseSource } from "../datasources/laundryDataSource";
|
|
768
|
+
import { adaptLaundryData, createEmptyLaundryData } from "../adapters/laundryAdapter";
|
|
769
|
+
|
|
770
|
+
// Data source dibuat sekali di luar komponen (anti re-render)
|
|
771
|
+
const source = createLaundrySupabaseSource(supabase);
|
|
772
|
+
|
|
773
|
+
// Label UI — semua teks yang tampil. Sesuaikan bahasa/konteks bisnismu.
|
|
774
|
+
const labels = {
|
|
775
|
+
title: "Dashboard Laundry",
|
|
776
|
+
refresh: "Refresh",
|
|
777
|
+
liveUpdate: "Live",
|
|
778
|
+
loadFailed: "Gagal memuat data.",
|
|
779
|
+
retry: "Coba Lagi",
|
|
780
|
+
allStatus: "Semua Status",
|
|
781
|
+
confirmedOnly: "Selesai",
|
|
782
|
+
pendingOnly: "Proses",
|
|
783
|
+
reset: "Reset",
|
|
784
|
+
confirmedBookings: "Total Order",
|
|
785
|
+
confirmedRevenue: "Total Pendapatan",
|
|
786
|
+
dailyTrends: "Tren Harian",
|
|
787
|
+
statusDistribution: "Distribusi Status",
|
|
788
|
+
recentBookings: "Order Terbaru",
|
|
789
|
+
noRecentBookings: "Belum ada order",
|
|
790
|
+
date: "Tanggal",
|
|
791
|
+
customer: "Pelanggan",
|
|
792
|
+
package: "Layanan",
|
|
793
|
+
total: "Total",
|
|
794
|
+
status: "Status",
|
|
795
|
+
bookingsMetric: "Order",
|
|
796
|
+
revenueMetric: "Pendapatan",
|
|
797
|
+
confirmedBookingMetric: "Order (Selesai)",
|
|
798
|
+
confirmedRevenueMetric: "Pendapatan (Selesai)",
|
|
799
|
+
dayLabel: (n) => `${n} hari`,
|
|
800
|
+
formatStatusLabel: (s) =>
|
|
801
|
+
({ confirmed: "Selesai", pending: "Proses", cancelled: "Batal" })[s] || s,
|
|
802
|
+
formatAudienceLabel: (v) => v || "-",
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
export default function LaundryDashboard() {
|
|
806
|
+
const state = useReusableDashboard({
|
|
807
|
+
config: laundryConfig, // ← Langkah 3
|
|
808
|
+
dataSource: source, // ← Langkah 1
|
|
809
|
+
adapter: adaptLaundryData, // ← Langkah 2
|
|
810
|
+
createEmptyState: createEmptyLaundryData, // ← Langkah 2
|
|
811
|
+
languageCode: "id",
|
|
812
|
+
dateLocale: "id-ID",
|
|
813
|
+
labels,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
return (
|
|
817
|
+
<ReusableDashboardView
|
|
818
|
+
config={laundryConfig}
|
|
819
|
+
labels={labels}
|
|
820
|
+
loading={state.loading}
|
|
821
|
+
error={state.error}
|
|
822
|
+
filters={state.filters}
|
|
823
|
+
onFilterChange={state.updateFilter}
|
|
824
|
+
onResetFilters={state.resetFilters}
|
|
825
|
+
onRefresh={state.refresh}
|
|
826
|
+
data={state.data}
|
|
827
|
+
dateLocale="id-ID"
|
|
828
|
+
liveUpdateEnabled={state.liveUpdateEnabled}
|
|
829
|
+
/>
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
Selesai. Buka halaman tersebut → dashboard laundry tampil lengkap dengan data
|
|
835
|
+
dari Supabase, filter tanggal, search, sort, dan pagination — semua sudah
|
|
836
|
+
ditangani modul.
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
### 7.3 Checklist & troubleshooting domain baru
|
|
841
|
+
|
|
842
|
+
| Gejala | Penyebab umum | Solusi |
|
|
843
|
+
|--------|---------------|--------|
|
|
844
|
+
| Stat card menampilkan `0` terus | `valueKey` di config tidak cocok dengan key `stats` adapter | Samakan nama, mis. `valueKey: "revenueConfirm"` ↔ `stats.revenueConfirm` |
|
|
845
|
+
| Chart kosong | Data source tidak mengembalikan `bookings`, atau status bukan `"confirmed"` | Pastikan field bernama `bookings` & status lowercase |
|
|
846
|
+
| Tabel kosong padahal ada data | Data source tidak mengembalikan `recent` | Tambahkan query `recent` (10 terbaru) |
|
|
847
|
+
| Error "is not a function" | Lupa pasang `createEmptyState` | Wajib kirim `createEmptyXxxData` ke hook |
|
|
848
|
+
| Label tampil sebagai key mentah | Key di config (`label`) tidak ada di objek `labels` | Tambahkan key tersebut di objek labels |
|
|
849
|
+
| Badge "Live" tidak muncul | `subscribeLiveUpdate` tidak ada / Realtime belum aktif | Tambahkan fungsi + aktifkan Realtime di Supabase |
|
|
850
|
+
|
|
851
|
+
Checklist final sebelum rilis ke client:
|
|
852
|
+
- [ ] Data source mengembalikan `{ bookings, recent }` (nama persis).
|
|
853
|
+
- [ ] Adapter mengembalikan `{ stats, charts, table }` lengkap.
|
|
854
|
+
- [ ] `createEmptyXxxData` punya struktur sama dengan output adapter.
|
|
855
|
+
- [ ] Semua `valueKey` di config ada di `stats`.
|
|
856
|
+
- [ ] Semua `label`/`emptyLabel` di config ada di objek `labels`.
|
|
857
|
+
- [ ] RLS Supabase mengizinkan `SELECT` untuk role `anon` (lihat [Bab 6](#6-setup-database-supabase)).
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
## 8. Konfigurasi Widget
|
|
862
|
+
|
|
863
|
+
Widget configuration menentukan apa yang ditampilkan di dashboard.
|
|
864
|
+
Kamu bisa gunakan preset yang ada atau buat konfigurasi baru.
|
|
865
|
+
|
|
866
|
+
```javascript
|
|
867
|
+
export const myConfig = {
|
|
868
|
+
id: "my.dashboard",
|
|
869
|
+
|
|
870
|
+
// Filter default saat pertama kali dibuka
|
|
871
|
+
defaultFilters: {
|
|
872
|
+
statusScope: "confirmed", // "confirmed" | "pending" | "all"
|
|
873
|
+
includePendingOverlay: false, // tampilkan overlay pending di chart?
|
|
874
|
+
audience: "", // "" | "domestic" | "foreign"
|
|
875
|
+
daysPreset: 30, // 7 | 30 | 90 | 0 (0 = custom range)
|
|
876
|
+
sortPkgBy: "revenue", // "bookings" | "revenue"
|
|
877
|
+
sortPkgDir: "desc", // "desc" | "asc"
|
|
878
|
+
},
|
|
879
|
+
|
|
880
|
+
widgets: {
|
|
881
|
+
// Stat cards (1–4 kartu)
|
|
882
|
+
stats: [
|
|
883
|
+
{
|
|
884
|
+
id: "totalOrders",
|
|
885
|
+
label: "confirmedBookings", // key dari objek labels
|
|
886
|
+
icon: "TrendingUp", // nama ikon Lucide
|
|
887
|
+
valueKey: "bookingsConfirm", // key dari data.stats yang dikembalikan adapter
|
|
888
|
+
format: "number", // "number" | "currency" | "percent"
|
|
889
|
+
accentColor: "blue", // "blue" | "green" | "violet" | "orange" | "sky" | "rose"
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
id: "totalRevenue",
|
|
893
|
+
label: "confirmedRevenue",
|
|
894
|
+
icon: "DollarSign",
|
|
895
|
+
valueKey: "revenueConfirm",
|
|
896
|
+
format: "currency",
|
|
897
|
+
accentColor: "green",
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
|
|
901
|
+
// Chart cards (1–4 chart)
|
|
902
|
+
charts: [
|
|
903
|
+
{
|
|
904
|
+
id: "trendHarian",
|
|
905
|
+
type: "dailyArea", // "dailyArea" | "statusPie" | "audiencePie" | "topPackagesBar"
|
|
906
|
+
label: "dailyTrends", // key dari objek labels
|
|
907
|
+
icon: "BarChart3",
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
id: "distribusiStatus",
|
|
911
|
+
type: "statusPie",
|
|
912
|
+
label: "statusDistribution",
|
|
913
|
+
icon: "PieChart",
|
|
914
|
+
},
|
|
915
|
+
],
|
|
916
|
+
|
|
917
|
+
// Tabel (hanya 1)
|
|
918
|
+
table: {
|
|
919
|
+
id: "recentOrders",
|
|
920
|
+
label: "recentBookings", // key dari objek labels
|
|
921
|
+
icon: "Calendar",
|
|
922
|
+
emptyLabel: "noRecentBookings", // key dari objek labels, untuk empty state
|
|
923
|
+
columns: [
|
|
924
|
+
{ id: "date", label: "date", accessor: "createdAt", type: "date" },
|
|
925
|
+
{ id: "customer", label: "customer", accessor: "customerName" },
|
|
926
|
+
{ id: "product", label: "package", accessor: "packageName" },
|
|
927
|
+
{ id: "total", label: "total", accessor: "totalIDR", type: "currency" },
|
|
928
|
+
{
|
|
929
|
+
id: "status", label: "status", accessor: "statusLabel",
|
|
930
|
+
type: "statusBadge",
|
|
931
|
+
statusAccessor: "status", // kolom yang berisi status lowercase (confirmed/pending/cancelled)
|
|
932
|
+
},
|
|
933
|
+
],
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
**Tipe kolom tabel:**
|
|
940
|
+
| `type` | Format Output |
|
|
941
|
+
|--------|---------------|
|
|
942
|
+
| `date` | Diformat dengan `dateLocale` (contoh: "01 Mei 2026") |
|
|
943
|
+
| `currency` | Diformat dengan prefix "Rp" dan titik ribuan |
|
|
944
|
+
| `statusBadge` | Badge berwarna sesuai status |
|
|
945
|
+
| _(tanpa type)_ | Ditampilkan apa adanya (string) |
|
|
946
|
+
|
|
947
|
+
**Nama ikon yang tersedia:**
|
|
948
|
+
`TrendingUp`, `TrendingDown`, `DollarSign`, `Users`, `PieChart`, `BarChart3`,
|
|
949
|
+
`Calendar`, `RotateCcw`, `Search`, `ChevronLeft`, `ChevronRight`, `ArrowUp`,
|
|
950
|
+
`ArrowDown`, `AlertCircle`
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
## 9. Label & Internasionalisasi
|
|
955
|
+
|
|
956
|
+
### Tanpa i18n (objek manual)
|
|
957
|
+
|
|
958
|
+
Buat objek labels dengan semua key yang diperlukan:
|
|
959
|
+
|
|
960
|
+
```javascript
|
|
961
|
+
const labels = {
|
|
962
|
+
// Header
|
|
963
|
+
title: "Dashboard",
|
|
964
|
+
refresh: "Refresh",
|
|
965
|
+
liveUpdate: "Live",
|
|
966
|
+
loadFailed: "Gagal memuat data.",
|
|
967
|
+
retry: "Coba Lagi",
|
|
968
|
+
|
|
969
|
+
// Filter
|
|
970
|
+
confirmedOnly: "Confirmed",
|
|
971
|
+
pendingOnly: "Pending",
|
|
972
|
+
allStatus: "Semua Status",
|
|
973
|
+
showPendingOverlay: "Tampilkan pending",
|
|
974
|
+
allAudience: "Semua",
|
|
975
|
+
audienceDomestic: "Domestic",
|
|
976
|
+
audienceForeign: "Foreign",
|
|
977
|
+
customDate: "Kustom",
|
|
978
|
+
reset: "Reset",
|
|
979
|
+
topSort: "Urutkan",
|
|
980
|
+
sortBookings: "Qty Terjual",
|
|
981
|
+
sortRevenue: "Revenue",
|
|
982
|
+
sortDesc: "Turun",
|
|
983
|
+
sortAsc: "Naik",
|
|
984
|
+
|
|
985
|
+
// Stat cards (sesuaikan dengan valueKey di config)
|
|
986
|
+
confirmedBookings: "Total Pesanan",
|
|
987
|
+
confirmedRevenue: "Total Pendapatan",
|
|
988
|
+
avgRevenue: "Rata-rata / Pesanan",
|
|
989
|
+
conversionRate: "Conversion Rate",
|
|
990
|
+
totalProducts: "Total Produk",
|
|
991
|
+
|
|
992
|
+
// Chart & tabel
|
|
993
|
+
dailyTrends: "Tren Harian",
|
|
994
|
+
statusDistribution: "Distribusi Status",
|
|
995
|
+
audienceDistribution: "Distribusi Audiens",
|
|
996
|
+
topPackages: "Produk Terlaris",
|
|
997
|
+
recentBookings: "Pesanan Terbaru",
|
|
998
|
+
date: "Tanggal",
|
|
999
|
+
customer: "Pelanggan",
|
|
1000
|
+
package: "Produk",
|
|
1001
|
+
audience: "Audiens",
|
|
1002
|
+
total: "Total",
|
|
1003
|
+
status: "Status",
|
|
1004
|
+
noRecentBookings: "Belum ada pesanan",
|
|
1005
|
+
|
|
1006
|
+
// Chart metrics
|
|
1007
|
+
bookingsMetric: "Pesanan",
|
|
1008
|
+
revenueMetric: "Pendapatan",
|
|
1009
|
+
pendingMetric: "Pesanan (Pending)",
|
|
1010
|
+
confirmedBookingMetric: "Pesanan (Confirmed)",
|
|
1011
|
+
confirmedRevenueMetric: "Pendapatan (Confirmed)",
|
|
1012
|
+
unknownAudience: "Unknown",
|
|
1013
|
+
|
|
1014
|
+
// Fungsi formatter (wajib ada)
|
|
1015
|
+
dayLabel: (n) => `${n} hari`,
|
|
1016
|
+
formatStatusLabel: (status) => ({
|
|
1017
|
+
confirmed: "Confirmed",
|
|
1018
|
+
pending: "Pending",
|
|
1019
|
+
cancelled: "Cancelled",
|
|
1020
|
+
shipped: "Dikirim",
|
|
1021
|
+
delivered: "Terkirim",
|
|
1022
|
+
})[status] || status,
|
|
1023
|
+
formatAudienceLabel: (value) => value || "Unknown",
|
|
1024
|
+
};
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
### Dengan react-i18next
|
|
110
1028
|
|
|
111
1029
|
```jsx
|
|
112
1030
|
import { useTranslation } from "react-i18next";
|
|
113
1031
|
import { createDashboardLabels } from "@rozaqi02/reusable-dashboard";
|
|
114
1032
|
|
|
115
|
-
|
|
116
|
-
const
|
|
1033
|
+
function Dashboard() {
|
|
1034
|
+
const { t, i18n } = useTranslation();
|
|
1035
|
+
const languageCode = (i18n.language || "id").slice(0, 2);
|
|
1036
|
+
const dateLocale = languageCode === "id" ? "id-ID" : "en-US";
|
|
1037
|
+
const labels = useMemo(() => createDashboardLabels(t), [t]);
|
|
1038
|
+
// ...
|
|
1039
|
+
}
|
|
117
1040
|
```
|
|
118
1041
|
|
|
1042
|
+
> `createDashboardLabels(t)` menggunakan key i18n dari namespace default.
|
|
1043
|
+
> Pastikan file terjemahan kamu memiliki key `admin.dashboard.*`.
|
|
1044
|
+
|
|
119
1045
|
---
|
|
120
1046
|
|
|
121
|
-
## API
|
|
1047
|
+
## 10. API Reference Lengkap
|
|
122
1048
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
1049
|
+
### Config
|
|
1050
|
+
|
|
1051
|
+
| Export | Tipe | Deskripsi |
|
|
1052
|
+
|--------|------|-----------|
|
|
1053
|
+
| `cidikaWidgetConfig` | Object | Config dashboard Cidika Travel |
|
|
1054
|
+
| `tokoSepatuWidgetConfig` | Object | Config dashboard Toko Sepatu |
|
|
1055
|
+
| `dummyUmkmWidgetConfig` | Object | Config dashboard UMKM generik |
|
|
126
1056
|
|
|
127
|
-
|
|
128
|
-
import { createCidikaSupabaseSource, createTokoSepatuSupabaseSource }
|
|
1057
|
+
### Data Source
|
|
129
1058
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
1059
|
+
| Export | Parameter | Deskripsi |
|
|
1060
|
+
|--------|-----------|-----------|
|
|
1061
|
+
| `createCidikaSupabaseSource(supabase)` | Supabase client | Data source Cidika Travel |
|
|
1062
|
+
| `createTokoSepatuSupabaseSource(supabase)` | Supabase client | Data source Toko Sepatu |
|
|
134
1063
|
|
|
135
|
-
|
|
136
|
-
import { useReusableDashboard, useRealtimeUpdate }
|
|
1064
|
+
### Data Adapter
|
|
137
1065
|
|
|
138
|
-
|
|
139
|
-
|
|
1066
|
+
| Export | Parameter | Deskripsi |
|
|
1067
|
+
|--------|-----------|-----------|
|
|
1068
|
+
| `adaptCidikaDashboardData({ raw, filters, range, dateLocale, languageCode, labels })` | — | Adapter Cidika |
|
|
1069
|
+
| `createEmptyDashboardData()` | — | Empty state Cidika |
|
|
1070
|
+
| `adaptTokoSepatuData({ raw, filters, range, dateLocale, labels })` | — | Adapter Toko Sepatu |
|
|
1071
|
+
| `createEmptyTokoSepatuData()` | — | Empty state Toko Sepatu |
|
|
1072
|
+
| `adaptDummyUmkmData({ raw, filters, range, dateLocale, labels })` | — | Adapter UMKM generik |
|
|
1073
|
+
| `createEmptyDummyUmkmData()` | — | Empty state UMKM generik |
|
|
140
1074
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1075
|
+
### Hook Utama
|
|
1076
|
+
|
|
1077
|
+
```typescript
|
|
1078
|
+
const state = useReusableDashboard({
|
|
1079
|
+
config: object, // Widget configuration
|
|
1080
|
+
dataSource: object, // Objek dari createXxxSupabaseSource()
|
|
1081
|
+
adapter: Function, // Fungsi adaptXxxData
|
|
1082
|
+
createEmptyState: Function, // Fungsi createEmptyXxxData
|
|
1083
|
+
languageCode: "id" | "en",
|
|
1084
|
+
dateLocale: "id-ID" | "en-US",
|
|
1085
|
+
labels: object,
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// state yang dikembalikan:
|
|
1089
|
+
state.data // Data dashboard terproses
|
|
1090
|
+
state.loading // boolean — sedang loading?
|
|
1091
|
+
state.error // string — pesan error (kosong jika tidak ada)
|
|
1092
|
+
state.filters // State filter aktif saat ini
|
|
1093
|
+
state.updateFilter // (field: string, value: any) => void
|
|
1094
|
+
state.resetFilters // () => void
|
|
1095
|
+
state.refresh // ({ silent?: boolean }) => void
|
|
1096
|
+
state.liveUpdateEnabled // boolean — realtime subscription aktif?
|
|
1097
|
+
state.lastUpdatedAt // Date | null
|
|
1098
|
+
state.range // { fromISO, toISO, daysWindow }
|
|
144
1099
|
```
|
|
145
1100
|
|
|
1101
|
+
### Komponen
|
|
1102
|
+
|
|
1103
|
+
| Komponen | Deskripsi |
|
|
1104
|
+
|----------|-----------|
|
|
1105
|
+
| `ReusableDashboardView` | Halaman dashboard lengkap — gunakan ini untuk integrasi cepat |
|
|
1106
|
+
| `StatCard` | Kartu metrik dengan warna aksen |
|
|
1107
|
+
| `ChartCard` | Kartu grafik (area/pie/bar) |
|
|
1108
|
+
| `DataTable` | Tabel dengan search, sort, pagination |
|
|
1109
|
+
| `FilterPanel` | Panel filter |
|
|
1110
|
+
| `SearchBar` | Input pencarian |
|
|
1111
|
+
| `DateRangeFilter` | Filter rentang tanggal (dropdown picker) |
|
|
1112
|
+
| `Badge` | Label status berwarna |
|
|
1113
|
+
| `Button` | Tombol |
|
|
1114
|
+
| `Input` | Input field |
|
|
1115
|
+
| `Typography` | Komponen teks |
|
|
1116
|
+
| `Icon` | Ikon (Lucide) |
|
|
1117
|
+
| `SkeletonLoader` | Placeholder loading |
|
|
1118
|
+
| `DashboardLayout` | Template layout dengan slot sidebar dan content |
|
|
1119
|
+
| `SidebarNavigation` | Navigasi sidebar |
|
|
1120
|
+
| `TopbarHeader` | Header atas |
|
|
1121
|
+
|
|
1122
|
+
### Utility
|
|
1123
|
+
|
|
1124
|
+
| Function | Deskripsi |
|
|
1125
|
+
|----------|-----------|
|
|
1126
|
+
| `createDashboardLabels(t)` | Buat labels dari fungsi `t` react-i18next |
|
|
1127
|
+
| `createDefaultFilters(base?)` | Buat state filter default |
|
|
1128
|
+
| `resolveDateRange({ daysPreset, dateFrom, dateTo })` | Resolve ke `{ fromISO, toISO, daysWindow }` |
|
|
1129
|
+
| `formatYYYYMMDD(date)` | Format `Date` ke `"YYYY-MM-DD"` |
|
|
1130
|
+
| `formatIDR(value)` | Format angka ke format Rupiah (`1.500.000`) |
|
|
1131
|
+
| `formatDate(value, locale)` | Format ISO date string dengan locale |
|
|
1132
|
+
| `shortId(value, length?)` | Potong string (default 8 karakter) |
|
|
1133
|
+
| `toNumber(value)` | Konversi ke angka, fallback ke 0 |
|
|
1134
|
+
| `buildDayBuckets(daysWindow, fromISO, dateLocale)` | Bangun array bucket harian untuk chart |
|
|
1135
|
+
| `sortMapEntries(map, direction)` | Urutkan entri Map berdasarkan nilai |
|
|
1136
|
+
|
|
146
1137
|
---
|
|
147
1138
|
|
|
148
|
-
##
|
|
1139
|
+
## 11. Pengembangan & Kontribusi
|
|
149
1140
|
|
|
150
1141
|
```bash
|
|
151
|
-
|
|
1142
|
+
# Clone repository
|
|
1143
|
+
git clone https://github.com/rozaqi02/reusable-dashboard-umkm.git
|
|
152
1144
|
cd reusable-dashboard-umkm
|
|
1145
|
+
|
|
1146
|
+
# Install dependencies
|
|
153
1147
|
npm install
|
|
154
|
-
|
|
155
|
-
|
|
1148
|
+
|
|
1149
|
+
# Build
|
|
1150
|
+
npm run build # Output ke dist/
|
|
1151
|
+
|
|
1152
|
+
# Test
|
|
1153
|
+
npm test # Jest + React Testing Library
|
|
1154
|
+
npm test -- --coverage # Lihat code coverage
|
|
156
1155
|
```
|
|
157
1156
|
|
|
1157
|
+
### Cara Publish Versi Baru ke npm
|
|
1158
|
+
|
|
1159
|
+
Modul ini publik di npm: `@rozaqi02/reusable-dashboard`. Setiap kali kode modul
|
|
1160
|
+
berubah, kamu wajib publish versi baru agar perubahan sampai ke client.
|
|
1161
|
+
|
|
1162
|
+
```bash
|
|
1163
|
+
# 1. Pastikan sudah login (sekali saja per device)
|
|
1164
|
+
npm login # buka browser untuk autentikasi
|
|
1165
|
+
npm whoami # verifikasi → harus muncul "rozaqi02"
|
|
1166
|
+
|
|
1167
|
+
# 2. Naikkan versi (pilih salah satu sesuai jenis perubahan)
|
|
1168
|
+
npm version patch # 1.1.2 → 1.1.3 (bug fix / perbaikan kecil)
|
|
1169
|
+
npm version minor # 1.1.2 → 1.2.0 (fitur baru, tanpa breaking change)
|
|
1170
|
+
npm version major # 1.1.2 → 2.0.0 (breaking change)
|
|
1171
|
+
|
|
1172
|
+
# 3. Publish (build otomatis jalan dulu lewat prepublishOnly)
|
|
1173
|
+
npm publish # akan meminta OTP (One-Time Password)
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
> **Catatan OTP:** karena akun npm kamu mengaktifkan 2FA, `npm publish` akan
|
|
1177
|
+
> meminta one-time password. Masukkan kode dari aplikasi authenticator kamu
|
|
1178
|
+
> (atau gunakan `npm publish --otp=123456`). Langkah ini **harus dijalankan
|
|
1179
|
+
> manual di terminal kamu** — tidak bisa diotomasi.
|
|
1180
|
+
|
|
1181
|
+
> **Publish ≠ git push.** `npm publish` hanya mengunggah isi folder `dist/`
|
|
1182
|
+
> (lihat field `files` di `package.json`) ke registry npm. Tidak bergantung pada
|
|
1183
|
+
> commit/push git terakhir. Jadi kamu tidak perlu push ke GitHub dulu agar
|
|
1184
|
+
> bisa publish — keduanya independen. (Tetap disarankan commit & push agar
|
|
1185
|
+
> source code di GitHub sinkron dengan versi yang dirilis.)
|
|
1186
|
+
|
|
1187
|
+
### Update Modul di Project Client
|
|
1188
|
+
|
|
1189
|
+
Setelah versi baru terbit, di setiap project yang memakai modul:
|
|
1190
|
+
|
|
1191
|
+
```bash
|
|
1192
|
+
npm install @rozaqi02/reusable-dashboard@latest
|
|
1193
|
+
```
|
|
1194
|
+
|
|
1195
|
+
Lalu restart dev server (`npm start` / `npm run dev`) agar perubahan termuat.
|
|
1196
|
+
|
|
158
1197
|
---
|
|
159
1198
|
|
|
160
1199
|
## License
|
|
161
1200
|
|
|
162
1201
|
MIT — Ahmad Abror Rozaqi Fatoni
|
|
1202
|
+
|
|
1203
|
+
---
|
|
1204
|
+
|
|
1205
|
+
**Made with ❤️ for UMKM Indonesia**
|