@rozaqi02/reusable-dashboard 1.1.2 → 1.1.4

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,713 @@ 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
+ var PRESET_SIGNATURES = {
1916
+ cidika: {
1917
+ name: "Cidika Travel",
1918
+ tables: ["bookings", "packages", "package_locales"]
1919
+ },
1920
+ tokoSepatu: {
1921
+ name: "Toko Sepatu / E-Commerce",
1922
+ tables: ["orders", "products", "customers"]
1923
+ }
1924
+ };
1925
+ function detectPreset(tables) {
1926
+ if (!tables || tables.length === 0) return null;
1927
+ const tSet = new Set(tables);
1928
+ if (PRESET_SIGNATURES.cidika.tables.every((t) => tSet.has(t))) return "cidika";
1929
+ if (PRESET_SIGNATURES.tokoSepatu.tables.every((t) => tSet.has(t))) return "tokoSepatu";
1930
+ return null;
1931
+ }
1932
+ function generateCode(mapping) {
1933
+ const {
1934
+ tableName,
1935
+ colDate,
1936
+ colStatus,
1937
+ colTotal,
1938
+ colCustomer,
1939
+ colItem,
1940
+ dashTitle,
1941
+ confirmedValue,
1942
+ pendingValue
1943
+ } = mapping;
1944
+ const safeTitle = dashTitle || "Dashboard";
1945
+ const confirmed = confirmedValue || "confirmed";
1946
+ const pending = pendingValue || "pending";
1947
+ const safeCustomer = colCustomer || "customer_name";
1948
+ const safeItem = colItem || "-";
1949
+ const colTotalFull = colTotal || "total";
1950
+ const dataSource = `// src/datasources/myDashboardSource.js
1951
+ // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
1952
+ // Tabel: ${tableName}
1953
+
1954
+ export function createMyDashboardSource(supabase) {
1955
+ return {
1956
+ async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
1957
+ const allQuery = supabase
1958
+ .from("${tableName}")
1959
+ .select("id, ${colDate}, ${colStatus}, ${colTotalFull}${colCustomer !== "customer_name" ? `, ${colCustomer}` : ", customer_name"}${colItem !== "-" ? `, ${colItem}` : ""}")
1960
+ .gte("${colDate}", fromISO)
1961
+ .lte("${colDate}", toISO)
1962
+ .order("${colDate}", { ascending: true });
1963
+
1964
+ const recentQuery = supabase
1965
+ .from("${tableName}")
1966
+ .select("id, ${colDate}, ${colStatus}, ${colTotalFull}${colCustomer !== "customer_name" ? `, ${colCustomer}` : ", customer_name"}${colItem !== "-" ? `, ${colItem}` : ""}")
1967
+ .gte("${colDate}", fromISO)
1968
+ .lte("${colDate}", toISO)
1969
+ .order("${colDate}", { ascending: false })
1970
+ .limit(10);
1971
+
1972
+ if (statusScope && statusScope !== "all") {
1973
+ recentQuery.eq("${colStatus}", statusScope);
1974
+ }
1975
+
1976
+ const [allRes, recentRes] = await Promise.all([allQuery, recentQuery]);
1977
+
1978
+ return {
1979
+ bookings: allRes.data || [], // semua transaksi (untuk chart & stats)
1980
+ recent: recentRes.data || [], // 10 terbaru (untuk tabel)
1981
+ packageLocales: [],
1982
+ staticCounts: { packages: 0, sections: 0 },
1983
+ };
1984
+ },
1985
+
1986
+ subscribeLiveUpdate(onEvent) {
1987
+ const ch = supabase
1988
+ .channel("rdb-dashboard-live")
1989
+ .on("postgres_changes", { event: "*", schema: "public", table: "${tableName}" }, onEvent)
1990
+ .subscribe();
1991
+ return () => supabase.removeChannel(ch);
1992
+ },
1993
+ };
1994
+ }`;
1995
+ const adapter = `// src/adapters/myDashboardAdapter.js
1996
+ // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
1997
+ // Mapping kolom: status="${colStatus}", total="${colTotalFull}", tanggal="${colDate}"
1998
+
1999
+ import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
2000
+
2001
+ export function createEmptyMyDashboardData() {
2002
+ return {
2003
+ stats: { bookingsConfirm: 0, revenueConfirm: 0 },
2004
+ charts: { dailyTrends: [], statusDistribution: [],
2005
+ audienceDistribution: [], topPackages: [] },
2006
+ table: { recentBookings: [] },
2007
+ };
2008
+ }
2009
+
2010
+ export function adaptMyDashboardData({ raw, range, dateLocale, labels }) {
2011
+ if (!raw) return createEmptyMyDashboardData();
2012
+
2013
+ const buckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
2014
+ const dayMap = new Map(buckets.map(b => [b.dateKey, b]));
2015
+ const statusMap = new Map();
2016
+ let confirmed = 0, revenue = 0;
2017
+
2018
+ (raw.bookings || []).forEach(row => {
2019
+ const status = String(row["${colStatus}"] || "pending").toLowerCase();
2020
+ const amount = toNumber(row["${colTotalFull}"]);
2021
+ const dayKey = String(row["${colDate}"] || "").slice(0, 10);
2022
+
2023
+ statusMap.set(status, (statusMap.get(status) || 0) + 1);
2024
+ const bucket = dayMap.get(dayKey);
2025
+ if (bucket) {
2026
+ if (status === "${confirmed}") { bucket.count++; bucket.revenue += amount; }
2027
+ if (status === "${pending}") { bucket.pendingCount++; }
2028
+ }
2029
+ if (status === "${confirmed}") { confirmed++; revenue += amount; }
2030
+ });
2031
+
2032
+ const avgRevenue = confirmed > 0 ? Math.round(revenue / confirmed) : 0;
2033
+ const totalTx = (raw.bookings || []).length;
2034
+ const conversionRate = totalTx > 0 ? Math.round((confirmed / totalTx) * 100) : 0;
2035
+
2036
+ return {
2037
+ stats: {
2038
+ bookingsConfirm: confirmed,
2039
+ revenueConfirm: revenue,
2040
+ avgRevenue,
2041
+ conversionRate,
2042
+ },
2043
+ charts: {
2044
+ dailyTrends: buckets,
2045
+ statusDistribution: Array.from(statusMap.entries())
2046
+ .sort((a, b) => b[1] - a[1])
2047
+ .map(([status, count]) => ({
2048
+ status, count,
2049
+ label: labels?.formatStatusLabel?.(status) || status,
2050
+ })),
2051
+ audienceDistribution: [],
2052
+ topPackages: [],
2053
+ },
2054
+ table: {
2055
+ recentBookings: (raw.recent || []).map(row => ({
2056
+ id: row.id,
2057
+ createdAt: row["${colDate}"],
2058
+ customerName: row["${colCustomer}"] || "-",
2059
+ packageName: ${colItem !== "-" ? `row["${colItem}"] || "-"` : '"-"'},
2060
+ audienceLabel: "-",
2061
+ totalIDR: toNumber(row["${colTotalFull}"]),
2062
+ status: String(row["${colStatus}"] || "pending").toLowerCase(),
2063
+ statusLabel: labels?.formatStatusLabel?.(row["${colStatus}"]) || row["${colStatus}"],
2064
+ })),
2065
+ },
2066
+ };
2067
+ }`;
2068
+ const widgetConfig = `// src/config/myDashboardConfig.js
2069
+ // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
2070
+
2071
+ export const myDashboardConfig = {
2072
+ id: "my.custom.dashboard",
2073
+ defaultFilters: {
2074
+ statusScope: "${confirmed}",
2075
+ daysPreset: 30,
2076
+ sortPkgBy: "bookings",
2077
+ sortPkgDir: "desc",
2078
+ },
2079
+ widgets: {
2080
+ stats: [
2081
+ { id: "orders", label: "confirmedBookings", icon: "TrendingUp",
2082
+ valueKey: "bookingsConfirm", format: "number", accentColor: "blue" },
2083
+ { id: "revenue", label: "confirmedRevenue", icon: "DollarSign",
2084
+ valueKey: "revenueConfirm", format: "currency", accentColor: "green" },
2085
+ { id: "avg", label: "avgRevenue", icon: "Users",
2086
+ valueKey: "avgRevenue", format: "currency", accentColor: "violet" },
2087
+ { id: "conversion", label: "conversionRate", icon: "PieChart",
2088
+ valueKey: "conversionRate", format: "percent", accentColor: "orange" },
2089
+ ],
2090
+ charts: [
2091
+ { id: "trend", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
2092
+ { id: "status", type: "statusPie", label: "statusDistribution", icon: "PieChart" },
2093
+ ],
2094
+ table: {
2095
+ id: "recentTx", label: "recentBookings", icon: "Calendar",
2096
+ emptyLabel: "noRecentBookings",
2097
+ columns: [
2098
+ { id: "date", label: "date", accessor: "createdAt", type: "date" },
2099
+ { id: "customer", label: "customer", accessor: "customerName" },${colItem !== "-" ? `
2100
+ { id: "item", label: "package", accessor: "packageName" },` : ""}
2101
+ { id: "total", label: "total", accessor: "totalIDR", type: "currency" },
2102
+ { id: "status", label: "status", accessor: "statusLabel",
2103
+ type: "statusBadge", statusAccessor: "status" },
2104
+ ],
2105
+ },
2106
+ },
2107
+ };`;
2108
+ const dashboard = `// src/pages/admin/Dashboard.jsx
2109
+ // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
2110
+ // Salin file ini ke halaman dashboard kamu.
2111
+
2112
+ import React from "react";
2113
+ import { supabase } from "../../lib/supabaseClient.js";
2114
+ import {
2115
+ ReusableDashboardView,
2116
+ useReusableDashboard,
2117
+ createDashboardConfig,
2118
+ } from "@rozaqi02/reusable-dashboard";
2119
+
2120
+ // Import 3 file yang digenerate wizard
2121
+ import { myDashboardConfig } from "./myDashboardConfig";
2122
+ import { createMyDashboardSource } from "./myDashboardSource";
2123
+ import { adaptMyDashboardData, createEmptyMyDashboardData } from "./myDashboardAdapter";
2124
+
2125
+ // Data source \u2014 dibuat sekali di luar komponen
2126
+ const source = createMyDashboardSource(supabase);
2127
+
2128
+ // Label UI \u2014 sesuaikan teks dengan bahasa/konteks bisnis kamu
2129
+ const labels = {
2130
+ title: "${safeTitle}",
2131
+ refresh: "Refresh",
2132
+ liveUpdate: "Live update",
2133
+ loadFailed: "Gagal memuat data.",
2134
+ retry: "Coba Lagi",
2135
+ confirmedOnly: "Selesai",
2136
+ pendingOnly: "Proses",
2137
+ allStatus: "Semua Status",
2138
+ showPendingOverlay: "Tampilkan pending",
2139
+ reset: "Reset",
2140
+ confirmedBookings: "Total Transaksi",
2141
+ confirmedRevenue: "Total Pendapatan",
2142
+ avgRevenue: "Rata-rata / Transaksi",
2143
+ conversionRate: "Conversion Rate",
2144
+ dailyTrends: "Tren Harian",
2145
+ statusDistribution: "Distribusi Status",
2146
+ recentBookings: "Transaksi Terbaru",
2147
+ noRecentBookings: "Belum ada transaksi",
2148
+ date: "Tanggal",
2149
+ customer: "Pelanggan",
2150
+ package: "Item",
2151
+ total: "Total",
2152
+ status: "Status",
2153
+ bookingsMetric: "Transaksi",
2154
+ revenueMetric: "Pendapatan",
2155
+ confirmedBookingMetric: "Transaksi (Selesai)",
2156
+ confirmedRevenueMetric: "Pendapatan (Selesai)",
2157
+ dayLabel: n => n + " hari",
2158
+ formatStatusLabel: s =>
2159
+ ({ "${confirmed}": "Selesai", "${pending}": "Proses" })[s] || (s || "-"),
2160
+ formatAudienceLabel: v => v || "-",
2161
+ };
2162
+
2163
+ // Kemas semua jadi 1 objek
2164
+ const dashboardConfig = createDashboardConfig({
2165
+ widgetConfig: myDashboardConfig,
2166
+ dataSource: source,
2167
+ adapter: adaptMyDashboardData,
2168
+ createEmptyState: createEmptyMyDashboardData,
2169
+ languageCode: "id",
2170
+ dateLocale: "id-ID",
2171
+ labels,
2172
+ });
2173
+
2174
+ export default function Dashboard() {
2175
+ const state = useReusableDashboard({ ...dashboardConfig, labels });
2176
+ return (
2177
+ <ReusableDashboardView
2178
+ config={dashboardConfig.config}
2179
+ labels={labels}
2180
+ loading={state.loading}
2181
+ error={state.error}
2182
+ filters={state.filters}
2183
+ onFilterChange={state.updateFilter}
2184
+ onResetFilters={state.resetFilters}
2185
+ onRefresh={state.refresh}
2186
+ data={state.data}
2187
+ dateLocale={dashboardConfig.dateLocale}
2188
+ liveUpdateEnabled={state.liveUpdateEnabled}
2189
+ supabase={supabase}
2190
+ dashboardConfig={dashboardConfig}
2191
+ />
2192
+ );
2193
+ }`;
2194
+ return { dataSource, adapter, widgetConfig, dashboard };
2195
+ }
2196
+ function SetupWizard({ issues = [], onDismiss, supabase }) {
2197
+ const [dismissed, setDismissed] = (0, import_react19.useState)(false);
2198
+ const [step, setStep] = (0, import_react19.useState)(0);
2199
+ const [detecting, setDetecting] = (0, import_react19.useState)(true);
2200
+ const [tables, setTables] = (0, import_react19.useState)([]);
2201
+ const [columns, setColumns] = (0, import_react19.useState)({});
2202
+ const [supabaseOk, setSupabaseOk] = (0, import_react19.useState)(false);
2203
+ const [detectionDone, setDetectionDone] = (0, import_react19.useState)(false);
2204
+ const [userHasDashboard, setUserHasDashboard] = (0, import_react19.useState)(null);
2205
+ const [selectedTable, setSelectedTable] = (0, import_react19.useState)("");
2206
+ const [colDate, setColDate] = (0, import_react19.useState)("");
2207
+ const [colStatus, setColStatus] = (0, import_react19.useState)("");
2208
+ const [colTotal, setColTotal] = (0, import_react19.useState)("");
2209
+ const [colCustomer, setColCustomer] = (0, import_react19.useState)("");
2210
+ const [colItem, setColItem] = (0, import_react19.useState)("");
2211
+ const [confirmedVal, setConfirmedVal] = (0, import_react19.useState)("confirmed");
2212
+ const [pendingVal, setPendingVal] = (0, import_react19.useState)("pending");
2213
+ const [dashTitle, setDashTitle] = (0, import_react19.useState)("Dashboard");
2214
+ const [loadingColumns, setLoadingColumns] = (0, import_react19.useState)(false);
2215
+ const [generatedCode, setGeneratedCode] = (0, import_react19.useState)(null);
2216
+ const [activeCodeTab, setActiveCodeTab] = (0, import_react19.useState)("dataSource");
2217
+ const [copied, setCopied] = (0, import_react19.useState)("");
2218
+ (0, import_react19.useEffect)(() => {
2219
+ if (!supabase) {
2220
+ setDetecting(false);
2221
+ setDetectionDone(true);
2222
+ return;
2223
+ }
2224
+ (async () => {
2225
+ try {
2226
+ const { data: rpcData, error: rpcErr } = await supabase.rpc("rdb_get_tables");
2227
+ if (!rpcErr && Array.isArray(rpcData)) {
2228
+ setTables(rpcData.map((r) => typeof r === "string" ? r : r.table_name || r.name || r));
2229
+ setSupabaseOk(true);
2230
+ } else {
2231
+ const { data: schemaData, error: schemaErr } = await supabase.from("information_schema.tables").select("table_name").eq("table_schema", "public").eq("table_type", "BASE TABLE").order("table_name");
2232
+ if (!schemaErr && schemaData) {
2233
+ setTables(schemaData.map((r) => r.table_name));
2234
+ setSupabaseOk(true);
2235
+ } else {
2236
+ const { error: pingErr } = await supabase.from("_rdb_ping_").select("*").limit(1);
2237
+ const connected = !pingErr || pingErr.code === "42P01" || pingErr.code === "PGRST116" || (pingErr.message || "").includes("does not exist");
2238
+ setSupabaseOk(connected);
2239
+ setTables([]);
2240
+ }
2241
+ }
2242
+ } catch {
2243
+ setSupabaseOk(false);
2244
+ } finally {
2245
+ setDetecting(false);
2246
+ setDetectionDone(true);
2247
+ }
2248
+ })();
2249
+ }, [supabase]);
2250
+ const loadColumns = (0, import_react19.useCallback)(async (tbl) => {
2251
+ if (!supabase || !tbl || columns[tbl]) return;
2252
+ setLoadingColumns(true);
2253
+ try {
2254
+ const { data: rpcCols, error: rpcErr } = await supabase.rpc("rdb_get_columns", { p_table: tbl });
2255
+ if (!rpcErr && Array.isArray(rpcCols)) {
2256
+ const colObjs = rpcCols.map(
2257
+ (c) => typeof c === "string" ? { column_name: c, data_type: "unknown" } : { column_name: c.column_name || c.name || c, data_type: c.data_type || "unknown" }
2258
+ );
2259
+ setColumns((prev) => ({ ...prev, [tbl]: colObjs }));
2260
+ autoDetectCols(colObjs);
2261
+ return;
2262
+ }
2263
+ const { data: schemaCols, error: schemaErr } = await supabase.from("information_schema.columns").select("column_name, data_type").eq("table_schema", "public").eq("table_name", tbl).order("ordinal_position");
2264
+ if (!schemaErr && schemaCols) {
2265
+ setColumns((prev) => ({ ...prev, [tbl]: schemaCols }));
2266
+ autoDetectCols(schemaCols);
2267
+ return;
2268
+ }
2269
+ const { data: sampleRow, error: sampleErr } = await supabase.from(tbl).select("*").limit(1);
2270
+ if (!sampleErr && sampleRow && sampleRow.length > 0) {
2271
+ const colObjs = Object.keys(sampleRow[0]).map((k) => ({
2272
+ column_name: k,
2273
+ data_type: typeof sampleRow[0][k]
2274
+ }));
2275
+ setColumns((prev) => ({ ...prev, [tbl]: colObjs }));
2276
+ autoDetectCols(colObjs);
2277
+ return;
2278
+ }
2279
+ setColumns((prev) => ({ ...prev, [tbl]: [] }));
2280
+ } catch {
2281
+ setColumns((prev) => ({ ...prev, [tbl]: [] }));
2282
+ } finally {
2283
+ setLoadingColumns(false);
2284
+ }
2285
+ }, [supabase, columns]);
2286
+ function autoDetectCols(colObjs) {
2287
+ const find = (candidates) => {
2288
+ var _a;
2289
+ return ((_a = colObjs.find((c) => candidates.includes(c.column_name.toLowerCase()))) == null ? void 0 : _a.column_name) || "";
2290
+ };
2291
+ setColDate(find(["created_at", "tanggal", "date", "transaction_date", "order_date", "waktu"]));
2292
+ setColStatus(find(["status", "state", "kondisi", "order_status", "payment_status"]));
2293
+ setColTotal(find(["total", "total_idr", "total_amount", "total_price", "harga_total", "amount", "nominal", "harga", "biaya"]));
2294
+ setColCustomer(find(["customer_name", "nama_pelanggan", "nama", "name", "client_name", "buyer_name", "pelanggan"]));
2295
+ setColItem(find(["service_type", "item_name", "product_name", "package_name", "layanan", "produk", "nama_layanan", "jenis", "nama_produk"]));
2296
+ }
2297
+ function copyCode(text, label) {
2298
+ navigator.clipboard.writeText(text).then(() => {
2299
+ setCopied(label);
2300
+ setTimeout(() => setCopied(""), 2e3);
2301
+ });
2302
+ }
2303
+ function handleDismiss() {
2304
+ setDismissed(true);
2305
+ if (onDismiss) onDismiss();
2306
+ }
2307
+ function handleGenerate() {
2308
+ if (!selectedTable || !colDate || !colStatus || !colTotal) return;
2309
+ const code = generateCode({
2310
+ tableName: selectedTable,
2311
+ colDate,
2312
+ colStatus,
2313
+ colTotal,
2314
+ colCustomer: colCustomer || "customer_name",
2315
+ colItem: colItem || "-",
2316
+ dashTitle,
2317
+ confirmedValue: confirmedVal,
2318
+ pendingValue: pendingVal
2319
+ });
2320
+ setGeneratedCode(code);
2321
+ setStep(3);
2322
+ }
2323
+ if (dismissed) return null;
2324
+ const detectedPreset = detectPreset(tables);
2325
+ const tableColumns = selectedTable ? columns[selectedTable] || [] : [];
2326
+ const colNames = tableColumns.map((c) => c.column_name);
2327
+ const mappingValid = selectedTable && colDate && colStatus && colTotal;
2328
+ const stepLabels = ["Deteksi", "Pilih Tabel", "Mapping Kolom", "Kode Siap Pakai"];
2329
+ return /* @__PURE__ */ import_react19.default.createElement(
2330
+ "div",
2331
+ {
2332
+ className: "rdb-wizard-overlay",
2333
+ role: "dialog",
2334
+ "aria-modal": "true",
2335
+ "aria-label": "Setup Wizard @rozaqi02/reusable-dashboard"
2336
+ },
2337
+ /* @__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" }, detecting ? "Mendeteksi kondisi project\u2026" : supabaseOk ? `Supabase tersambung \xB7 ${tables.length} tabel ditemukan` : "Panduan konfigurasi @rozaqi02/reusable-dashboard"))), /* @__PURE__ */ import_react19.default.createElement(
2338
+ "button",
2339
+ {
2340
+ type: "button",
2341
+ className: "rdb-wizard-close",
2342
+ onClick: handleDismiss,
2343
+ title: "Tutup"
2344
+ },
2345
+ "\u2715"
2346
+ )), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-steps" }, stepLabels.map((label, i) => /* @__PURE__ */ import_react19.default.createElement(
2347
+ "button",
2348
+ {
2349
+ key: i,
2350
+ type: "button",
2351
+ className: `rdb-wizard-step-btn ${step === i ? "rdb-wizard-step-active" : ""}`,
2352
+ onClick: () => {
2353
+ if (i < step || i === step + 1 && step < 2) setStep(i);
2354
+ }
2355
+ },
2356
+ /* @__PURE__ */ import_react19.default.createElement("span", null, step > i ? "\u2705" : i + 1, "."),
2357
+ /* @__PURE__ */ import_react19.default.createElement("span", null, label)
2358
+ ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-body" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-section" }, step === 0 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, issues.length > 0 && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issues", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 6 } }, "Konfigurasi belum lengkap:"), issues.map((iss, i) => /* @__PURE__ */ import_react19.default.createElement("div", { key: i, className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, "\u274C"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, iss)))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 10 } }, "\u{1F50D} Hasil deteksi otomatis:"), detecting ? /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)" } }, "Mendeteksi\u2026") : /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, supabaseOk ? "\u2705" : "\u274C"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Supabase ", supabaseOk ? "tersambung" : "tidak tersambung \u2014 pastikan supabaseClient.js sudah dikonfigurasi dan prop supabase dikirim ke ReusableDashboardView")), supabaseOk && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, tables.length > 0 ? "\u2705" : "\u26A0\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, tables.length > 0 ? `${tables.length} tabel public ditemukan` : "Daftar tabel tidak bisa dibaca otomatis (RLS aktif) \u2014 kamu bisa ketik nama tabel di step berikutnya")), supabaseOk && tables.length > 0 && detectedPreset && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, "\u2705"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Tabel cocok dengan preset ", /* @__PURE__ */ import_react19.default.createElement("strong", null, PRESET_SIGNATURES[detectedPreset].name))))), !supabaseOk && detectionDone && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-alert", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u2139\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Aktifkan koneksi Supabase"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Tambahkan prop ke ReusableDashboardView:", " ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "supabase=", "{supabase}")))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ import_react19.default.createElement(
2359
+ "button",
2360
+ {
2361
+ type: "button",
2362
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2363
+ onClick: handleDismiss
2364
+ },
2365
+ "Lanjutkan tanpa wizard"
2366
+ ), supabaseOk && /* @__PURE__ */ import_react19.default.createElement(
2367
+ "button",
2368
+ {
2369
+ type: "button",
2370
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2371
+ onClick: () => setStep(1)
2372
+ },
2373
+ "Lanjut \u2192 Pilih Tabel"
2374
+ ))), step === 1 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__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 } }, "Pilih Tabel Transaksi")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 16 } }, "Pilih tabel yang berisi data transaksi bisnis kamu."), tables.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 8 } }, "Tabel yang ditemukan:"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginBottom: 16 } }, tables.map((tbl) => /* @__PURE__ */ import_react19.default.createElement(
2375
+ "button",
2376
+ {
2377
+ key: tbl,
2378
+ type: "button",
2379
+ onClick: () => {
2380
+ setSelectedTable(tbl);
2381
+ loadColumns(tbl);
2382
+ },
2383
+ className: `rdb-btn rdb-btn-sm ${selectedTable === tbl ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2384
+ style: { justifyContent: "flex-start", fontFamily: "monospace" }
2385
+ },
2386
+ selectedTable === tbl ? "\u2705 " : "\u25CB ",
2387
+ tbl
2388
+ )))), tables.length === 0 && /* @__PURE__ */ import_react19.default.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-alert", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u2139\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Daftar tabel tidak bisa terbaca otomatis"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Ini normal \u2014 Supabase memblokir akses ke ", /* @__PURE__ */ import_react19.default.createElement("code", null, "information_schema"), " via API publik. Ketik nama tabel secara manual di bawah."))), /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Nama tabel transaksi kamu"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react19.default.createElement(
2389
+ "input",
2390
+ {
2391
+ className: "rdb-input",
2392
+ value: selectedTable,
2393
+ onChange: (e) => setSelectedTable(e.target.value),
2394
+ placeholder: "cth: orders, transaksi, bookings, penjualan",
2395
+ style: { maxWidth: 320 }
2396
+ }
2397
+ ), /* @__PURE__ */ import_react19.default.createElement(
2398
+ "button",
2399
+ {
2400
+ type: "button",
2401
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2402
+ disabled: !selectedTable || loadingColumns,
2403
+ onClick: () => loadColumns(selectedTable)
2404
+ },
2405
+ loadingColumns ? "Membaca\u2026" : "Baca kolom"
2406
+ ))), tables.length > 0 && !selectedTable && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { color: "var(--rdb-text-muted)", marginBottom: 12 } }, "Atau ketik nama tabel secara manual:", /* @__PURE__ */ import_react19.default.createElement(
2407
+ "input",
2408
+ {
2409
+ className: "rdb-input",
2410
+ style: { marginTop: 6, maxWidth: 280 },
2411
+ placeholder: "nama tabel lain",
2412
+ onChange: (e) => {
2413
+ setSelectedTable(e.target.value);
2414
+ if (e.target.value) loadColumns(e.target.value);
2415
+ }
2416
+ }
2417
+ )), selectedTable && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip", style: { marginBottom: 12 } }, "Tabel ", /* @__PURE__ */ import_react19.default.createElement("strong", null, selectedTable), " dipilih.", loadingColumns ? " Membaca kolom\u2026" : tableColumns.length > 0 ? ` ${tableColumns.length} kolom ditemukan \u2014 kolom sudah ter-auto-detect di step berikutnya.` : tableColumns.length === 0 && !loadingColumns && columns[selectedTable] !== void 0 ? " Tabel kosong atau tidak ada kolom terbaca \u2014 kamu bisa ketik nama kolom manual di step berikutnya." : ""), /* @__PURE__ */ import_react19.default.createElement("div", { style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Judul dashboard (opsional)"), /* @__PURE__ */ import_react19.default.createElement(
2418
+ "input",
2419
+ {
2420
+ className: "rdb-input",
2421
+ value: dashTitle,
2422
+ style: { maxWidth: 280 },
2423
+ onChange: (e) => setDashTitle(e.target.value),
2424
+ placeholder: "cth: Dashboard Laundry Bersih"
2425
+ }
2426
+ )), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ import_react19.default.createElement(
2427
+ "button",
2428
+ {
2429
+ type: "button",
2430
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2431
+ onClick: () => setStep(0)
2432
+ },
2433
+ "\u2190 Kembali"
2434
+ ), /* @__PURE__ */ import_react19.default.createElement(
2435
+ "button",
2436
+ {
2437
+ type: "button",
2438
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2439
+ disabled: !selectedTable || loadingColumns,
2440
+ onClick: () => setStep(2)
2441
+ },
2442
+ "Lanjut \u2192 Mapping Kolom"
2443
+ ))), step === 2 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__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 } }, "Mapping Kolom \u2014 Tabel: ", /* @__PURE__ */ import_react19.default.createElement("code", { style: { fontSize: "0.9rem" } }, selectedTable))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 16 } }, "Pilih kolom yang berperan sebagai masing-masing data. Kolom sudah ter-deteksi otomatis \u2014 cukup verifikasi dan sesuaikan jika perlu."), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Tanggal ", /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "red" } }, "*")), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2444
+ "select",
2445
+ {
2446
+ className: "rdb-select",
2447
+ value: colDate,
2448
+ onChange: (e) => setColDate(e.target.value)
2449
+ },
2450
+ /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 pilih kolom \u2014"),
2451
+ colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2452
+ ) : /* @__PURE__ */ import_react19.default.createElement(
2453
+ "input",
2454
+ {
2455
+ className: "rdb-input",
2456
+ value: colDate,
2457
+ onChange: (e) => setColDate(e.target.value),
2458
+ placeholder: "cth: created_at"
2459
+ }
2460
+ ), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 2 } }, "Tipe timestamp/date")), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Status ", /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "red" } }, "*")), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2461
+ "select",
2462
+ {
2463
+ className: "rdb-select",
2464
+ value: colStatus,
2465
+ onChange: (e) => setColStatus(e.target.value)
2466
+ },
2467
+ /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 pilih kolom \u2014"),
2468
+ colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2469
+ ) : /* @__PURE__ */ import_react19.default.createElement(
2470
+ "input",
2471
+ {
2472
+ className: "rdb-input",
2473
+ value: colStatus,
2474
+ onChange: (e) => setColStatus(e.target.value),
2475
+ placeholder: "cth: status"
2476
+ }
2477
+ ), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 2 } }, "Nilai selesai/pending")), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Total (uang) ", /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "red" } }, "*")), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2478
+ "select",
2479
+ {
2480
+ className: "rdb-select",
2481
+ value: colTotal,
2482
+ onChange: (e) => setColTotal(e.target.value)
2483
+ },
2484
+ /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 pilih kolom \u2014"),
2485
+ colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2486
+ ) : /* @__PURE__ */ import_react19.default.createElement(
2487
+ "input",
2488
+ {
2489
+ className: "rdb-input",
2490
+ value: colTotal,
2491
+ onChange: (e) => setColTotal(e.target.value),
2492
+ placeholder: "cth: total_amount"
2493
+ }
2494
+ ), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 2 } }, "Integer Rupiah")), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Nama Pelanggan"), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2495
+ "select",
2496
+ {
2497
+ className: "rdb-select",
2498
+ value: colCustomer,
2499
+ onChange: (e) => setColCustomer(e.target.value)
2500
+ },
2501
+ /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 opsional \u2014"),
2502
+ colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2503
+ ) : /* @__PURE__ */ import_react19.default.createElement(
2504
+ "input",
2505
+ {
2506
+ className: "rdb-input",
2507
+ value: colCustomer,
2508
+ onChange: (e) => setColCustomer(e.target.value),
2509
+ placeholder: "cth: customer_name (opsional)"
2510
+ }
2511
+ )), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Nama Item/Layanan"), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2512
+ "select",
2513
+ {
2514
+ className: "rdb-select",
2515
+ value: colItem,
2516
+ onChange: (e) => setColItem(e.target.value)
2517
+ },
2518
+ /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 opsional \u2014"),
2519
+ colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2520
+ ) : /* @__PURE__ */ import_react19.default.createElement(
2521
+ "input",
2522
+ {
2523
+ className: "rdb-input",
2524
+ value: colItem,
2525
+ onChange: (e) => setColItem(e.target.value),
2526
+ placeholder: "cth: service_type (opsional)"
2527
+ }
2528
+ ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow", style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 8 } }, "Nilai status di tabel kamu:"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, 'Nilai "Selesai/Confirmed"'), /* @__PURE__ */ import_react19.default.createElement(
2529
+ "input",
2530
+ {
2531
+ className: "rdb-input",
2532
+ value: confirmedVal,
2533
+ onChange: (e) => setConfirmedVal(e.target.value),
2534
+ placeholder: "cth: confirmed, selesai, lunas"
2535
+ }
2536
+ )), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, 'Nilai "Pending/Proses"'), /* @__PURE__ */ import_react19.default.createElement(
2537
+ "input",
2538
+ {
2539
+ className: "rdb-input",
2540
+ value: pendingVal,
2541
+ onChange: (e) => setPendingVal(e.target.value),
2542
+ placeholder: "cth: pending, proses, menunggu"
2543
+ }
2544
+ )))), !mappingValid && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-error-banner", style: { marginBottom: 12 } }, "Pilih minimal kolom Tanggal, Status, dan Total untuk generate kode."), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ import_react19.default.createElement(
2545
+ "button",
2546
+ {
2547
+ type: "button",
2548
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2549
+ onClick: () => setStep(1)
2550
+ },
2551
+ "\u2190 Kembali"
2552
+ ), /* @__PURE__ */ import_react19.default.createElement(
2553
+ "button",
2554
+ {
2555
+ type: "button",
2556
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2557
+ disabled: !mappingValid,
2558
+ onClick: handleGenerate
2559
+ },
2560
+ "\u2728 Generate Kode \u2192"
2561
+ ))), step === 3 && generatedCode && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Kode Siap Pakai")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip", style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("strong", null, "4 file sudah digenerate."), " Salin masing-masing ke project kamu. Tidak perlu memahami logika di dalamnya \u2014 cukup save dan jalankan."), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issues", style: { marginBottom: 16 } }, [
2562
+ ["myDashboardSource.js", "src/datasources/", "File koneksi ke Supabase"],
2563
+ ["myDashboardAdapter.js", "src/adapters/", "Transformasi data \u2192 format dashboard"],
2564
+ ["myDashboardConfig.js", "src/config/ (atau langsung di Dashboard.jsx)", "Konfigurasi widget"],
2565
+ ["Dashboard.jsx", "src/pages/admin/", "Ganti halaman dashboard lama"]
2566
+ ].map(([file, path, desc], i) => /* @__PURE__ */ import_react19.default.createElement("div", { key: i, className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700 } }, i + 1, "."), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Simpan ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, file), " ke", " ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, path), " \u2014 ", desc))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700 } }, "5."), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Import CSS di ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "src/index.css"), ":", " ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, '@import "../node_modules/@rozaqi02/reusable-dashboard/dist/index.css";')))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 4, marginBottom: 8, flexWrap: "wrap" } }, [
2567
+ ["dataSource", "myDashboardSource.js"],
2568
+ ["adapter", "myDashboardAdapter.js"],
2569
+ ["widgetConfig", "myDashboardConfig.js"],
2570
+ ["dashboard", "Dashboard.jsx"]
2571
+ ].map(([key, label]) => /* @__PURE__ */ import_react19.default.createElement(
2572
+ "button",
2573
+ {
2574
+ key,
2575
+ type: "button",
2576
+ onClick: () => setActiveCodeTab(key),
2577
+ className: `rdb-btn rdb-btn-sm ${activeCodeTab === key ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2578
+ style: { fontFamily: "monospace", fontSize: "0.75rem" }
2579
+ },
2580
+ label
2581
+ ))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ import_react19.default.createElement(
2582
+ "button",
2583
+ {
2584
+ type: "button",
2585
+ onClick: () => copyCode(generatedCode[activeCodeTab], activeCodeTab),
2586
+ className: "rdb-btn rdb-btn-sm rdb-btn-secondary",
2587
+ style: { position: "absolute", top: 8, right: 8, zIndex: 1 }
2588
+ },
2589
+ copied === activeCodeTab ? "\u2705 Disalin!" : "\u{1F4CB} Salin"
2590
+ ), /* @__PURE__ */ import_react19.default.createElement("pre", { className: "rdb-wizard-code", style: { maxHeight: 320, overflow: "auto" } }, generatedCode[activeCodeTab])), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip", style: { marginTop: 12 } }, "Setelah semua file tersimpan dan dev server direstart, wizard tidak akan muncul lagi."), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 12 } }, /* @__PURE__ */ import_react19.default.createElement(
2591
+ "button",
2592
+ {
2593
+ type: "button",
2594
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2595
+ onClick: () => setStep(2)
2596
+ },
2597
+ "\u2190 Edit mapping"
2598
+ ), /* @__PURE__ */ import_react19.default.createElement(
2599
+ "button",
2600
+ {
2601
+ type: "button",
2602
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2603
+ onClick: handleDismiss
2604
+ },
2605
+ "Tutup wizard \u2713"
2606
+ ))))))
2607
+ );
2608
+ }
2609
+ SetupWizard.propTypes = {
2610
+ issues: import_prop_types17.default.arrayOf(import_prop_types17.default.string),
2611
+ onDismiss: import_prop_types17.default.func,
2612
+ supabase: import_prop_types17.default.object
2613
+ };
2614
+
2615
+ // src/presentation/ReusableDashboardView.jsx
1836
2616
  function ReusableDashboardView({
1837
2617
  config,
1838
2618
  labels,
@@ -1844,9 +2624,48 @@ function ReusableDashboardView({
1844
2624
  onRefresh,
1845
2625
  data,
1846
2626
  dateLocale,
1847
- liveUpdateEnabled
2627
+ liveUpdateEnabled,
2628
+ supabase,
2629
+ dashboardConfig
1848
2630
  }) {
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(
2631
+ var _a;
2632
+ const [wizardDismissed, setWizardDismissed] = import_react20.default.useState(false);
2633
+ const configIssues = import_react20.default.useMemo(() => {
2634
+ var _a2, _b, _c, _d, _e, _f, _g;
2635
+ if (wizardDismissed) return [];
2636
+ if (dashboardConfig) {
2637
+ const { issues: issues2 } = validateDashboardConfig(dashboardConfig);
2638
+ return issues2;
2639
+ }
2640
+ const issues = [];
2641
+ if (!config) issues.push("Prop 'config' (widgetConfig) belum diisi.");
2642
+ 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.");
2643
+ 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.");
2644
+ 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.");
2645
+ if (!labels) issues.push("Prop 'labels' belum diisi.");
2646
+ if (!(labels == null ? void 0 : labels.title)) issues.push("labels.title belum diisi.");
2647
+ if (!(labels == null ? void 0 : labels.formatStatusLabel)) issues.push("labels.formatStatusLabel (function) belum diisi.");
2648
+ return issues;
2649
+ }, [config, labels, dashboardConfig, wizardDismissed]);
2650
+ const showWizard = !wizardDismissed && configIssues.length > 0;
2651
+ if (!config || !config.widgets) {
2652
+ return /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-view" }, /* @__PURE__ */ import_react20.default.createElement(
2653
+ SetupWizard,
2654
+ {
2655
+ issues: ["Prop 'config' (widgetConfig) belum diisi atau tidak valid."],
2656
+ onDismiss: () => setWizardDismissed(true),
2657
+ supabase
2658
+ }
2659
+ ));
2660
+ }
2661
+ return /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-view" }, showWizard && /* @__PURE__ */ import_react20.default.createElement(
2662
+ SetupWizard,
2663
+ {
2664
+ issues: configIssues,
2665
+ onDismiss: () => setWizardDismissed(true),
2666
+ supabase
2667
+ }
2668
+ ), /* @__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
2669
  FilterPanel,
1851
2670
  {
1852
2671
  filters,
@@ -1854,42 +2673,45 @@ function ReusableDashboardView({
1854
2673
  onFilterChange,
1855
2674
  onResetFilters
1856
2675
  }
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(
2676
+ )), /* @__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) => {
2677
+ var _a2;
2678
+ return /* @__PURE__ */ import_react20.default.createElement(
2679
+ StatCard,
2680
+ {
2681
+ key: widget.id,
2682
+ label: (labels == null ? void 0 : labels[widget.label]) ?? widget.label,
2683
+ value: ((_a2 = data == null ? void 0 : data.stats) == null ? void 0 : _a2[widget.valueKey]) ?? 0,
2684
+ icon: widget.icon,
2685
+ format: widget.format,
2686
+ accentColor: widget.accentColor
2687
+ }
2688
+ );
2689
+ })), /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-charts-grid" }, config.widgets.charts.map((widget) => /* @__PURE__ */ import_react20.default.createElement(
1868
2690
  ChartCard,
