@rozaqi02/reusable-dashboard
v1.3.4
Published
Reusable dashboard module for admin web apps (UMKM).
Maintainers
Readme
@rozaqi02/reusable-dashboard
Modul dashboard admin reusable untuk aplikasi UMKM berbasis React + Supabase. Mendukung berbagai domain bisnis (travel, toko online, UMKM generik) tanpa menulis ulang komponen. CSS sudah terbundle — tidak perlu konfigurasi Tailwind.
Daftar Isi
- ⚡ Cara Tercepat —
<AutoDashboard>(Plug & Play) - Instalasi
- Prasyarat Teknis
- Preset yang Tersedia
- Quick Start — Toko Sepatu (Contoh Lengkap)
- Quick Start — Cidika Travel
- Setup Database Supabase
- Cara Membuat Adapter untuk Domain Bisnis Baru
- Konfigurasi Widget
- Label & Internasionalisasi
- API Reference Lengkap
- Setup Wizard — Panduan Konfigurasi Interaktif
- Pengembangan & Kontribusi
0. Cara Tercepat — <AutoDashboard> (Plug & Play)
⚠️ PENTING (BACA INI DULU):
- Jangan lupa untuk meng-import file CSS modul di file main/global CSS proyek client agar tampilan tidak pecah (lihat Bab 1).
Kalau kamu hanya ingin dashboard jadi tanpa menulis file config apa pun, pakai
komponen <AutoDashboard>.
Ada dua cara untuk menggunakannya:
Opsi A: Zero-Config Mode (Rekomendasi — v1.3.0+)
Cukup panggil komponen dengan prop supabase saja:
import { AutoDashboard } from "@rozaqi02/reusable-dashboard";
import { supabase } from "./lib/supabaseClient";
export default function Dashboard() {
return <AutoDashboard supabase={supabase} />;
}Saat pertama kali dibuka, Setup Wizard akan muncul secara otomatis. Kamu bisa memilih tabel, memetakan kolom secara visual, lalu klik tombol "🚀 Terapkan Langsung" di langkah terakhir. Konfigurasi akan disimpan di localStorage (rdb_wizard_config) dan dashboard langsung tampil. Jika ingin mengubah konfigurasi di kemudian hari, klik tombol "🛠️ Wizard" di kanan atas dashboard.
Opsi B: Mengisi Props Manual
Beri tahu nama tabel dan pemetaan kolom melalui props:
import { AutoDashboard } from "@rozaqi02/reusable-dashboard";
import { supabase } from "./lib/supabaseClient";
export default function Dashboard() {
return (
<AutoDashboard
supabase={supabase}
table="bookings"
columns={{
date: "created_at", // WAJIB — kolom timestamp
status: "status", // opsional — kolom status transaksi
total: "total_idr", // opsional — kolom nilai uang
customer: "customer_name", // opsional — nama pelanggan
item: "package_id", // opsional — nama produk/paket/layanan
audience: "audience", // opsional — segmen pelanggan
}}
confirmedValue="confirmed" // nilai status yang dihitung "berhasil"
pendingValue="pending" // nilai status yang dihitung "menunggu"
title="Dashboard Cidika"
/>
);
}Jangan lupa import CSS-nya sekali saja (lihat Bab 1).
Fitur Tambahan: Paginasi Fleksibel (v1.3.0+)
Tabel transaksi pada dashboard kini dilengkapi selector baris (5 / 10 / 25 / 50 baris per halaman) di pojok kanan atas tabel. Pilihan pengguna akan disimpan otomatis di localStorage (rdb_table_page_size).
Filter Global & Lokal (v1.3.2+)
Untuk memberikan kejelasan interaksi pengguna, filter dibagi menjadi dua kategori:
- Filter Global (di panel atas): Memfilter data transaksi keseluruhan berdasarkan status transaksi, segmen pelanggan (audiens), dan rentang tanggal.
- Filter Lokal (di pojok kanan atas grafik "Item Teratas"): Mengatur pengurutan data grafik teratas secara lokal (berdasarkan Jumlah/Nilai, Naik/Turun).
Props <AutoDashboard>
| Prop | Tipe | Wajib | Keterangan |
|------|------|-------|------------|
| supabase | object | ✅ | Instance Supabase client. |
| table | string | ✅ | Nama tabel sumber data, mis. "bookings", "orders". |
| columns.date | string | ✅ | Kolom timestamp untuk tren harian & filter rentang. |
| columns.status | string | — | Kolom status. Tanpa ini semua baris dianggap "berhasil". |
| columns.total | string | — | Kolom nilai uang untuk metrik pendapatan. |
| columns.customer | string | — | Kolom nama pelanggan (muncul di tabel). |
| columns.item | string | — | Kolom produk/paket/layanan (mengaktifkan chart "Item Teratas"). |
| columns.audience | string | — | Kolom segmen (mengaktifkan chart distribusi segmen). |
| confirmedValue | string | — | Nilai status "berhasil" (default "confirmed"). |
| pendingValue | string | — | Nilai status "menunggu" (default "pending"). |
| title | string | — | Judul dashboard. |
| labels | object | — | Override sebagian/seluruh teks UI (lihat Bab 9). |
| dateLocale | string | — | Locale format tanggal (default "id-ID"). |
Widget menyesuaikan otomatis: chart "Distribusi Segmen" hanya muncul bila columns.audience
diisi, dan chart "Item Teratas" hanya muncul bila columns.item diisi.
Kapan memakai ini vs preset/adapter manual?
<AutoDashboard> cocok untuk data transaksional — data yang punya waktu, status,
dan (opsional) nilai uang. Ini mencakup mayoritas UMKM: travel, retail, laundry, klinik,
restoran, rental, kursus, bengkel, dan sejenisnya.
Untuk kasus yang lebih kompleks — gabungan banyak tabel, metrik khusus domain (rating bintang, okupansi kamar), atau data non-transaksional (blog/CMS, inventori murni) — gunakan pendekatan adapter manual (Bab 7) atau preset (Bab 3) yang memberi kontrol penuh.
1. Instalasi
npm install @rozaqi02/reusable-dashboard
npm install recharts @supabase/supabase-jsImport CSS
CSS modul harus di-import secara eksplisit di file CSS utama project kamu.
Create React App — tambahkan di src/index.css:
@import "../node_modules/@rozaqi02/reusable-dashboard/dist/index.css";Vite — tambahkan di src/index.css atau src/main.jsx:
/* index.css */
@import "@rozaqi02/reusable-dashboard/dist/index.css";// atau di main.jsx
import "@rozaqi02/reusable-dashboard/dist/index.css";Next.js — tambahkan di app/globals.css atau pages/_app.jsx:
@import "@rozaqi02/reusable-dashboard/dist/index.css";2. Prasyarat Teknis
| Prasyarat | Versi Minimum | Keterangan | |-----------|---------------|------------| | Node.js | 18.x | Runtime JavaScript (18 atau lebih baru) | | React | 18.0.0 | UI framework (mendukung React 19) | | recharts | 2.0.0 | Library chart (AreaChart, PieChart, BarChart) | | @supabase/supabase-js | 2.0.0 | Client Supabase untuk koneksi database | | Akun Supabase | — | Gratis di supabase.com |
Modul ini dirancang khusus untuk aplikasi React yang menggunakan Supabase sebagai backend. Belum mendukung backend lain (Firebase, REST API, GraphQL) tanpa menulis data source sendiri.
3. Preset yang Tersedia
Modul menyediakan 3 preset siap pakai untuk domain bisnis yang berbeda:
| Preset | Domain | Tabel Supabase yang Dibutuhkan | Siap Pakai? |
|--------|--------|-------------------------------|-------------|
| Cidika Travel | Travel agency / paket wisata | bookings, packages, package_locales, page_sections | Config + Adapter + Source |
| Toko Sepatu | Toko online / e-commerce | orders, order_items, products, customers | Config + Adapter + Source |
| Dummy UMKM | UMKM generik (contoh domain baru) | orders, products, customers | Config + Adapter saja* |
*Preset Dummy UMKM menyediakan widget config (
dummyUmkmWidgetConfig) dan adapter (adaptDummyUmkmData), tetapi tidak menyertakan data source siap pakai. Data source-nya ditulis manual — lihat contoh kode di Bab 7 — Studi Kasus Laundry. Ini sengaja dijadikan template untuk kamu yang ingin membuat domain baru (lihat Bab 7).
Hanya 2 data source siap pakai yang diekspor modul:
createCidikaSupabaseSource dan createTokoSepatuSupabaseSource.
Setiap preset siap pakai sudah termasuk: konfigurasi widget, data adapter, dan data source Supabase.
4. Quick Start — Toko Sepatu
Ini adalah contoh paling lengkap. Ikuti step by step dari nol.
Step 1 — Setup Supabase
A. Buat project Supabase di app.supabase.com
B. Jalankan SQL schema berikut di Supabase SQL Editor (Settings → SQL Editor → New Query):
-- Tabel pelanggan
CREATE TABLE public.customers (
id uuid NOT NULL DEFAULT gen_random_uuid(),
name text NOT NULL,
email text,
phone text,
city text,
created_at timestamp with time zone DEFAULT now(),
CONSTRAINT customers_pkey PRIMARY KEY (id)
);
-- Tabel produk
CREATE TABLE public.products (
id uuid NOT NULL DEFAULT gen_random_uuid(),
name text NOT NULL,
brand text NOT NULL DEFAULT 'Generic',
category text NOT NULL DEFAULT 'sneakers',
price_idr integer NOT NULL,
stock integer NOT NULL DEFAULT 0,
is_active boolean NOT NULL DEFAULT true,
created_at timestamp with time zone DEFAULT now(),
CONSTRAINT products_pkey PRIMARY KEY (id)
);
-- Tabel pesanan
CREATE TABLE public.orders (
id uuid NOT NULL DEFAULT gen_random_uuid(),
customer_id uuid NOT NULL,
total_amount integer NOT NULL,
status text NOT NULL DEFAULT 'pending',
created_at timestamp with time zone DEFAULT now(),
CONSTRAINT orders_pkey PRIMARY KEY (id),
CONSTRAINT orders_customer_id_fkey FOREIGN KEY (customer_id)
REFERENCES public.customers(id)
);
-- Tabel detail item pesanan
CREATE TABLE public.order_items (
id bigint GENERATED ALWAYS AS IDENTITY,
order_id uuid NOT NULL,
product_id uuid NOT NULL,
qty integer NOT NULL DEFAULT 1,
price_idr integer NOT NULL,
subtotal integer NOT NULL,
CONSTRAINT order_items_pkey PRIMARY KEY (id),
CONSTRAINT order_items_order_id_fkey FOREIGN KEY (order_id)
REFERENCES public.orders(id),
CONSTRAINT order_items_product_id_fkey FOREIGN KEY (product_id)
REFERENCES public.products(id)
);
-- Aktifkan Realtime (untuk fitur live update)
ALTER PUBLICATION supabase_realtime ADD TABLE public.orders;
ALTER PUBLICATION supabase_realtime ADD TABLE public.products;
ALTER PUBLICATION supabase_realtime ADD TABLE public.customers;C. Isi data contoh (opsional, untuk testing):
File seed data untuk Toko Sepatu tersedia di supabase-setup.sql di dalam
project toko-sepatu-app.
Catatan: folder
examples/tidak ikut saatnpm install(paket npm hanya berisidist/).
D. Aktifkan Realtime di Supabase Dashboard:
Database → Replication → centang tabel orders, products, customers.
Step 2 — Buat Supabase client
// src/lib/supabaseClient.js
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
process.env.REACT_APP_SUPABASE_URL, // CRA
process.env.REACT_APP_SUPABASE_ANON_KEY
// Jika pakai Vite: import.meta.env.VITE_SUPABASE_URL
);# .env (di root project)
REACT_APP_SUPABASE_URL=https://xxxxxx.supabase.co
REACT_APP_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Jika pakai Vite:
# VITE_SUPABASE_URL=https://xxxxxx.supabase.co
# VITE_SUPABASE_ANON_KEY=eyJ...Nilai URL dan ANON KEY didapat dari Supabase Dashboard: Settings → API → Project URL & anon key.
Step 3 — Implementasi halaman Dashboard
// src/pages/Dashboard.jsx
import React, { useMemo } from "react";
import { supabase } from "../lib/supabaseClient";
import {
ReusableDashboardView,
tokoSepatuWidgetConfig,
createTokoSepatuSupabaseSource,
adaptTokoSepatuData,
createEmptyTokoSepatuData,
useReusableDashboard,
} from "@rozaqi02/reusable-dashboard";
// Inisialisasi data source — dibuat sekali di luar komponen
const source = createTokoSepatuSupabaseSource(supabase);
// Label UI dalam Bahasa Indonesia
// Ganti nilai-nilai ini sesuai bahasa/konteks bisnis kamu
const labels = {
title: "Dashboard Toko Saya",
refresh: "Refresh",
liveUpdate: "Live",
loadFailed: "Gagal memuat data.",
retry: "Coba Lagi",
confirmedOnly: "Confirmed",
pendingOnly: "Pending",
allStatus: "Semua Status",
showPendingOverlay: "Tampilkan pending",
allAudience: "Semua",
audienceDomestic: "Domestic",
audienceForeign: "Foreign",
customDate: "Kustom",
reset: "Reset",
topSort: "Urutkan",
sortBookings: "Qty Terjual",
sortRevenue: "Revenue",
sortDesc: "Turun",
sortAsc: "Naik",
confirmedBookings: "Total Pesanan",
confirmedRevenue: "Total Pendapatan",
avgRevenue: "Rata-rata / Pesanan",
conversionRate: "Conversion Rate",
totalProducts: "Total Produk",
dailyTrends: "Tren Harian",
statusDistribution: "Distribusi Status",
topPackages: "Produk Terlaris",
recentBookings: "Pesanan Terbaru",
date: "Tanggal",
customer: "Pelanggan",
package: "Produk",
audience: "Audiens",
total: "Total",
status: "Status",
noRecentBookings: "Belum ada pesanan",
bookingsMetric: "Pesanan",
revenueMetric: "Pendapatan",
pendingMetric: "Pesanan (Pending)",
confirmedBookingMetric: "Pesanan (Confirmed)",
confirmedRevenueMetric: "Pendapatan (Confirmed)",
unknownAudience: "Unknown",
dayLabel: (n) => `${n} hari`,
formatStatusLabel: (s) => ({ confirmed: "Confirmed", pending: "Pending", cancelled: "Cancelled" })[s] || s,
formatAudienceLabel: (v) => v || "Unknown",
};
export default function Dashboard() {
const state = useReusableDashboard({
config: tokoSepatuWidgetConfig,
dataSource: source,
adapter: adaptTokoSepatuData,
createEmptyState: createEmptyTokoSepatuData,
languageCode: "id",
dateLocale: "id-ID",
labels,
});
return (
<ReusableDashboardView
config={tokoSepatuWidgetConfig}
labels={labels}
loading={state.loading}
error={state.error}
filters={state.filters}
onFilterChange={state.updateFilter}
onResetFilters={state.resetFilters}
onRefresh={state.refresh}
data={state.data}
dateLocale="id-ID"
liveUpdateEnabled={state.liveUpdateEnabled}
/>
);
}Step 4 — Jalankan
npm start # Create React App
# atau
npm run dev # ViteBuka browser → dashboard tampil dengan data dari Supabase.
4.5 Cara Cepat dengan createDashboardConfig
Mulai versi 1.1.3, modul menyediakan factory function createDashboardConfig yang
mengemas semua konfigurasi (widgetConfig + dataSource + adapter + labels) menjadi
1 objek tunggal. Ini cara yang lebih ringkas dan direkomendasikan untuk domain baru.
Sebelum (4 parameter terpisah ke hook): Sesudah (1 objek terpadu):
useReusableDashboard({ const cfg = createDashboardConfig({
config: tokoSepatuWidgetConfig, widgetConfig: tokoSepatuWidgetConfig,
dataSource: source, →→→ dataSource: source,
adapter: adaptTokoSepatuData, adapter: adaptTokoSepatuData,
createEmptyState: createEmpty, createEmptyState: createEmpty,
languageCode: "id", languageCode: "id",
dateLocale: "id-ID", dateLocale: "id-ID",
labels, });
}); useReusableDashboard({ ...cfg, labels });Contoh penggunaan lengkap:
import { supabase } from "../lib/supabaseClient";
import {
ReusableDashboardView,
useReusableDashboard,
createDashboardConfig, // ← factory baru
tokoSepatuWidgetConfig,
createTokoSepatuSupabaseSource,
adaptTokoSepatuData,
createEmptyTokoSepatuData,
} from "@rozaqi02/reusable-dashboard";
import { myWidgetConfig } from "./myWidgetConfig";
import { createMySource } from "./myDataSource";
import { adaptMyData, createEmptyMyData } from "./myAdapter";
// Buat di luar komponen — sekali saja
const dashboardConfig = createDashboardConfig({
widgetConfig: myWidgetConfig,
dataSource: createMySource(supabase),
adapter: adaptMyData,
createEmptyState: createEmptyMyData,
languageCode: "id",
dateLocale: "id-ID",
// labels bisa disertakan di sini atau di-override saat render
});
const labels = { title: "Dashboard Saya", /* ... */ };
export default function MyDashboard() {
const state = useReusableDashboard({ ...dashboardConfig, labels });
return (
<ReusableDashboardView
config={dashboardConfig.config}
labels={labels}
loading={state.loading}
error={state.error}
filters={state.filters}
onFilterChange={state.updateFilter}
onResetFilters={state.resetFilters}
onRefresh={state.refresh}
data={state.data}
dateLocale={dashboardConfig.dateLocale}
liveUpdateEnabled={state.liveUpdateEnabled}
supabase={supabase} // aktifkan fitur baca tabel di wizard
dashboardConfig={dashboardConfig} // aktifkan validasi otomatis
/>
);
}Apa yang terjadi jika config belum lengkap?
createDashboardConfig menyimpan info validasi di dashboardConfig._meta:
dashboardConfig._meta = {
isValid: true, // false jika ada bagian yang kurang
missing: [], // ["adapter", "dataSource"] jika belum diisi
hasDataSource: true,
hasRealtimeSupport: true, // false jika subscribeLiveUpdate tidak ada
hasLabels: true,
widgetCount: { stats: 4, charts: 4, tableColumns: 6 },
}Jika ada bagian yang kurang, ReusableDashboardView secara otomatis menampilkan
Setup Wizard (lihat Bab 11).
5. Quick Start — Cidika Travel
Ganti hanya 4 baris dari contoh Toko Sepatu di atas:
import {
ReusableDashboardView,
cidikaWidgetConfig, // ← ganti ini
createCidikaSupabaseSource, // ← ganti ini
adaptCidikaDashboardData, // ← ganti ini
createEmptyDashboardData, // ← ganti ini
createDashboardLabels,
useReusableDashboard,
} from "@rozaqi02/reusable-dashboard";
// Jika pakai react-i18next:
const { t } = useTranslation();
const labels = useMemo(() => createDashboardLabels(t), [t]);
// Jika tidak pakai i18n, gunakan objek labels manual seperti contoh Toko Sepatu
const source = createCidikaSupabaseSource(supabase);
const state = useReusableDashboard({
config: cidikaWidgetConfig,
dataSource: source,
adapter: adaptCidikaDashboardData,
createEmptyState: createEmptyDashboardData,
languageCode: "id",
dateLocale: "id-ID",
labels,
});Tabel Supabase yang dibutuhkan untuk Cidika Travel:
-- Tabel utama
bookings (id, created_at, total_idr, status, package_id, audience, customer_name)
packages (id, slug, price, is_active)
package_locales (package_id, lang, title) -- untuk nama paket multi-bahasa
page_sections (id, page_id, type, order) -- untuk menghitung jumlah section6. Setup Database Supabase
Cara mendapatkan URL dan API Key
- Login ke app.supabase.com
- Buka project kamu
- Klik Settings (ikon gear) di sidebar kiri
- Klik API
- Salin:
- Project URL →
REACT_APP_SUPABASE_URL - anon public key →
REACT_APP_SUPABASE_ANON_KEY
- Project URL →
Mengaktifkan Realtime (wajib untuk live update)
- Di Supabase Dashboard, klik Database di sidebar
- Klik Replication
- Di bagian Source, aktifkan toggle untuk tabel yang ingin di-listen
- Atau jalankan SQL berikut:
-- Untuk Toko Sepatu:
ALTER PUBLICATION supabase_realtime ADD TABLE public.orders;
ALTER PUBLICATION supabase_realtime ADD TABLE public.products;
-- Untuk Cidika Travel:
ALTER PUBLICATION supabase_realtime ADD TABLE public.bookings;
ALTER PUBLICATION supabase_realtime ADD TABLE public.packages;Row Level Security (RLS)
Jika dashboard tidak menampilkan data padahal data sudah ada, kemungkinan RLS (Row Level Security) memblokir query. Solusi sementara untuk development:
-- PERHATIAN: Hanya untuk development. Jangan gunakan di production
-- tanpa menambahkan policy yang benar.
ALTER TABLE public.orders DISABLE ROW LEVEL SECURITY;
ALTER TABLE public.customers DISABLE ROW LEVEL SECURITY;
ALTER TABLE public.products DISABLE ROW LEVEL SECURITY;Untuk production, tambahkan policy yang sesuai:
-- Contoh policy: izinkan semua operasi SELECT untuk role anon
CREATE POLICY "Allow read for anon" ON public.orders
FOR SELECT TO anon USING (true);7. Cara Membuat Adapter untuk Domain Bisnis Baru
Bab ini adalah panduan dari nol untuk client yang domainnya bukan travel atau toko sepatu (misal: laundry, klinik, rental mobil, kursus, dll). Kamu hanya perlu menulis 3 file kecil, tanpa pernah menyentuh kode inti modul.
7.0 Pahami dulu alurnya (WAJIB dibaca)
Modul ini memisahkan 3 tanggung jawab. Memahami peran masing-masing membuat sisanya gampang:
DATABASE Supabase
│
▼
┌───────────────┐ "Ambil data mentah dari Supabase"
│ 1. DATA SOURCE │ → tahu nama tabel & kolom kamu
└───────────────┘ → output: objek { bookings, recent, staticCounts }
│
▼
┌───────────────┐ "Ubah data mentah → format standar dashboard"
│ 2. ADAPTER │ → hitung stats, susun data chart & tabel
└───────────────┘ → output: objek { stats, charts, table }
│
▼
┌───────────────┐ "Tentukan WIDGET apa yang tampil & labelnya"
│ 3. WIDGET │ → kartu mana, chart mana, kolom tabel mana
│ CONFIG │
└───────────────┘
│
▼
useReusableDashboard(...) → <ReusableDashboardView /> (UI jadi)Aturan emas:
- Data Source = satu-satunya bagian yang tahu nama tabel/kolom database kamu.
- Adapter = satu-satunya bagian yang berisi logika hitung (revenue, konversi, dll).
- Widget Config = satu-satunya bagian yang menentukan tampilan (deklaratif, tanpa logika).
Urutan mengerjakan: Data Source → Adapter → Widget Config → rangkai di halaman.
7.1 Kontrak antar-lapisan (referensi cepat)
Output Data Source (yang masuk ke adapter sebagai raw):
{
bookings: [...], // WAJIB. Array semua transaksi dalam rentang tanggal.
recent: [...], // WAJIB. Array 10 transaksi terbaru (untuk tabel).
packageLocales: [], // Opsional. Untuk nama multi-bahasa. Boleh [].
staticCounts: { // Opsional. Angka agregat (mis. jumlah produk).
packages: 0,
sections: 0,
},
}Nama field harus
bookingsdanrecentwalau domainmu bukan booking — ini istilah internal modul. Isi datanya bebas (order, transaksi, reservasi, dll).
Output Adapter (yang dirender oleh komponen):
{
stats: {
bookingsConfirm: number, // angka untuk stat card (key bebas, asal cocok config)
revenueConfirm: number,
// ...tambahkan key lain sesuai kebutuhan
},
charts: {
dailyTrends: [ // area chart tren harian
{ dateKey: "2026-05-01", label: "01 Mei", count: 5, revenue: 2500000, pendingCount: 2 }
],
statusDistribution: [ // pie chart distribusi status
{ status: "confirmed", label: "Confirmed", count: 30 },
],
audienceDistribution: [], // pie chart kedua (boleh kosong [])
topPackages: [ // bar chart terlaris (boleh kosong [])
{ packageId: "uuid", name: "Nama Produk", value: 10 },
],
},
table: {
recentBookings: [ // baris tabel transaksi terbaru
{
id: "uuid",
createdAt: "2026-05-01T10:00:00Z", // ISO string
customerName: "Budi Santoso",
packageName: "Nama Produk",
audienceLabel: "Domestic", // boleh "-"
totalIDR: 500000, // integer Rupiah
status: "confirmed", // lowercase
statusLabel: "Confirmed",
}
],
},
}Penting:
valueKeydi widget config (Bab 8) harus cocok dengan key distats. Contoh: jika config punyavalueKey: "revenueConfirm", adapter wajib mengembalikanstats.revenueConfirm.
7.2 STUDI KASUS: Aplikasi Laundry (dari nol)
Misalkan client punya 1 tabel Supabase bernama laundry_orders:
| Kolom | Tipe | Contoh |
|-------|------|--------|
| id | uuid | a1b2... |
| created_at | timestamptz | 2026-05-01T10:00:00Z |
| customer_name | text | Budi Santoso |
| total_price | integer | 50000 |
| status | text | confirmed / pending |
| service_type | text | Cuci Kering |
Target dashboard: 2 stat card (total order, total pendapatan), 2 chart (tren harian + distribusi status), dan 1 tabel order terbaru.
Langkah 1 — Tulis DATA SOURCE
Buat file src/datasources/laundryDataSource.js. Tugasnya: query Supabase,
kembalikan dalam bentuk { bookings, recent, staticCounts }.
// src/datasources/laundryDataSource.js
export function createLaundrySupabaseSource(supabase) {
return {
// Dipanggil otomatis oleh useReusableDashboard setiap filter berubah.
// Parameter fromISO/toISO/statusScope sudah dihitung oleh modul.
async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
// (a) Semua order dalam rentang tanggal → untuk chart & stats
const { data: orders = [] } = await supabase
.from("laundry_orders")
.select("id, created_at, customer_name, total_price, status, service_type")
.gte("created_at", fromISO)
.lte("created_at", toISO)
.order("created_at", { ascending: true });
// (b) 10 order terbaru → untuk tabel "Order Terbaru"
const recentQuery = supabase
.from("laundry_orders")
.select("id, created_at, customer_name, total_price, status, service_type")
.gte("created_at", fromISO)
.lte("created_at", toISO)
.order("created_at", { ascending: false })
.limit(10);
// Hormati filter status dari UI (confirmed/pending/all)
if (statusScope && statusScope !== "all") {
recentQuery.eq("status", statusScope);
}
const { data: recent = [] } = await recentQuery;
// (c) Angka agregat opsional, mis. jumlah jenis layanan
const { count: serviceCount } = await supabase
.from("services")
.select("*", { count: "exact", head: true });
// Kembalikan SESUAI KONTRAK (nama bookings & recent wajib)
return {
bookings: orders,
recent,
packageLocales: [],
staticCounts: { packages: serviceCount || 0, sections: 0 },
};
},
// OPSIONAL — aktifkan live update realtime. Hapus jika tak perlu.
subscribeLiveUpdate(onEvent) {
const channel = supabase
.channel("laundry-dashboard-live")
.on("postgres_changes",
{ event: "*", schema: "public", table: "laundry_orders" }, onEvent)
.subscribe();
return () => supabase.removeChannel(channel);
},
};
}Tips: kalau kamu tidak butuh realtime, cukup hapus fungsi
subscribeLiveUpdate. Badge "Live" otomatis tidak muncul.
Langkah 2 — Tulis ADAPTER
Buat file src/adapters/laundryAdapter.js. Tugasnya: ubah raw dari data source
menjadi { stats, charts, table }. Pakai helper toNumber & buildDayBuckets
dari modul agar tidak menulis ulang logika tanggal.
// src/adapters/laundryAdapter.js
import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
// (1) State kosong — dipakai modul sebagai initial state & saat error.
// Strukturnya HARUS sama dengan output adaptLaundryData di bawah.
export function createEmptyLaundryData() {
return {
stats: { bookingsConfirm: 0, revenueConfirm: 0 },
charts: { dailyTrends: [], statusDistribution: [], audienceDistribution: [], topPackages: [] },
table: { recentBookings: [] },
};
}
// (2) Fungsi adapter utama.
export function adaptLaundryData({ raw, filters, range, dateLocale, labels }) {
if (!raw) return createEmptyLaundryData();
const orders = raw.bookings || [];
const recent = raw.recent || [];
// Siapkan bucket harian kosong untuk chart tren (1 bucket = 1 hari)
const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
const dayLookup = new Map(dailyBuckets.map((b) => [b.dateKey, b]));
const statusMap = new Map();
let confirmed = 0;
let revenue = 0;
// Loop semua order: hitung total & isi bucket harian
orders.forEach((row) => {
const status = String(row.status || "pending").toLowerCase();
const amount = toNumber(row.total_price); // ← kolom DB kamu
const dayKey = String(row.created_at || "").slice(0, 10);
statusMap.set(status, (statusMap.get(status) || 0) + 1);
const bucket = dayLookup.get(dayKey);
if (bucket && status === "confirmed") {
bucket.count += 1;
bucket.revenue += amount;
}
if (status === "confirmed") { confirmed += 1; revenue += amount; }
});
return {
// stats: key di sini HARUS cocok dengan valueKey di widget config
stats: {
bookingsConfirm: confirmed,
revenueConfirm: revenue,
},
charts: {
dailyTrends: dailyBuckets,
statusDistribution: Array.from(statusMap.entries()).map(([status, count]) => ({
status,
label: labels?.formatStatusLabel?.(status) || status,
count,
})),
audienceDistribution: [], // tidak dipakai laundry → kosong
topPackages: [], // tidak dipakai laundry → kosong
},
table: {
recentBookings: recent.map((row) => {
const status = String(row.status || "pending").toLowerCase();
return {
id: row.id,
createdAt: row.created_at,
customerName: row.customer_name || "-",
packageName: row.service_type || "-", // ← kolom DB kamu
audienceLabel: "-",
totalIDR: toNumber(row.total_price),
status,
statusLabel: labels?.formatStatusLabel?.(status) || status,
};
}),
},
};
}Catatan:
audienceDistributiondantopPackagesboleh[]jika domainmu tidak butuh. Lebih baik lagi: jangan daftarkan chart itu di widget config (Langkah 3), supaya tidak tampil sama sekali.
Langkah 3 — Tulis WIDGET CONFIG
Buat file src/config/laundryConfig.js. Ini murni deklaratif — menentukan
widget apa yang muncul. valueKey harus cocok dengan key stats dari adapter,
dan label adalah key dari objek labels (Langkah 4).
// src/config/laundryConfig.js
export const laundryConfig = {
id: "laundry.dashboard",
defaultFilters: {
statusScope: "confirmed", // filter awal saat dibuka
daysPreset: 30, // tampilkan 30 hari terakhir
sortPkgBy: "bookings",
sortPkgDir: "desc",
},
widgets: {
// 2 stat card → valueKey cocok dgn stats di adapter
stats: [
{ id: "orders", label: "confirmedBookings", icon: "TrendingUp",
valueKey: "bookingsConfirm", format: "number", accentColor: "blue" },
{ id: "revenue", label: "confirmedRevenue", icon: "DollarSign",
valueKey: "revenueConfirm", format: "currency", accentColor: "green" },
],
// 2 chart → hanya yang ada datanya (tren & status)
charts: [
{ id: "trend", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
{ id: "status", type: "statusPie", label: "statusDistribution", icon: "PieChart" },
],
// 1 tabel → kolom mengacu ke field recentBookings dari adapter
table: {
id: "recentOrders",
label: "recentBookings",
icon: "Calendar",
emptyLabel: "noRecentBookings",
columns: [
{ id: "date", label: "date", accessor: "createdAt", type: "date" },
{ id: "customer", label: "customer", accessor: "customerName" },
{ id: "service", label: "package", accessor: "packageName" },
{ id: "total", label: "total", accessor: "totalIDR", type: "currency" },
{ id: "status", label: "status", accessor: "statusLabel",
type: "statusBadge", statusAccessor: "status" },
],
},
},
};Detail tiap field config (tipe kolom, nama ikon, format) ada di Bab 8 — Konfigurasi Widget.
Langkah 4 — Rangkai semuanya di halaman Dashboard
Sekarang gabungkan ke-3 file di atas + objek labels, lalu serahkan ke
useReusableDashboard dan render ReusableDashboardView.
// src/pages/LaundryDashboard.jsx
import React, { useMemo } from "react";
import { supabase } from "../lib/supabaseClient";
import {
ReusableDashboardView,
useReusableDashboard,
} from "@rozaqi02/reusable-dashboard";
// 3 file yang baru kamu buat:
import { laundryConfig } from "../config/laundryConfig";
import { createLaundrySupabaseSource } from "../datasources/laundryDataSource";
import { adaptLaundryData, createEmptyLaundryData } from "../adapters/laundryAdapter";
// Data source dibuat sekali di luar komponen (anti re-render)
const source = createLaundrySupabaseSource(supabase);
// Label UI — semua teks yang tampil. Sesuaikan bahasa/konteks bisnismu.
const labels = {
title: "Dashboard Laundry",
refresh: "Refresh",
liveUpdate: "Live",
loadFailed: "Gagal memuat data.",
retry: "Coba Lagi",
allStatus: "Semua Status",
confirmedOnly: "Selesai",
pendingOnly: "Proses",
reset: "Reset",
confirmedBookings: "Total Order",
confirmedRevenue: "Total Pendapatan",
dailyTrends: "Tren Harian",
statusDistribution: "Distribusi Status",
recentBookings: "Order Terbaru",
noRecentBookings: "Belum ada order",
date: "Tanggal",
customer: "Pelanggan",
package: "Layanan",
total: "Total",
status: "Status",
bookingsMetric: "Order",
revenueMetric: "Pendapatan",
confirmedBookingMetric: "Order (Selesai)",
confirmedRevenueMetric: "Pendapatan (Selesai)",
dayLabel: (n) => `${n} hari`,
formatStatusLabel: (s) =>
({ confirmed: "Selesai", pending: "Proses", cancelled: "Batal" })[s] || s,
formatAudienceLabel: (v) => v || "-",
};
export default function LaundryDashboard() {
const state = useReusableDashboard({
config: laundryConfig, // ← Langkah 3
dataSource: source, // ← Langkah 1
adapter: adaptLaundryData, // ← Langkah 2
createEmptyState: createEmptyLaundryData, // ← Langkah 2
languageCode: "id",
dateLocale: "id-ID",
labels,
});
return (
<ReusableDashboardView
config={laundryConfig}
labels={labels}
loading={state.loading}
error={state.error}
filters={state.filters}
onFilterChange={state.updateFilter}
onResetFilters={state.resetFilters}
onRefresh={state.refresh}
data={state.data}
dateLocale="id-ID"
liveUpdateEnabled={state.liveUpdateEnabled}
/>
);
}Selesai. Buka halaman tersebut → dashboard laundry tampil lengkap dengan data dari Supabase, filter tanggal, search, sort, dan pagination — semua sudah ditangani modul.
7.3 Checklist & troubleshooting domain baru
| Gejala | Penyebab umum | Solusi |
|--------|---------------|--------|
| Stat card menampilkan 0 terus | valueKey di config tidak cocok dengan key stats adapter | Samakan nama, mis. valueKey: "revenueConfirm" ↔ stats.revenueConfirm |
| Chart kosong | Data source tidak mengembalikan bookings, atau status bukan "confirmed" | Pastikan field bernama bookings & status lowercase |
| Tabel kosong padahal ada data | Data source tidak mengembalikan recent | Tambahkan query recent (10 terbaru) |
| Error "is not a function" | Lupa pasang createEmptyState | Wajib kirim createEmptyXxxData ke hook |
| Label tampil sebagai key mentah | Key di config (label) tidak ada di objek labels | Tambahkan key tersebut di objek labels |
| Badge "Live" tidak muncul | subscribeLiveUpdate tidak ada / Realtime belum aktif | Tambahkan fungsi + aktifkan Realtime di Supabase |
Checklist final sebelum rilis ke client:
- [ ] Data source mengembalikan
{ bookings, recent }(nama persis). - [ ] Adapter mengembalikan
{ stats, charts, table }lengkap. - [ ]
createEmptyXxxDatapunya struktur sama dengan output adapter. - [ ] Semua
valueKeydi config ada distats. - [ ] Semua
label/emptyLabeldi config ada di objeklabels. - [ ] RLS Supabase mengizinkan
SELECTuntuk roleanon(lihat Bab 6).
8. Konfigurasi Widget
Widget configuration menentukan apa yang ditampilkan di dashboard. Kamu bisa gunakan preset yang ada atau buat konfigurasi baru.
export const myConfig = {
id: "my.dashboard",
// Filter default saat pertama kali dibuka
defaultFilters: {
statusScope: "confirmed", // "confirmed" | "pending" | "all"
includePendingOverlay: false, // tampilkan overlay pending di chart?
audience: "", // "" | "domestic" | "foreign"
daysPreset: 30, // 7 | 30 | 90 | 0 (0 = custom range)
sortPkgBy: "revenue", // "bookings" | "revenue"
sortPkgDir: "desc", // "desc" | "asc"
},
widgets: {
// Stat cards (1–4 kartu)
stats: [
{
id: "totalOrders",
label: "confirmedBookings", // key dari objek labels
icon: "TrendingUp", // nama ikon Lucide
valueKey: "bookingsConfirm", // key dari data.stats yang dikembalikan adapter
format: "number", // "number" | "currency" | "percent"
accentColor: "blue", // "blue" | "green" | "violet" | "orange" | "sky" | "rose"
},
{
id: "totalRevenue",
label: "confirmedRevenue",
icon: "DollarSign",
valueKey: "revenueConfirm",
format: "currency",
accentColor: "green",
},
],
// Chart cards (1–4 chart)
charts: [
{
id: "trendHarian",
type: "dailyArea", // "dailyArea" | "statusPie" | "audiencePie" | "topPackagesBar"
label: "dailyTrends", // key dari objek labels
icon: "BarChart3",
},
{
id: "distribusiStatus",
type: "statusPie",
label: "statusDistribution",
icon: "PieChart",
},
],
// Tabel (hanya 1)
table: {
id: "recentOrders",
label: "recentBookings", // key dari objek labels
icon: "Calendar",
emptyLabel: "noRecentBookings", // key dari objek labels, untuk empty state
columns: [
{ id: "date", label: "date", accessor: "createdAt", type: "date" },
{ id: "customer", label: "customer", accessor: "customerName" },
{ id: "product", label: "package", accessor: "packageName" },
{ id: "total", label: "total", accessor: "totalIDR", type: "currency" },
{
id: "status", label: "status", accessor: "statusLabel",
type: "statusBadge",
statusAccessor: "status", // kolom yang berisi status lowercase (confirmed/pending/cancelled)
},
],
},
},
};Tipe kolom tabel:
| type | Format Output |
|--------|---------------|
| date | Diformat dengan dateLocale (contoh: "01 Mei 2026") |
| currency | Diformat dengan prefix "Rp" dan titik ribuan |
| statusBadge | Badge berwarna sesuai status |
| (tanpa type) | Ditampilkan apa adanya (string) |
Nama ikon yang tersedia:
TrendingUp, TrendingDown, DollarSign, Users, PieChart, BarChart3,
Calendar, RotateCcw, Search, ChevronLeft, ChevronRight, ArrowUp,
ArrowDown, AlertCircle
9. Label & Internasionalisasi
Tanpa i18n (objek manual)
Buat objek labels dengan semua key yang diperlukan:
const labels = {
// Header
title: "Dashboard",
refresh: "Refresh",
liveUpdate: "Live",
loadFailed: "Gagal memuat data.",
retry: "Coba Lagi",
// Filter
confirmedOnly: "Confirmed",
pendingOnly: "Pending",
allStatus: "Semua Status",
showPendingOverlay: "Tampilkan pending",
allAudience: "Semua",
audienceDomestic: "Domestic",
audienceForeign: "Foreign",
customDate: "Kustom",
reset: "Reset",
topSort: "Urutkan",
sortBookings: "Qty Terjual",
sortRevenue: "Revenue",
sortDesc: "Turun",
sortAsc: "Naik",
// Stat cards (sesuaikan dengan valueKey di config)
confirmedBookings: "Total Pesanan",
confirmedRevenue: "Total Pendapatan",
avgRevenue: "Rata-rata / Pesanan",
conversionRate: "Conversion Rate",
totalProducts: "Total Produk",
// Chart & tabel
dailyTrends: "Tren Harian",
statusDistribution: "Distribusi Status",
audienceDistribution: "Distribusi Audiens",
topPackages: "Produk Terlaris",
recentBookings: "Pesanan Terbaru",
date: "Tanggal",
customer: "Pelanggan",
package: "Produk",
audience: "Audiens",
total: "Total",
status: "Status",
noRecentBookings: "Belum ada pesanan",
// Chart metrics
bookingsMetric: "Pesanan",
revenueMetric: "Pendapatan",
pendingMetric: "Pesanan (Pending)",
confirmedBookingMetric: "Pesanan (Confirmed)",
confirmedRevenueMetric: "Pendapatan (Confirmed)",
unknownAudience: "Unknown",
// Fungsi formatter (wajib ada)
dayLabel: (n) => `${n} hari`,
formatStatusLabel: (status) => ({
confirmed: "Confirmed",
pending: "Pending",
cancelled: "Cancelled",
shipped: "Dikirim",
delivered: "Terkirim",
})[status] || status,
formatAudienceLabel: (value) => value || "Unknown",
};Dengan react-i18next
import { useTranslation } from "react-i18next";
import { createDashboardLabels } from "@rozaqi02/reusable-dashboard";
function Dashboard() {
const { t, i18n } = useTranslation();
const languageCode = (i18n.language || "id").slice(0, 2);
const dateLocale = languageCode === "id" ? "id-ID" : "en-US";
const labels = useMemo(() => createDashboardLabels(t), [t]);
// ...
}
createDashboardLabels(t)menggunakan key i18n dari namespace default. Pastikan file terjemahan kamu memiliki keyadmin.dashboard.*.
10. API Reference Lengkap
Config
| Export | Tipe | Deskripsi |
|--------|------|-----------|
| cidikaWidgetConfig | Object | Config dashboard Cidika Travel |
| tokoSepatuWidgetConfig | Object | Config dashboard Toko Sepatu |
| dummyUmkmWidgetConfig | Object | Config dashboard UMKM generik |
| createDashboardConfig(options) | Function | Factory: kemas semua config jadi 1 objek (v1.1.3+) |
| validateDashboardConfig(cfg) | Function | Validasi config, kembalikan { valid, issues } (v1.1.3+) |
createDashboardConfig(options) — parameter:
| Parameter | Tipe | Wajib | Keterangan |
|-----------|------|-------|------------|
| widgetConfig | Object | ✅ | Konfigurasi widget deklaratif |
| dataSource | Object | ✅ | Hasil createXxxSupabaseSource(supabase) |
| adapter | Function | ✅ | Fungsi adaptXxxData |
| createEmptyState | Function | ✅ | Fungsi createEmptyXxxData |
| labels | Object | — | Objek label UI (bisa di-override saat render) |
| languageCode | string | — | Default "id" |
| dateLocale | string | — | Default "id-ID" |
Data Source
| Export | Parameter | Deskripsi |
|--------|-----------|-----------|
| createCidikaSupabaseSource(supabase) | Supabase client | Data source Cidika Travel |
| createTokoSepatuSupabaseSource(supabase) | Supabase client | Data source Toko Sepatu |
Data Adapter
| Export | Parameter | Deskripsi |
|--------|-----------|-----------|
| adaptCidikaDashboardData({ raw, filters, range, dateLocale, languageCode, labels }) | — | Adapter Cidika |
| createEmptyDashboardData() | — | Empty state Cidika |
| adaptTokoSepatuData({ raw, filters, range, dateLocale, labels }) | — | Adapter Toko Sepatu |
| createEmptyTokoSepatuData() | — | Empty state Toko Sepatu |
| adaptDummyUmkmData({ raw, filters, range, dateLocale, labels }) | — | Adapter UMKM generik |
| createEmptyDummyUmkmData() | — | Empty state UMKM generik |
Hook Utama
const state = useReusableDashboard({
config: object, // Widget configuration
dataSource: object, // Objek dari createXxxSupabaseSource()
adapter: Function, // Fungsi adaptXxxData
createEmptyState: Function, // Fungsi createEmptyXxxData
languageCode: "id" | "en",
dateLocale: "id-ID" | "en-US",
labels: object,
});
// state yang dikembalikan:
state.data // Data dashboard terproses
state.loading // boolean — sedang loading?
state.error // string — pesan error (kosong jika tidak ada)
state.filters // State filter aktif saat ini
state.updateFilter // (field: string, value: any) => void
state.resetFilters // () => void
state.refresh // ({ silent?: boolean }) => void
state.liveUpdateEnabled // boolean — realtime subscription aktif?
state.lastUpdatedAt // Date | null
state.range // { fromISO, toISO, daysWindow }Komponen
| Komponen | Deskripsi |
|----------|-----------|
| ReusableDashboardView | Halaman dashboard lengkap — gunakan ini untuk integrasi cepat |
| SetupWizard | Pop-up wizard panduan konfigurasi interaktif (v1.1.3+) |
| StatCard | Kartu metrik dengan warna aksen |
| ChartCard | Kartu grafik (area/pie/bar) |
| DataTable | Tabel dengan search, sort, pagination |
| FilterPanel | Panel filter |
| SearchBar | Input pencarian |
| DateRangeFilter | Filter rentang tanggal (dropdown picker) |
| Badge | Label status berwarna |
| Button | Tombol |
| Input | Input field |
| Typography | Komponen teks |
| Icon | Ikon (Lucide) |
| SkeletonLoader | Placeholder loading |
| DashboardLayout | Template layout dengan slot sidebar dan content |
| SidebarNavigation | Navigasi sidebar |
| TopbarHeader | Header atas |
Props ReusableDashboardView (lengkap):
| Prop | Tipe | Wajib | Keterangan |
|------|------|-------|------------|
| config | Object | ✅ | Widget configuration |
| labels | Object | ✅ | Objek label UI |
| loading | boolean | — | Status loading data |
| error | string | — | Pesan error |
| filters | Object | — | State filter aktif |
| onFilterChange | Function | ✅ | (field, value) => void |
| onResetFilters | Function | ✅ | () => void |
| onRefresh | Function | ✅ | () => void |
| data | Object | — | Data { stats, charts, table } |
| dateLocale | string | — | Default "id-ID" |
| liveUpdateEnabled | boolean | — | Badge "Live" aktif |
| supabase | Object | — | Supabase client — aktifkan fitur baca tabel di wizard (v1.1.3+) |
| dashboardConfig | Object | — | Hasil createDashboardConfig() — aktifkan validasi otomatis (v1.1.3+) |
Utility
| Function | Deskripsi |
|----------|-----------|
| createDashboardLabels(t) | Buat labels dari fungsi t react-i18next |
| createDefaultFilters(base?) | Buat state filter default |
| resolveDateRange({ daysPreset, dateFrom, dateTo }) | Resolve ke { fromISO, toISO, daysWindow } |
| formatYYYYMMDD(date) | Format Date ke "YYYY-MM-DD" |
| formatIDR(value) | Format angka ke format Rupiah (1.500.000) |
| formatDate(value, locale) | Format ISO date string dengan locale |
| shortId(value, length?) | Potong string (default 8 karakter) |
| toNumber(value) | Konversi ke angka, fallback ke 0 |
| buildDayBuckets(daysWindow, fromISO, dateLocale) | Bangun array bucket harian untuk chart |
| sortMapEntries(map, direction) | Urutkan entri Map berdasarkan nilai |
11. Setup Wizard — Panduan Konfigurasi Interaktif
Mulai versi 1.1.3, modul menyertakan Setup Wizard: pop-up panduan interaktif yang
muncul otomatis saat ReusableDashboardView mendeteksi konfigurasi belum lengkap.
Ini menjawab kebutuhan: "developer baru yang belum pernah pakai modul ini tahu harus mulai dari mana tanpa harus baca README dari awal."
Cara kerja
Wizard aktif ketika salah satu kondisi terpenuhi:
- Komponen
<AutoDashboard>dipanggil tanpa propstabledancolumns(Zero-Config mode, v1.3.0+) dan belum ada konfigurasi tersimpan dilocalStorage. - Prop
dashboardConfig(daricreateDashboardConfig) punya_meta.isValid === false. - Prop
config,labels, ataulabels.formatStatusLabeltidak ditemukan pada<ReusableDashboardView>.
Wizard tidak muncul jika semua konfigurasi sudah valid.
🚀 Terapkan Langsung (Zero-Config / Render Langsung) (v1.3.0+)
Di langkah terakhir wizard (Langkah 4), terdapat tombol "🚀 Terapkan Langsung" di samping tombol "Tutup wizard". Tombol ini ditujukan untuk penggunaan dengan komponen <AutoDashboard> (Zero-Config).
Ketika tombol ini diklik:
- Konfigurasi hasil pemetaan visual langsung diterapkan ke dashboard saat itu juga.
- Konfigurasi disimpan ke browser client via
localStoragedengan keyrdb_wizard_config. - Dashboard akan langsung ter-render tanpa mengharuskan developer menyalin kode konfigurasi ke codebase proyek.
Fitur wizard
Step 0 — Overview:
- Checklist masalah yang ditemukan (mis.
"adapter belum diisi") - Diagram alur konfigurasi: Data Source → Adapter → Widget Config
Step 1 — Data Source:
- Contoh kode
fetchDashboardSnapshotyang siap di-copy - Tombol "Baca tabel Supabase langsung" — membaca daftar tabel dari project
Supabase kamu secara realtime via
information_schema.tables
Step 2 — Adapter:
- Contoh kode
adaptMyDatadenganbuildDayBuckets+toNumber - Penjelasan bahwa key
statsharus cocok denganvalueKeydi widget config
Step 3 — Widget Config & Rangkaian Akhir:
- Contoh kode lengkap dengan
createDashboardConfigdari langkah 1–3
Cara mengaktifkan fitur baca tabel Supabase
Tambahkan prop supabase ke ReusableDashboardView:
<ReusableDashboardView
config={dashboardConfig.config}
labels={labels}
// ... props lain
supabase={supabase} // ← aktifkan fitur baca tabel
dashboardConfig={dashboardConfig} // ← aktifkan validasi otomatis
/>Saat wizard terbuka di Step 1, pengguna bisa klik tombol untuk melihat daftar tabel yang tersedia di project Supabase mereka:
Tabel ditemukan (4):
bookings packages package_locales page_sectionsFitur ini membutuhkan RLS Supabase mengizinkan
SELECTpadainformation_schema.tablesuntuk roleanon, atau fallback ke RPCget_tables. Jika tidak bisa diakses, wizard tetap berfungsi — hanya tombol baca tabel yang menampilkan pesan error.
Menggunakan wizard tanpa createDashboardConfig
Wizard juga bisa dipakai standalone (misalnya saat testing atau demo):
import { SetupWizard } from "@rozaqi02/reusable-dashboard";
// Tampilkan wizard dengan isu kustom
<SetupWizard
issues={["Widget config belum diisi", "Data source tidak ditemukan"]}
supabase={supabase}
onDismiss={() => console.log("wizard ditutup")}
/>Menutup wizard
Klik tombol "Lanjutkan tanpa wizard" (di Step 1) atau "Tutup wizard ✓" (di Step 4).
Wizard tidak muncul lagi selama sesi berlangsung (state wizardDismissed disimpan
di level komponen).
12. Pengembangan & Kontribusi
# Clone repository
git clone https://github.com/rozaqi02/reusable-dashboard-umkm.git
cd reusable-dashboard-umkm
# Install dependencies
npm install
# Build
npm run build # Output ke dist/
# Test
npm test # Jest + React Testing Library
npm test -- --coverage # Lihat code coverageCara Publish Versi Baru ke npm
Modul ini publik di npm: @rozaqi02/reusable-dashboard. Setiap kali kode modul
berubah, kamu wajib publish versi baru agar perubahan sampai ke client.
# 1. Pastikan sudah login (sekali saja per device)
npm login # buka browser untuk autentikasi
npm whoami # verifikasi → harus muncul "rozaqi02"
# 2. Naikkan versi (pilih salah satu sesuai jenis perubahan)
npm version patch # 1.1.2 → 1.1.3 (bug fix / perbaikan kecil)
npm version minor # 1.1.2 → 1.2.0 (fitur baru, tanpa breaking change)
npm version major # 1.1.2 → 2.0.0 (breaking change)
# 3. Publish (build otomatis jalan dulu lewat prepublishOnly)
npm publish # akan meminta OTP (One-Time Password)Catatan OTP: karena akun npm kamu mengaktifkan 2FA,
npm publishakan meminta one-time password. Masukkan kode dari aplikasi authenticator kamu (atau gunakannpm publish --otp=123456). Langkah ini harus dijalankan manual di terminal kamu — tidak bisa diotomasi.
Publish ≠ git push.
npm publishhanya mengunggah isi folderdist/(lihat fieldfilesdipackage.json) ke registry npm. Tidak bergantung pada commit/push git terakhir. Jadi kamu tidak perlu push ke GitHub dulu agar bisa publish — keduanya independen. (Tetap disarankan commit & push agar source code di GitHub sinkron dengan versi yang dirilis.)
Update Modul di Project Client
Setelah versi baru terbit, di setiap project yang memakai modul:
npm install @rozaqi02/reusable-dashboard@latestLalu restart dev server (npm start / npm run dev) agar perubahan termuat.
License
MIT — Ahmad Abror Rozaqi Fatoni
Made with ❤️ for UMKM Indonesia
