@rozaqi02/reusable-dashboard 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -41,6 +41,7 @@ __export(index_exports, {
41
41
  Input: () => Input,
42
42
  ReusableDashboardView: () => ReusableDashboardView,
43
43
  SearchBar: () => SearchBar,
44
+ SetupWizard: () => SetupWizard,
44
45
  SidebarNavigation: () => SidebarNavigation,
45
46
  SkeletonLoader: () => SkeletonLoader,
46
47
  StatCard: () => StatCard,
@@ -52,6 +53,7 @@ __export(index_exports, {
52
53
  buildDayBuckets: () => buildDayBuckets,
53
54
  cidikaWidgetConfig: () => cidikaWidgetConfig,
54
55
  createCidikaSupabaseSource: () => createCidikaSupabaseSource,
56
+ createDashboardConfig: () => createDashboardConfig,
55
57
  createDashboardLabels: () => createDashboardLabels,
56
58
  createDefaultFilters: () => createDefaultFilters,
57
59
  createEmptyDashboardData: () => createEmptyDashboardData,
@@ -69,7 +71,8 @@ __export(index_exports, {
69
71
  toNumber: () => toNumber,
70
72
  tokoSepatuWidgetConfig: () => tokoSepatuWidgetConfig,
71
73
  useRealtimeUpdate: () => useRealtimeUpdate,
72
- useReusableDashboard: () => useReusableDashboard
74
+ useReusableDashboard: () => useReusableDashboard,
75
+ validateDashboardConfig: () => validateDashboardConfig
73
76
  });
74
77
  module.exports = __toCommonJS(index_exports);
75
78
 
@@ -312,6 +315,78 @@ var tokoSepatuWidgetConfig = {
312
315
  }
313
316
  };
314
317
 
318
+ // src/config/createDashboardConfig.js
319
+ function createDashboardConfig({
320
+ widgetConfig,
321
+ dataSource,
322
+ adapter,
323
+ createEmptyState,
324
+ labels,
325
+ languageCode = "id",
326
+ dateLocale = "id-ID"
327
+ }) {
328
+ var _a, _b, _c, _d, _e, _f, _g;
329
+ const missing = [];
330
+ if (!widgetConfig) missing.push("widgetConfig");
331
+ if (!dataSource) missing.push("dataSource");
332
+ if (!adapter) missing.push("adapter");
333
+ if (!createEmptyState) missing.push("createEmptyState");
334
+ return {
335
+ // Properti yang langsung dibaca useReusableDashboard
336
+ config: widgetConfig,
337
+ dataSource,
338
+ adapter,
339
+ createEmptyState,
340
+ languageCode,
341
+ dateLocale,
342
+ labels,
343
+ // Metadata untuk setup wizard & validasi
344
+ _meta: {
345
+ isValid: missing.length === 0,
346
+ missing,
347
+ hasDataSource: Boolean(dataSource == null ? void 0 : dataSource.fetchDashboardSnapshot),
348
+ hasRealtimeSupport: Boolean(dataSource == null ? void 0 : dataSource.subscribeLiveUpdate),
349
+ hasLabels: Boolean(labels),
350
+ widgetCount: {
351
+ stats: ((_b = (_a = widgetConfig == null ? void 0 : widgetConfig.widgets) == null ? void 0 : _a.stats) == null ? void 0 : _b.length) ?? 0,
352
+ charts: ((_d = (_c = widgetConfig == null ? void 0 : widgetConfig.widgets) == null ? void 0 : _c.charts) == null ? void 0 : _d.length) ?? 0,
353
+ tableColumns: ((_g = (_f = (_e = widgetConfig == null ? void 0 : widgetConfig.widgets) == null ? void 0 : _e.table) == null ? void 0 : _f.columns) == null ? void 0 : _g.length) ?? 0
354
+ }
355
+ }
356
+ };
357
+ }
358
+ function validateDashboardConfig(dashboardConfig) {
359
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
360
+ const issues = [];
361
+ if (!dashboardConfig) {
362
+ return { valid: false, issues: ["dashboardConfig tidak ditemukan. Pastikan sudah memanggil createDashboardConfig()."] };
363
+ }
364
+ const meta = dashboardConfig == null ? void 0 : dashboardConfig._meta;
365
+ if (!meta) {
366
+ issues.push("Config tidak dibuat melalui createDashboardConfig(). Gunakan factory function ini untuk validasi otomatis.");
367
+ } else {
368
+ if (meta.missing.length > 0) {
369
+ meta.missing.forEach((m) => issues.push(`Properti wajib belum diisi: "${m}"`));
370
+ }
371
+ if (!meta.hasDataSource) {
372
+ issues.push("dataSource.fetchDashboardSnapshot() tidak ditemukan. Pastikan sudah membuat data source yang benar.");
373
+ }
374
+ }
375
+ if (!((_c = (_b = (_a = dashboardConfig.config) == null ? void 0 : _a.widgets) == null ? void 0 : _b.stats) == null ? void 0 : _c.length)) {
376
+ issues.push("widgetConfig.widgets.stats kosong. Tambahkan minimal 1 stat card.");
377
+ }
378
+ if (!((_f = (_e = (_d = dashboardConfig.config) == null ? void 0 : _d.widgets) == null ? void 0 : _e.charts) == null ? void 0 : _f.length)) {
379
+ issues.push("widgetConfig.widgets.charts kosong. Tambahkan minimal 1 chart.");
380
+ }
381
+ if (!((_j = (_i = (_h = (_g = dashboardConfig.config) == null ? void 0 : _g.widgets) == null ? void 0 : _h.table) == null ? void 0 : _i.columns) == null ? void 0 : _j.length)) {
382
+ issues.push("widgetConfig.widgets.table.columns kosong. Tambahkan minimal 1 kolom tabel.");
383
+ }
384
+ if (!dashboardConfig.labels) {
385
+ issues.push("labels belum diisi. Sediakan objek labels agar teks UI tampil dengan benar.");
386
+ }
387
+ return { valid: issues.length === 0, issues };
388
+ }
389
+
315
390
  // src/utils/formatters.js
316
391
  function formatIDR(value) {
317
392
  try {
@@ -1567,7 +1642,7 @@ function ChartCard({
1567
1642
  className = ""
1568
1643
  }) {
1569
1644
  const content = (() => {
1570
- if (loading) return /* @__PURE__ */ import_react14.default.createElement(SkeletonLoader, { className: "h-64" });
1645
+ if (loading) return /* @__PURE__ */ import_react14.default.createElement(SkeletonLoader, { style: { height: 256 } });
1571
1646
  if (widget.type === "dailyArea") {
1572
1647
  return renderDailyArea(labels, filters, chartData.dailyTrends);
1573
1648
  }
@@ -1580,9 +1655,9 @@ function ChartCard({
1580
1655
  if (widget.type === "topPackagesBar") {
1581
1656
  return renderTopPackages(chartData.topPackages, labels, filters.sortPkgBy);
1582
1657
  }
1583
- return /* @__PURE__ */ import_react14.default.createElement("div", { className: "text-sm text-slate-500" }, "Unsupported chart type.");
1658
+ return /* @__PURE__ */ import_react14.default.createElement("div", { className: "rdb-muted" }, "Unsupported chart type.");
1584
1659
  })();
1585
- return /* @__PURE__ */ import_react14.default.createElement("div", { className: `card p-4 ${className}` }, /* @__PURE__ */ import_react14.default.createElement(
1660
+ return /* @__PURE__ */ import_react14.default.createElement("div", { className: `rdb-card rdb-chart-card ${className}` }, /* @__PURE__ */ import_react14.default.createElement(
1586
1661
  ChartHeader,
1587
1662
  {
1588
1663
  title: labels[widget.label] || widget.label,
@@ -1831,8 +1906,281 @@ DashboardLayout.propTypes = {
1831
1906
  };
1832
1907
 
1833
1908
  // src/presentation/ReusableDashboardView.jsx
1909
+ var import_react20 = __toESM(require("react"), 1);
1910
+ var import_prop_types18 = __toESM(require("prop-types"), 1);
1911
+
1912
+ // src/presentation/SetupWizard.jsx
1834
1913
  var import_react19 = __toESM(require("react"), 1);
1835
1914
  var import_prop_types17 = __toESM(require("prop-types"), 1);
1915
+ function SetupWizard({ issues = [], onDismiss, supabase }) {
1916
+ const [step, setStep] = (0, import_react19.useState)(0);
1917
+ const [tables, setTables] = (0, import_react19.useState)(null);
1918
+ const [loadingTables, setLoadingTables] = (0, import_react19.useState)(false);
1919
+ const [tableError, setTableError] = (0, import_react19.useState)("");
1920
+ const [dismissed, setDismissed] = (0, import_react19.useState)(false);
1921
+ if (dismissed) return null;
1922
+ const steps = [
1923
+ { id: 0, label: "Overview", icon: "\u{1F3E0}" },
1924
+ { id: 1, label: "1. Data Source", icon: "\u{1F5C4}\uFE0F" },
1925
+ { id: 2, label: "2. Adapter", icon: "\u{1F504}" },
1926
+ { id: 3, label: "3. Widget Config", icon: "\u2699\uFE0F" }
1927
+ ];
1928
+ async function handleReadTables() {
1929
+ if (!supabase) {
1930
+ setTableError("Supabase client belum tersambung. Pastikan prop supabase sudah diisi.");
1931
+ return;
1932
+ }
1933
+ setLoadingTables(true);
1934
+ setTableError("");
1935
+ try {
1936
+ const { data, error } = await supabase.from("information_schema.tables").select("table_name").eq("table_schema", "public").eq("table_type", "BASE TABLE").order("table_name");
1937
+ if (error) throw error;
1938
+ setTables((data || []).map((r) => r.table_name));
1939
+ } catch (err) {
1940
+ try {
1941
+ const { data: rpcData } = await supabase.rpc("get_tables");
1942
+ if (rpcData) {
1943
+ setTables(rpcData.map((r) => r.table_name || r));
1944
+ } else {
1945
+ setTableError("Tidak dapat membaca tabel. Pastikan RLS mengizinkan akses atau tambahkan policy SELECT untuk anon.");
1946
+ }
1947
+ } catch {
1948
+ setTableError("Gagal membaca tabel dari Supabase: " + ((err == null ? void 0 : err.message) || "Unknown error"));
1949
+ }
1950
+ } finally {
1951
+ setLoadingTables(false);
1952
+ }
1953
+ }
1954
+ function handleDismiss() {
1955
+ setDismissed(true);
1956
+ if (onDismiss) onDismiss();
1957
+ }
1958
+ return /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-overlay", role: "dialog", "aria-modal": "true", "aria-label": "Dashboard Setup Wizard" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-modal" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-header" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-header-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-logo" }, "\u{1F6E0}\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-h2", style: { margin: 0 } }, "Setup Dashboard"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption" }, "Panduan konfigurasi modul @rozaqi02/reusable-dashboard"))), /* @__PURE__ */ import_react19.default.createElement(
1959
+ "button",
1960
+ {
1961
+ type: "button",
1962
+ className: "rdb-wizard-close",
1963
+ onClick: handleDismiss,
1964
+ "aria-label": "Tutup wizard",
1965
+ title: "Lanjutkan tanpa wizard"
1966
+ },
1967
+ "\u2715"
1968
+ )), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-steps" }, steps.map((s) => /* @__PURE__ */ import_react19.default.createElement(
1969
+ "button",
1970
+ {
1971
+ key: s.id,
1972
+ type: "button",
1973
+ className: `rdb-wizard-step-btn ${step === s.id ? "rdb-wizard-step-active" : ""}`,
1974
+ onClick: () => setStep(s.id)
1975
+ },
1976
+ /* @__PURE__ */ import_react19.default.createElement("span", null, s.icon),
1977
+ /* @__PURE__ */ import_react19.default.createElement("span", null, s.label)
1978
+ ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-body" }, step === 0 && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-section" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-alert" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u26A0\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Dashboard belum terkonfigurasi"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Ditemukan ", issues.length, " masalah yang perlu diselesaikan sebelum dashboard dapat berjalan."))), issues.length > 0 && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issues" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 8 } }, "Masalah yang ditemukan:"), issues.map((issue, i) => /* @__PURE__ */ import_react19.default.createElement("div", { key: i, className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-issue-icon" }, "\u274C"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, issue)))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 12 } }, "Alur konfigurasi yang perlu dilakukan:"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow-steps" }, [
1979
+ { num: "1", label: "Data Source", desc: "Tulis fungsi query ke Supabase" },
1980
+ { num: "\u2192", label: "", desc: "" },
1981
+ { num: "2", label: "Adapter", desc: "Petakan data mentah ke format standar" },
1982
+ { num: "\u2192", label: "", desc: "" },
1983
+ { num: "3", label: "Widget Config", desc: "Definisikan widget yang ditampilkan" }
1984
+ ].map((item, i) => item.num === "\u2192" ? /* @__PURE__ */ import_react19.default.createElement("div", { key: i, className: "rdb-wizard-flow-arrow" }, "\u2192") : /* @__PURE__ */ import_react19.default.createElement("div", { key: i, className: "rdb-wizard-flow-box" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow-num" }, item.num), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow-label" }, item.label), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow-desc" }, item.desc))))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 } }, /* @__PURE__ */ import_react19.default.createElement("button", { type: "button", className: "rdb-btn rdb-btn-secondary rdb-btn-sm", onClick: handleDismiss }, "Lanjutkan tanpa wizard"), /* @__PURE__ */ import_react19.default.createElement("button", { type: "button", className: "rdb-btn rdb-btn-primary rdb-btn-sm", onClick: () => setStep(1) }, "Mulai panduan \u2192"))), step === 1 && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-section" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "1"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Data Source")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 12 } }, "Data Source adalah fungsi yang membaca data dari Supabase. Buat file", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, " src/datasources/myDataSource.js"), " dengan isi berikut:"), /* @__PURE__ */ import_react19.default.createElement("pre", { className: "rdb-wizard-code" }, `// src/datasources/myDataSource.js
1985
+ export function createMySupabaseSource(supabase) {
1986
+ return {
1987
+ async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
1988
+ const { data: orders = [] } = await supabase
1989
+ .from("MY_TABLE") // \u2190 ganti nama tabel kamu
1990
+ .select("id, created_at, total_price, status, customer_name")
1991
+ .gte("created_at", fromISO)
1992
+ .lte("created_at", toISO)
1993
+ .order("created_at", { ascending: true });
1994
+
1995
+ const { data: recent = [] } = await supabase
1996
+ .from("MY_TABLE")
1997
+ .select("id, created_at, customer_name, total_price, status")
1998
+ .order("created_at", { ascending: false })
1999
+ .limit(10);
2000
+
2001
+ return {
2002
+ bookings: orders, // WAJIB: nama harus "bookings"
2003
+ recent, // WAJIB: nama harus "recent"
2004
+ packageLocales: [],
2005
+ staticCounts: { packages: 0, sections: 0 },
2006
+ };
2007
+ },
2008
+
2009
+ // Opsional: live update
2010
+ subscribeLiveUpdate(onEvent) {
2011
+ const ch = supabase.channel("my-dashboard-live")
2012
+ .on("postgres_changes",
2013
+ { event: "*", schema: "public", table: "MY_TABLE" }, onEvent)
2014
+ .subscribe();
2015
+ return () => supabase.removeChannel(ch);
2016
+ },
2017
+ };
2018
+ }`), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-supabase-reader" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 8 } }, "\u{1F50D} Baca tabel dari Supabase project kamu secara langsung:"), /* @__PURE__ */ import_react19.default.createElement(
2019
+ "button",
2020
+ {
2021
+ type: "button",
2022
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2023
+ onClick: handleReadTables,
2024
+ disabled: loadingTables
2025
+ },
2026
+ loadingTables ? "Membaca..." : "Tampilkan daftar tabel Supabase"
2027
+ ), tableError && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-table-error" }, tableError), tables && tables.length > 0 && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-table-list" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginBottom: 6 } }, "Tabel ditemukan (", tables.length, "):"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-table-chips" }, tables.map((t) => /* @__PURE__ */ import_react19.default.createElement("span", { key: t, className: "rdb-wizard-chip" }, t))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 8, color: "var(--rdb-text-muted)" } }, "Ganti ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "MY_TABLE"), " di contoh kode di atas dengan nama tabel yang sesuai.")), !supabase && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-table-error" }, "Tambahkan prop ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "supabase"), " ke", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, " <ReusableDashboardView supabase=", "{supabase}", " />"), " untuk mengaktifkan fitur ini.")), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 } }, /* @__PURE__ */ import_react19.default.createElement("button", { type: "button", className: "rdb-btn rdb-btn-secondary rdb-btn-sm", onClick: () => setStep(0) }, "\u2190 Kembali"), /* @__PURE__ */ import_react19.default.createElement("button", { type: "button", className: "rdb-btn rdb-btn-primary rdb-btn-sm", onClick: () => setStep(2) }, "Lanjut \u2192"))), step === 2 && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-section" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "2"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Data Adapter")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 12 } }, "Adapter mengubah data mentah dari Data Source ke format standar yang dimengerti komponen dashboard. Buat file ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "src/adapters/myAdapter.js"), ":"), /* @__PURE__ */ import_react19.default.createElement("pre", { className: "rdb-wizard-code" }, `// src/adapters/myAdapter.js
2028
+ import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
2029
+
2030
+ export function createEmptyMyData() {
2031
+ return {
2032
+ stats: { bookingsConfirm: 0, revenueConfirm: 0 },
2033
+ charts: {
2034
+ dailyTrends: [],
2035
+ statusDistribution: [],
2036
+ audienceDistribution: [],
2037
+ topPackages: [],
2038
+ },
2039
+ table: { recentBookings: [] },
2040
+ };
2041
+ }
2042
+
2043
+ export function adaptMyData({ raw, range, dateLocale, labels }) {
2044
+ if (!raw) return createEmptyMyData();
2045
+
2046
+ const buckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
2047
+ const dayMap = new Map(buckets.map(b => [b.dateKey, b]));
2048
+ const statusMap = new Map();
2049
+ let confirmed = 0, revenue = 0;
2050
+
2051
+ (raw.bookings || []).forEach(row => {
2052
+ const status = String(row.status || "pending").toLowerCase();
2053
+ const amount = toNumber(row.total_price); // \u2190 sesuaikan nama kolom
2054
+ const dayKey = String(row.created_at || "").slice(0, 10);
2055
+
2056
+ statusMap.set(status, (statusMap.get(status) || 0) + 1);
2057
+ const bucket = dayMap.get(dayKey);
2058
+ if (bucket && status === "confirmed") {
2059
+ bucket.count += 1;
2060
+ bucket.revenue += amount;
2061
+ }
2062
+ if (status === "confirmed") { confirmed++; revenue += amount; }
2063
+ });
2064
+
2065
+ return {
2066
+ stats: { bookingsConfirm: confirmed, revenueConfirm: revenue },
2067
+ charts: {
2068
+ dailyTrends: buckets,
2069
+ statusDistribution: Array.from(statusMap.entries())
2070
+ .map(([status, count]) => ({
2071
+ status,
2072
+ label: labels?.formatStatusLabel?.(status) || status,
2073
+ count,
2074
+ })),
2075
+ audienceDistribution: [],
2076
+ topPackages: [],
2077
+ },
2078
+ table: {
2079
+ recentBookings: (raw.recent || []).map(row => ({
2080
+ id: row.id,
2081
+ createdAt: row.created_at,
2082
+ customerName: row.customer_name || "-",
2083
+ packageName: row.service_type || "-", // \u2190 sesuaikan nama kolom
2084
+ audienceLabel: "-",
2085
+ totalIDR: toNumber(row.total_price),
2086
+ status: String(row.status || "pending").toLowerCase(),
2087
+ statusLabel: labels?.formatStatusLabel?.(row.status) || row.status,
2088
+ })),
2089
+ },
2090
+ };
2091
+ }`), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip" }, "\u{1F4A1} ", /* @__PURE__ */ import_react19.default.createElement("strong", null, "Aturan penting:"), " key di ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "stats"), " (mis. ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "bookingsConfirm"), ") harus cocok dengan ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "valueKey"), " di widget config pada Step 3."), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 } }, /* @__PURE__ */ import_react19.default.createElement("button", { type: "button", className: "rdb-btn rdb-btn-secondary rdb-btn-sm", onClick: () => setStep(1) }, "\u2190 Kembali"), /* @__PURE__ */ import_react19.default.createElement("button", { type: "button", className: "rdb-btn rdb-btn-primary rdb-btn-sm", onClick: () => setStep(3) }, "Lanjut \u2192"))), step === 3 && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-section" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "3"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Widget Config & Rangkaian Akhir")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 12 } }, "Widget Config mendefinisikan tampilan dashboard: kartu mana, chart apa, kolom tabel apa. Gunakan ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "createDashboardConfig()"), " untuk mengemas semuanya jadi 1 objek:"), /* @__PURE__ */ import_react19.default.createElement("pre", { className: "rdb-wizard-code" }, `// src/pages/MyDashboard.jsx
2092
+ import { supabase } from "../lib/supabaseClient";
2093
+ import {
2094
+ ReusableDashboardView,
2095
+ useReusableDashboard,
2096
+ createDashboardConfig,
2097
+ } from "@rozaqi02/reusable-dashboard";
2098
+
2099
+ import { createMySupabaseSource } from "../datasources/myDataSource";
2100
+ import { adaptMyData, createEmptyMyData } from "../adapters/myAdapter";
2101
+
2102
+ // 1. Widget Config \u2014 deklaratif, tentukan apa yang tampil
2103
+ const myWidgetConfig = {
2104
+ id: "my.dashboard",
2105
+ defaultFilters: { statusScope: "confirmed", daysPreset: 30 },
2106
+ widgets: {
2107
+ stats: [
2108
+ { id: "orders", label: "confirmedBookings", icon: "TrendingUp",
2109
+ valueKey: "bookingsConfirm", format: "number", accentColor: "blue" },
2110
+ { id: "revenue", label: "confirmedRevenue", icon: "DollarSign",
2111
+ valueKey: "revenueConfirm", format: "currency", accentColor: "green" },
2112
+ ],
2113
+ charts: [
2114
+ { id: "trend", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
2115
+ { id: "status", type: "statusPie", label: "statusDistribution", icon: "PieChart" },
2116
+ ],
2117
+ table: {
2118
+ id: "recent", label: "recentBookings", icon: "Calendar",
2119
+ emptyLabel: "noRecentBookings",
2120
+ columns: [
2121
+ { id: "date", label: "date", accessor: "createdAt", type: "date" },
2122
+ { id: "customer", label: "customer", accessor: "customerName" },
2123
+ { id: "total", label: "total", accessor: "totalIDR", type: "currency" },
2124
+ { id: "status", label: "status", accessor: "statusLabel",
2125
+ type: "statusBadge", statusAccessor: "status" },
2126
+ ],
2127
+ },
2128
+ },
2129
+ };
2130
+
2131
+ // 2. Kemas semua jadi 1 objek (buat di luar komponen, sekali saja)
2132
+ const dashConfig = createDashboardConfig({
2133
+ widgetConfig: myWidgetConfig,
2134
+ dataSource: createMySupabaseSource(supabase),
2135
+ adapter: adaptMyData,
2136
+ createEmptyState: createEmptyMyData,
2137
+ languageCode: "id",
2138
+ dateLocale: "id-ID",
2139
+ });
2140
+
2141
+ // 3. Labels \u2014 teks UI
2142
+ const labels = { title: "Dashboard Saya", refresh: "Refresh",
2143
+ liveUpdate: "Live", loadFailed: "Gagal memuat.", retry: "Coba Lagi",
2144
+ confirmedBookings: "Total Order", confirmedRevenue: "Total Pendapatan",
2145
+ dailyTrends: "Tren Harian", statusDistribution: "Distribusi Status",
2146
+ recentBookings: "Order Terbaru", noRecentBookings: "Belum ada order",
2147
+ date: "Tanggal", customer: "Pelanggan", total: "Total", status: "Status",
2148
+ bookingsMetric: "Order", revenueMetric: "Pendapatan",
2149
+ confirmedBookingMetric: "Order (Confirmed)",
2150
+ confirmedRevenueMetric: "Pendapatan (Confirmed)",
2151
+ dayLabel: n => n + " hari",
2152
+ formatStatusLabel: s =>
2153
+ ({ confirmed: "Selesai", pending: "Proses", cancelled: "Batal" })[s] || s,
2154
+ formatAudienceLabel: v => v || "-",
2155
+ };
2156
+
2157
+ // 4. Render \u2014 selesai!
2158
+ export default function MyDashboard() {
2159
+ const state = useReusableDashboard({ ...dashConfig, labels });
2160
+ return (
2161
+ <ReusableDashboardView
2162
+ config={dashConfig.config}
2163
+ labels={labels}
2164
+ loading={state.loading}
2165
+ error={state.error}
2166
+ filters={state.filters}
2167
+ onFilterChange={state.updateFilter}
2168
+ onResetFilters={state.resetFilters}
2169
+ onRefresh={state.refresh}
2170
+ data={state.data}
2171
+ dateLocale={dashConfig.dateLocale}
2172
+ liveUpdateEnabled={state.liveUpdateEnabled}
2173
+ />
2174
+ );
2175
+ }`), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip" }, "\u2705 ", /* @__PURE__ */ import_react19.default.createElement("strong", null, "Selesai!"), " Jalankan ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "npm start"), " dan buka halaman dashboard. Wizard ini tidak akan muncul lagi setelah konfigurasi valid."), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 } }, /* @__PURE__ */ import_react19.default.createElement("button", { type: "button", className: "rdb-btn rdb-btn-secondary rdb-btn-sm", onClick: () => setStep(2) }, "\u2190 Kembali"), /* @__PURE__ */ import_react19.default.createElement("button", { type: "button", className: "rdb-btn rdb-btn-primary rdb-btn-sm", onClick: handleDismiss }, "Tutup & lanjutkan \u2713"))))));
2176
+ }
2177
+ SetupWizard.propTypes = {
2178
+ issues: import_prop_types17.default.arrayOf(import_prop_types17.default.string),
2179
+ onDismiss: import_prop_types17.default.func,
2180
+ supabase: import_prop_types17.default.object
2181
+ };
2182
+
2183
+ // src/presentation/ReusableDashboardView.jsx
1836
2184
  function ReusableDashboardView({
1837
2185
  config,
1838
2186
  labels,
@@ -1844,9 +2192,48 @@ function ReusableDashboardView({
1844
2192
  onRefresh,
1845
2193
  data,
1846
2194
  dateLocale,
1847
- liveUpdateEnabled
2195
+ liveUpdateEnabled,
2196
+ supabase,
2197
+ dashboardConfig
1848
2198
  }) {
1849
- return /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-view" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-view-container" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-header-panel" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-header-top" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-header-title-group" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h1" }, labels.title), liveUpdateEnabled ? /* @__PURE__ */ import_react19.default.createElement(Badge, { status: "success" }, labels.liveUpdate) : null), /* @__PURE__ */ import_react19.default.createElement(Button, { variant: "secondary", size: "sm", onClick: () => onRefresh(), title: labels.refresh }, /* @__PURE__ */ import_react19.default.createElement(Icon, { name: "RotateCcw", size: 16 }))), error ? /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-error-banner" }, /* @__PURE__ */ import_react19.default.createElement("span", null, error), /* @__PURE__ */ import_react19.default.createElement(Button, { variant: "secondary", size: "sm", onClick: () => onRefresh() }, labels.retry)) : null, /* @__PURE__ */ import_react19.default.createElement(
2199
+ var _a;
2200
+ const [wizardDismissed, setWizardDismissed] = import_react20.default.useState(false);
2201
+ const configIssues = import_react20.default.useMemo(() => {
2202
+ var _a2, _b, _c, _d, _e, _f, _g;
2203
+ if (wizardDismissed) return [];
2204
+ if (dashboardConfig) {
2205
+ const { issues: issues2 } = validateDashboardConfig(dashboardConfig);
2206
+ return issues2;
2207
+ }
2208
+ const issues = [];
2209
+ if (!config) issues.push("Prop 'config' (widgetConfig) belum diisi.");
2210
+ if (!((_b = (_a2 = config == null ? void 0 : config.widgets) == null ? void 0 : _a2.stats) == null ? void 0 : _b.length)) issues.push("widgetConfig.widgets.stats kosong. Tambahkan minimal 1 stat card.");
2211
+ if (!((_d = (_c = config == null ? void 0 : config.widgets) == null ? void 0 : _c.charts) == null ? void 0 : _d.length)) issues.push("widgetConfig.widgets.charts kosong. Tambahkan minimal 1 chart.");
2212
+ if (!((_g = (_f = (_e = config == null ? void 0 : config.widgets) == null ? void 0 : _e.table) == null ? void 0 : _f.columns) == null ? void 0 : _g.length)) issues.push("widgetConfig.widgets.table.columns kosong.");
2213
+ if (!labels) issues.push("Prop 'labels' belum diisi.");
2214
+ if (!(labels == null ? void 0 : labels.title)) issues.push("labels.title belum diisi.");
2215
+ if (!(labels == null ? void 0 : labels.formatStatusLabel)) issues.push("labels.formatStatusLabel (function) belum diisi.");
2216
+ return issues;
2217
+ }, [config, labels, dashboardConfig, wizardDismissed]);
2218
+ const showWizard = !wizardDismissed && configIssues.length > 0;
2219
+ if (!config || !config.widgets) {
2220
+ return /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-view" }, /* @__PURE__ */ import_react20.default.createElement(
2221
+ SetupWizard,
2222
+ {
2223
+ issues: ["Prop 'config' (widgetConfig) belum diisi atau tidak valid."],
2224
+ onDismiss: () => setWizardDismissed(true),
2225
+ supabase
2226
+ }
2227
+ ));
2228
+ }
2229
+ return /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-view" }, showWizard && /* @__PURE__ */ import_react20.default.createElement(
2230
+ SetupWizard,
2231
+ {
2232
+ issues: configIssues,
2233
+ onDismiss: () => setWizardDismissed(true),
2234
+ supabase
2235
+ }
2236
+ ), /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-view-container" }, /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-header-panel" }, /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-header-top" }, /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-header-title-group" }, /* @__PURE__ */ import_react20.default.createElement("span", { className: "rdb-h1" }, (labels == null ? void 0 : labels.title) ?? "Dashboard"), liveUpdateEnabled ? /* @__PURE__ */ import_react20.default.createElement(Badge, { status: "success" }, (labels == null ? void 0 : labels.liveUpdate) ?? "Live") : null), /* @__PURE__ */ import_react20.default.createElement(Button, { variant: "secondary", size: "sm", onClick: () => onRefresh(), title: (labels == null ? void 0 : labels.refresh) ?? "Refresh" }, /* @__PURE__ */ import_react20.default.createElement(Icon, { name: "RotateCcw", size: 16 }))), error ? /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-error-banner" }, /* @__PURE__ */ import_react20.default.createElement("span", null, error), /* @__PURE__ */ import_react20.default.createElement(Button, { variant: "secondary", size: "sm", onClick: () => onRefresh() }, (labels == null ? void 0 : labels.retry) ?? "Retry")) : null, /* @__PURE__ */ import_react20.default.createElement(
1850
2237
  FilterPanel,
1851
2238
  {
1852
2239
  filters,
@@ -1854,42 +2241,45 @@ function ReusableDashboardView({
1854
2241
  onFilterChange,
1855
2242
  onResetFilters
1856
2243
  }
1857
- )), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-stats-grid" }, loading ? Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ import_react19.default.createElement(SkeletonLoader, { key: i, style: { height: 112 } })) : config.widgets.stats.map((widget) => /* @__PURE__ */ import_react19.default.createElement(
1858
- StatCard,
1859
- {
1860
- key: widget.id,
1861
- label: labels[widget.label] || widget.label,
1862
- value: data.stats[widget.valueKey],
1863
- icon: widget.icon,
1864
- format: widget.format,
1865
- accentColor: widget.accentColor
1866
- }
1867
- ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-charts-grid" }, config.widgets.charts.map((widget) => /* @__PURE__ */ import_react19.default.createElement(
2244
+ )), /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-stats-grid" }, loading ? Array.from({ length: config.widgets.stats.length || 4 }).map((_, i) => /* @__PURE__ */ import_react20.default.createElement(SkeletonLoader, { key: i, style: { height: 112 } })) : config.widgets.stats.map((widget) => {
2245
+ var _a2;
2246
+ return /* @__PURE__ */ import_react20.default.createElement(
2247
+ StatCard,
2248
+ {
2249
+ key: widget.id,
2250
+ label: (labels == null ? void 0 : labels[widget.label]) ?? widget.label,
2251
+ value: ((_a2 = data == null ? void 0 : data.stats) == null ? void 0 : _a2[widget.valueKey]) ?? 0,
2252
+ icon: widget.icon,
2253
+ format: widget.format,
2254
+ accentColor: widget.accentColor
2255
+ }
2256
+ );
2257
+ })), /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-charts-grid" }, config.widgets.charts.map((widget) => /* @__PURE__ */ import_react20.default.createElement(
1868
2258
  ChartCard,
1869
2259
  {
1870
2260
  key: widget.id,
1871
2261
  widget,
1872
- labels,
2262
+ labels: labels ?? {},
1873
2263
  loading,
1874
- filters,
1875
- chartData: data.charts
2264
+ filters: filters ?? {},
2265
+ chartData: (data == null ? void 0 : data.charts) ?? {}
1876
2266
  }
1877
- ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-card", style: { padding: 16 } }, /* @__PURE__ */ import_react19.default.createElement(
2267
+ ))), /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-card", style: { padding: 16 } }, /* @__PURE__ */ import_react20.default.createElement(
1878
2268
  "div",
1879
2269
  {
1880
2270
  style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 12 },
1881
2271
  className: "rdb-h3"
1882
2272
  },
1883
- /* @__PURE__ */ import_react19.default.createElement(Icon, { name: config.widgets.table.icon, size: 18 }),
1884
- labels[config.widgets.table.label] || config.widgets.table.label
1885
- ), loading ? /* @__PURE__ */ import_react19.default.createElement(SkeletonLoader, { style: { height: 192 } }) : /* @__PURE__ */ import_react19.default.createElement(
2273
+ /* @__PURE__ */ import_react20.default.createElement(Icon, { name: config.widgets.table.icon ?? "Calendar", size: 18 }),
2274
+ (labels == null ? void 0 : labels[config.widgets.table.label]) ?? config.widgets.table.label
2275
+ ), loading ? /* @__PURE__ */ import_react20.default.createElement(SkeletonLoader, { style: { height: 192 } }) : /* @__PURE__ */ import_react20.default.createElement(
1886
2276
  DataTable,
1887
2277
  {
1888
2278
  columns: config.widgets.table.columns,
1889
- data: data.table.recentBookings,
1890
- labels,
1891
- dateLocale,
1892
- emptyLabel: labels[config.widgets.table.emptyLabel] || config.widgets.table.emptyLabel,
2279
+ data: ((_a = data == null ? void 0 : data.table) == null ? void 0 : _a.recentBookings) ?? [],
2280
+ labels: labels ?? {},
2281
+ dateLocale: dateLocale ?? "id-ID",
2282
+ emptyLabel: (labels == null ? void 0 : labels[config.widgets.table.emptyLabel]) ?? config.widgets.table.emptyLabel,
1893
2283
  searchable: true,
1894
2284
  sortable: true,
1895
2285
  pageSize: 10
@@ -1897,17 +2287,21 @@ function ReusableDashboardView({
1897
2287
  ))));
1898
2288
  }
1899
2289
  ReusableDashboardView.propTypes = {
1900
- config: import_prop_types17.default.object.isRequired,
1901
- labels: import_prop_types17.default.object.isRequired,
1902
- loading: import_prop_types17.default.bool,
1903
- error: import_prop_types17.default.string,
1904
- filters: import_prop_types17.default.object,
1905
- onFilterChange: import_prop_types17.default.func.isRequired,
1906
- onResetFilters: import_prop_types17.default.func.isRequired,
1907
- onRefresh: import_prop_types17.default.func.isRequired,
1908
- data: import_prop_types17.default.object,
1909
- dateLocale: import_prop_types17.default.string,
1910
- liveUpdateEnabled: import_prop_types17.default.bool
2290
+ config: import_prop_types18.default.object.isRequired,
2291
+ labels: import_prop_types18.default.object.isRequired,
2292
+ loading: import_prop_types18.default.bool,
2293
+ error: import_prop_types18.default.string,
2294
+ filters: import_prop_types18.default.object,
2295
+ onFilterChange: import_prop_types18.default.func.isRequired,
2296
+ onResetFilters: import_prop_types18.default.func.isRequired,
2297
+ onRefresh: import_prop_types18.default.func.isRequired,
2298
+ data: import_prop_types18.default.object,
2299
+ dateLocale: import_prop_types18.default.string,
2300
+ liveUpdateEnabled: import_prop_types18.default.bool,
2301
+ /** Supabase client — aktifkan fitur baca tabel di wizard */
2302
+ supabase: import_prop_types18.default.object,
2303
+ /** Hasil createDashboardConfig() — untuk validasi otomatis wizard */
2304
+ dashboardConfig: import_prop_types18.default.object
1911
2305
  };
1912
2306
 
1913
2307
  // src/utils/labels.js
@@ -2043,6 +2437,7 @@ function createDashboardLabels(t) {
2043
2437
  Input,
2044
2438
  ReusableDashboardView,
2045
2439
  SearchBar,
2440
+ SetupWizard,
2046
2441
  SidebarNavigation,
2047
2442
  SkeletonLoader,
2048
2443
  StatCard,
@@ -2054,6 +2449,7 @@ function createDashboardLabels(t) {
2054
2449
  buildDayBuckets,
2055
2450
  cidikaWidgetConfig,
2056
2451
  createCidikaSupabaseSource,
2452
+ createDashboardConfig,
2057
2453
  createDashboardLabels,
2058
2454
  createDefaultFilters,
2059
2455
  createEmptyDashboardData,
@@ -2071,6 +2467,7 @@ function createDashboardLabels(t) {
2071
2467
  toNumber,
2072
2468
  tokoSepatuWidgetConfig,
2073
2469
  useRealtimeUpdate,
2074
- useReusableDashboard
2470
+ useReusableDashboard,
2471
+ validateDashboardConfig
2075
2472
  });
2076
2473
  //# sourceMappingURL=index.cjs.map