1869
2691
  {
1870
2692
  key: widget.id,
1871
2693
  widget,
1872
- labels,
2694
+ labels: labels ?? {},
1873
2695
  loading,
1874
- filters,
1875
- chartData: data.charts
2696
+ filters: filters ?? {},
2697
+ chartData: (data == null ? void 0 : data.charts) ?? {}
1876
2698
  }
1877
- ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-card", style: { padding: 16 } }, /* @__PURE__ */ import_react19.default.createElement(
2699
+ ))), /* @__PURE__ */ import_react20.default.createElement("div", { className: "rdb-card", style: { padding: 16 } }, /* @__PURE__ */ import_react20.default.createElement(
1878
2700
  "div",
1879
2701
  {
1880
2702
  style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 12 },
1881
2703
  className: "rdb-h3"
1882
2704
  },
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(
2705
+ /* @__PURE__ */ import_react20.default.createElement(Icon, { name: config.widgets.table.icon ?? "Calendar", size: 18 }),
2706
+ (labels == null ? void 0 : labels[config.widgets.table.label]) ?? config.widgets.table.label
2707
+ ), loading ? /* @__PURE__ */ import_react20.default.createElement(SkeletonLoader, { style: { height: 192 } }) : /* @__PURE__ */ import_react20.default.createElement(
1886
2708
  DataTable,
1887
2709
  {
1888
2710
  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,
2711
+ data: ((_a = data == null ? void 0 : data.table) == null ? void 0 : _a.recentBookings) ?? [],
2712
+ labels: labels ?? {},
2713
+ dateLocale: dateLocale ?? "id-ID",
2714
+ emptyLabel: (labels == null ? void 0 : labels[config.widgets.table.emptyLabel]) ?? config.widgets.table.emptyLabel,
1893
2715
  searchable: true,
1894
2716
  sortable: true,
1895
2717
  pageSize: 10
@@ -1897,17 +2719,21 @@ function ReusableDashboardView({
1897
2719
  ))));
1898
2720
  }
