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