npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rozaqi02/reusable-dashboard

v1.3.4

Published

Reusable dashboard module for admin web apps (UMKM).

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.

version license npm


Daftar Isi

  1. ⚡ Cara Tercepat — <AutoDashboard> (Plug & Play)
  2. Instalasi
  3. Prasyarat Teknis
  4. Preset yang Tersedia
  5. Quick Start — Toko Sepatu (Contoh Lengkap)
  6. Quick Start — Cidika Travel
  7. Setup Database Supabase
  8. Cara Membuat Adapter untuk Domain Bisnis Baru
  9. Konfigurasi Widget
  10. Label & Internasionalisasi
  11. API Reference Lengkap
  12. Setup Wizard — Panduan Konfigurasi Interaktif
  13. Pengembangan & Kontribusi

0. Cara Tercepat — <AutoDashboard> (Plug & Play)

⚠️ PENTING (BACA INI DULU):

  1. 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:

  1. Filter Global (di panel atas): Memfilter data transaksi keseluruhan berdasarkan status transaksi, segmen pelanggan (audiens), dan rentang tanggal.
  2. 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-js

Import 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 saat npm install (paket npm hanya berisi dist/).

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  # Vite

Buka 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 section

6. Setup Database Supabase

Cara mendapatkan URL dan API Key

  1. Login ke app.supabase.com
  2. Buka project kamu
  3. Klik Settings (ikon gear) di sidebar kiri
  4. Klik API
  5. Salin:
    • Project URLREACT_APP_SUPABASE_URL
    • anon public key → REACT_APP_SUPABASE_ANON_KEY

Mengaktifkan Realtime (wajib untuk live update)

  1. Di Supabase Dashboard, klik Database di sidebar
  2. Klik Replication
  3. Di bagian Source, aktifkan toggle untuk tabel yang ingin di-listen
  4. 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 bookings dan recent walau 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: valueKey di widget config (Bab 8) harus cocok dengan key di stats. Contoh: jika config punya valueKey: "revenueConfirm", adapter wajib mengembalikan stats.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: audienceDistribution dan topPackages boleh [] 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.
  • [ ] createEmptyXxxData punya struktur sama dengan output adapter.
  • [ ] Semua valueKey di config ada di stats.
  • [ ] Semua label/emptyLabel di config ada di objek labels.
  • [ ] RLS Supabase mengizinkan SELECT untuk role anon (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 key admin.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 props table dan columns (Zero-Config mode, v1.3.0+) dan belum ada konfigurasi tersimpan di localStorage.
  • Prop dashboardConfig (dari createDashboardConfig) punya _meta.isValid === false.
  • Prop config, labels, atau labels.formatStatusLabel tidak 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:

  1. Konfigurasi hasil pemetaan visual langsung diterapkan ke dashboard saat itu juga.
  2. Konfigurasi disimpan ke browser client via localStorage dengan key rdb_wizard_config.
  3. 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 fetchDashboardSnapshot yang 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 adaptMyData dengan buildDayBuckets + toNumber
  • Penjelasan bahwa key stats harus cocok dengan valueKey di widget config

Step 3 — Widget Config & Rangkaian Akhir:

  • Contoh kode lengkap dengan createDashboardConfig dari 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_sections

Fitur ini membutuhkan RLS Supabase mengizinkan SELECT pada information_schema.tables untuk role anon, atau fallback ke RPC get_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 coverage

Cara 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 publish akan meminta one-time password. Masukkan kode dari aplikasi authenticator kamu (atau gunakan npm publish --otp=123456). Langkah ini harus dijalankan manual di terminal kamu — tidak bisa diotomasi.

Publish ≠ git push. npm publish hanya mengunggah isi folder dist/ (lihat field files di package.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@latest

Lalu restart dev server (npm start / npm run dev) agar perubahan termuat.


License

MIT — Ahmad Abror Rozaqi Fatoni


Made with ❤️ for UMKM Indonesia