1899
2721
  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
2722
+ config: import_prop_types18.default.object.isRequired,
2723
+ labels: import_prop_types18.default.object.isRequired,
2724
+ loading: import_prop_types18.default.bool,
2725
+ error: import_prop_types18.default.string,
2726
+ filters: import_prop_types18.default.object,
2727
+ onFilterChange: import_prop_types18.default.func.isRequired,
2728
+ onResetFilters: import_prop_types18.default.func.isRequired,
2729
+ onRefresh: import_prop_types18.default.func.isRequired,
2730
+ data: import_prop_types18.default.object,
2731
+ dateLocale: import_prop_types18.default.string,
2732
+ liveUpdateEnabled: import_prop_types18.default.bool,
2733
+ /** Supabase client — aktifkan fitur baca tabel di wizard */
2734
+ supabase: import_prop_types18.default.object,
2735
+ /** Hasil createDashboardConfig() — untuk validasi otomatis wizard */
2736
+ dashboardConfig: import_prop_types18.default.object
1911
2737
  };
1912
2738
 
1913
2739
  // src/utils/labels.js
@@ -2043,6 +2869,7 @@ function createDashboardLabels(t) {
2043
2869
  Input,
2044
2870
  ReusableDashboardView,
2045
2871
  SearchBar,
2872
+ SetupWizard,
2046
2873
  SidebarNavigation,
2047
2874
  SkeletonLoader,
2048
2875
  StatCard,
@@ -2054,6 +2881,7 @@ function createDashboardLabels(t) {
2054
2881
  buildDayBuckets,
2055
2882
  cidikaWidgetConfig,
2056
2883
  createCidikaSupabaseSource,
2884
+ createDashboardConfig,
2057
2885
  createDashboardLabels,
2058
2886
  createDefaultFilters,
2059
2887
  createEmptyDashboardData,
@@ -2071,6 +2899,7 @@ function createDashboardLabels(t) {
2071
2899
  toNumber,
2072
2900
  tokoSepatuWidgetConfig,
2073
2901
  useRealtimeUpdate,
2074
- useReusableDashboard
2902
+ useReusableDashboard,
2903
+ validateDashboardConfig
2075
2904
  });
2076
2905
  //# sourceMappingURL=index.cjs.map