@rozaqi02/reusable-dashboard 1.1.4 → 1.2.0

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
@@ -309,6 +309,111 @@ function validateDashboardConfig(dashboardConfig) {
309
309
  return { valid: issues.length === 0, issues };
310
310
  }
311
311
 
312
+ // src/config/createUniversalWidgetConfig.js
313
+ function createUniversalWidgetConfig(columns = {}, opts = {}) {
314
+ const hasAudience = Boolean(columns.audience);
315
+ const hasItem = Boolean(columns.item);
316
+ const hasCustomer = Boolean(columns.customer);
317
+ const hasTotal = Boolean(columns.total);
318
+ const charts = [
319
+ { id: "dailyTrends", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
320
+ { id: "statusDistribution", type: "statusPie", label: "statusDistribution", icon: "PieChart" }
321
+ ];
322
+ if (hasAudience) {
323
+ charts.push({
324
+ id: "audienceDistribution",
325
+ type: "audiencePie",
326
+ label: "audienceDistribution",
327
+ icon: "Users"
328
+ });
329
+ }
330
+ if (hasItem) {
331
+ charts.push({
332
+ id: "topPackages",
333
+ type: "topPackagesBar",
334
+ label: "topPackages",
335
+ icon: "TrendingUp"
336
+ });
337
+ }
338
+ const tableColumns = [
339
+ { id: "date", label: "date", accessor: "createdAt", type: "date" }
340
+ ];
341
+ if (hasCustomer) {
342
+ tableColumns.push({ id: "customer", label: "customer", accessor: "customerName" });
343
+ }
344
+ if (hasItem) {
345
+ tableColumns.push({ id: "package", label: "package", accessor: "packageName" });
346
+ }
347
+ if (hasAudience) {
348
+ tableColumns.push({ id: "audience", label: "audience", accessor: "audienceLabel" });
349
+ }
350
+ if (hasTotal) {
351
+ tableColumns.push({ id: "total", label: "total", accessor: "totalIDR", type: "currency" });
352
+ }
353
+ tableColumns.push({
354
+ id: "status",
355
+ label: "status",
356
+ accessor: "statusLabel",
357
+ type: "statusBadge",
358
+ statusAccessor: "status"
359
+ });
360
+ return {
361
+ id: opts.id || "universal.dashboard",
362
+ defaultFilters: {
363
+ statusScope: "confirmed",
364
+ includePendingOverlay: false,
365
+ audience: "",
366
+ daysPreset: 30,
367
+ sortPkgBy: "bookings",
368
+ sortPkgDir: "desc"
369
+ },
370
+ widgets: {
371
+ stats: [
372
+ {
373
+ id: "bookingsConfirm",
374
+ label: "confirmedBookings",
375
+ icon: "TrendingUp",
376
+ valueKey: "bookingsConfirm",
377
+ format: "number",
378
+ accentColor: "blue"
379
+ },
380
+ {
381
+ id: "revenueConfirm",
382
+ label: "confirmedRevenue",
383
+ icon: "DollarSign",
384
+ valueKey: "revenueConfirm",
385
+ format: "currency",
386
+ accentColor: "green"
387
+ },
388
+ {
389
+ id: "avgRevenue",
390
+ label: "avgRevenue",
391
+ icon: "Users",
392
+ valueKey: "avgRevenue",
393
+ format: "currency",
394
+ accentColor: "violet"
395
+ },
396
+ {
397
+ id: "conversionRate",
398
+ label: "conversionRate",
399
+ icon: "PieChart",
400
+ valueKey: "conversionRate",
401
+ format: "percent",
402
+ accentColor: "orange"
403
+ }
404
+ ],
405
+ charts,
406
+ table: {
407
+ id: "recentBookings",
408
+ label: "recentBookings",
409
+ icon: "Calendar",
410
+ emptyLabel: "noRecentBookings",
411
+ columns: tableColumns
412
+ }
413
+ }
414
+ };
415
+ }
416
+
312
417
  // src/utils/formatters.js
313
418
  function formatIDR(value) {
314
419
  try {
@@ -714,6 +819,131 @@ function adaptTokoSepatuData({ raw, filters, range, dateLocale, labels }) {
714
819
  };
715
820
  }
716
821
 
822
+ // src/data-adapter/universalAdapter.js
823
+ function createEmptyUniversalData() {
824
+ return {
825
+ stats: {
826
+ bookingsConfirm: 0,
827
+ bookingsPending: 0,
828
+ revenueConfirm: 0,
829
+ avgRevenue: 0,
830
+ conversionRate: 0
831
+ },
832
+ charts: {
833
+ dailyTrends: [],
834
+ statusDistribution: [],
835
+ audienceDistribution: [],
836
+ topPackages: []
837
+ },
838
+ table: {
839
+ recentBookings: []
840
+ }
841
+ };
842
+ }
843
+ function adaptUniversalData({
844
+ raw,
845
+ filters = {},
846
+ range,
847
+ dateLocale = "id-ID",
848
+ labels = {},
849
+ options = {}
850
+ }) {
851
+ if (!raw) return createEmptyUniversalData();
852
+ const confirmedValue = String(options.confirmedValue || "confirmed").toLowerCase();
853
+ const pendingValue = String(options.pendingValue || "pending").toLowerCase();
854
+ const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
855
+ const dayLookup = new Map(dailyBuckets.map((bucket) => [bucket.dateKey, bucket]));
856
+ const statusMap = /* @__PURE__ */ new Map();
857
+ const audienceMap = /* @__PURE__ */ new Map();
858
+ const itemCountMap = /* @__PURE__ */ new Map();
859
+ const itemRevenueMap = /* @__PURE__ */ new Map();
860
+ let bookingsConfirm = 0;
861
+ let bookingsPending = 0;
862
+ let revenueConfirm = 0;
863
+ const safeStatusLabel = typeof labels.formatStatusLabel === "function" ? labels.formatStatusLabel : (s) => String(s);
864
+ const safeAudienceLabel = typeof labels.formatAudienceLabel === "function" ? labels.formatAudienceLabel : (a) => String(a);
865
+ (raw.bookings || []).forEach((row) => {
866
+ const dayKey = String(row.created_at || "").slice(0, 10);
867
+ const status = String(row.status || pendingValue).toLowerCase();
868
+ const total = toNumber(row.total_idr);
869
+ const audience = row.audience || "unknown";
870
+ const item = row.item || "-";
871
+ statusMap.set(status, (statusMap.get(status) || 0) + 1);
872
+ audienceMap.set(audience, (audienceMap.get(audience) || 0) + 1);
873
+ const bucket = dayLookup.get(dayKey);
874
+ if (bucket) {
875
+ if (status === confirmedValue) {
876
+ bucket.count += 1;
877
+ bucket.revenue += total;
878
+ }
879
+ if (status === pendingValue) {
880
+ bucket.pendingCount += 1;
881
+ }
882
+ }
883
+ if (status === confirmedValue) {
884
+ bookingsConfirm += 1;
885
+ revenueConfirm += total;
886
+ itemCountMap.set(item, (itemCountMap.get(item) || 0) + 1);
887
+ itemRevenueMap.set(item, (itemRevenueMap.get(item) || 0) + total);
888
+ } else if (status === pendingValue) {
889
+ bookingsPending += 1;
890
+ }
891
+ });
892
+ const statusDistribution = sortMapEntries(statusMap, "desc").map(
893
+ ([status, count]) => ({
894
+ status,
895
+ label: safeStatusLabel(status),
896
+ count
897
+ })
898
+ );
899
+ const audienceDistribution = sortMapEntries(audienceMap, "desc").map(
900
+ ([audience, count]) => ({
901
+ audience,
902
+ label: safeAudienceLabel(audience),
903
+ count
904
+ })
905
+ );
906
+ const metricMap = filters.sortPkgBy === "revenue" ? itemRevenueMap : itemCountMap;
907
+ const topPackages = sortMapEntries(metricMap, filters.sortPkgDir || "desc").slice(0, 5).map(([item, value]) => ({
908
+ packageId: item,
909
+ name: item,
910
+ value: toNumber(value)
911
+ }));
912
+ const recentBookings = (raw.recent || []).map((row) => {
913
+ const status = String(row.status || pendingValue).toLowerCase();
914
+ return {
915
+ id: row.id,
916
+ createdAt: row.created_at,
917
+ customerName: row.customer_name || "-",
918
+ packageName: row.item || "-",
919
+ audienceLabel: safeAudienceLabel(row.audience),
920
+ totalIDR: toNumber(row.total_idr),
921
+ status,
922
+ statusLabel: safeStatusLabel(status)
923
+ };
924
+ });
925
+ const avgRevenue = bookingsConfirm > 0 ? Math.round(revenueConfirm / bookingsConfirm) : 0;
926
+ const conversionRate = bookingsConfirm + bookingsPending > 0 ? Math.round(bookingsConfirm / (bookingsConfirm + bookingsPending) * 100) : 0;
927
+ return {
928
+ stats: {
929
+ bookingsConfirm,
930
+ bookingsPending,
931
+ revenueConfirm,
932
+ avgRevenue,
933
+ conversionRate
934
+ },
935
+ charts: {
936
+ dailyTrends: dailyBuckets,
937
+ statusDistribution,
938
+ audienceDistribution,
939
+ topPackages
940
+ },
941
+ table: {
942
+ recentBookings
943
+ }
944
+ };
945
+ }
946
+
717
947
  // src/data-source/cidikaSupabaseSource.js
718
948
  function ensureNoError(response, message) {
719
949
  if (response == null ? void 0 : response.error) {
@@ -887,6 +1117,84 @@ function createTokoSepatuSupabaseSource(supabase) {
887
1117
  };
888
1118
  }
889
1119
 
1120
+ // src/data-source/universalSupabaseSource.js
1121
+ function createUniversalSource(supabase, { table, columns = {} } = {}) {
1122
+ if (!supabase) {
1123
+ throw new Error("createUniversalSource: prop 'supabase' wajib diisi.");
1124
+ }
1125
+ if (!table) {
1126
+ throw new Error("createUniversalSource: opsi 'table' wajib diisi.");
1127
+ }
1128
+ if (!columns.date) {
1129
+ throw new Error(
1130
+ "createUniversalSource: columns.date wajib diisi (kolom timestamp)."
1131
+ );
1132
+ }
1133
+ const map = {
1134
+ date: columns.date,
1135
+ status: columns.status || null,
1136
+ total: columns.total || null,
1137
+ customer: columns.customer || null,
1138
+ item: columns.item || null,
1139
+ audience: columns.audience || null
1140
+ };
1141
+ const selectCols = Array.from(
1142
+ /* @__PURE__ */ new Set(["id", ...Object.values(map).filter(Boolean)])
1143
+ ).join(", ");
1144
+ const normalize = (row) => ({
1145
+ id: row.id ?? row[map.date],
1146
+ created_at: row[map.date],
1147
+ status: map.status ? row[map.status] : "confirmed",
1148
+ total_idr: map.total ? row[map.total] : 0,
1149
+ item: map.item ? row[map.item] : null,
1150
+ customer_name: map.customer ? row[map.customer] : null,
1151
+ audience: map.audience ? row[map.audience] : "unknown"
1152
+ });
1153
+ return {
1154
+ async fetchDashboardSnapshot({ fromISO, toISO, audience, statusScope }) {
1155
+ const baseQuery = () => supabase.from(table).select(selectCols).gte(map.date, fromISO).lte(map.date, toISO);
1156
+ const bookingsQuery = baseQuery().order(map.date, { ascending: true });
1157
+ if (audience && map.audience) bookingsQuery.eq(map.audience, audience);
1158
+ const recentQuery = baseQuery().order(map.date, { ascending: false }).limit(10);
1159
+ if (audience && map.audience) recentQuery.eq(map.audience, audience);
1160
+ if (statusScope && statusScope !== "all" && map.status) {
1161
+ recentQuery.eq(map.status, statusScope);
1162
+ }
1163
+ const [bookingsRes, recentRes] = await Promise.all([
1164
+ bookingsQuery,
1165
+ recentQuery
1166
+ ]);
1167
+ if (bookingsRes == null ? void 0 : bookingsRes.error) {
1168
+ const err = new Error(
1169
+ `Gagal membaca tabel "${table}". Periksa nama tabel/kolom & RLS policy.`
1170
+ );
1171
+ err.cause = bookingsRes.error;
1172
+ throw err;
1173
+ }
1174
+ const bookings = (bookingsRes.data || []).map(normalize);
1175
+ const recent = (recentRes.error ? [] : recentRes.data || []).map(
1176
+ normalize
1177
+ );
1178
+ return {
1179
+ bookings,
1180
+ recent,
1181
+ packageLocales: [],
1182
+ staticCounts: {}
1183
+ };
1184
+ },
1185
+ subscribeLiveUpdate(onEvent) {
1186
+ const channel = supabase.channel(`reusable-dashboard-universal-${table}`).on(
1187
+ "postgres_changes",
1188
+ { event: "*", schema: "public", table },
1189
+ onEvent
1190
+ ).subscribe();
1191
+ return () => {
1192
+ supabase.removeChannel(channel);
1193
+ };
1194
+ }
1195
+ };
1196
+ }
1197
+
890
1198
  // src/hooks/useReusableDashboard.js
891
1199
  import { useCallback, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
892
1200
 
@@ -1864,80 +2172,95 @@ import PropTypes18 from "prop-types";
1864
2172
  import React17, { useState as useState6, useEffect as useEffect3, useCallback as useCallback5 } from "react";
1865
2173
  import PropTypes17 from "prop-types";
1866
2174
  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
- }
2175
+ cidika: { name: "Cidika Travel", tables: ["bookings", "packages", "package_locales"] },
2176
+ tokoSepatu: { name: "Toko Sepatu / E-Commerce", tables: ["orders", "products", "customers"] }
1875
2177
  };
1876
2178
  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";
2179
+ if (!(tables == null ? void 0 : tables.length)) return null;
2180
+ const s = new Set(tables);
2181
+ if (PRESET_SIGNATURES.cidika.tables.every((t) => s.has(t))) return "cidika";
2182
+ if (PRESET_SIGNATURES.tokoSepatu.tables.every((t) => s.has(t))) return "tokoSepatu";
1881
2183
  return null;
1882
2184
  }
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";
2185
+ var SQL_GRANT_SCHEMA = `GRANT SELECT ON information_schema.tables TO anon;
2186
+ GRANT SELECT ON information_schema.columns TO anon;`;
2187
+ var sqlRls = (tbl) => `-- Pilih salah satu sesuai kebutuhan (jalankan di Supabase SQL Editor):
2188
+
2189
+ -- Opsi 1: Untuk DEVELOPMENT \u2014 nonaktifkan RLS (paling cepat)
2190
+ ALTER TABLE public.${tbl} DISABLE ROW LEVEL SECURITY;
2191
+
2192
+ -- Opsi 2: Untuk PRODUCTION \u2014 izinkan semua orang baca
2193
+ ALTER TABLE public.${tbl} ENABLE ROW LEVEL SECURITY;
2194
+ CREATE POLICY "dashboard_read" ON public.${tbl}
2195
+ FOR SELECT TO anon USING (true);
2196
+
2197
+ -- Opsi 3: Untuk PRODUCTION \u2014 hanya user yang login
2198
+ ALTER TABLE public.${tbl} ENABLE ROW LEVEL SECURITY;
2199
+ CREATE POLICY "dashboard_auth" ON public.${tbl}
2200
+ FOR SELECT TO authenticated USING (true);`;
2201
+ function generateCode({
2202
+ tableName,
2203
+ colDate,
2204
+ colStatus,
2205
+ colTotal,
2206
+ colCustomer,
2207
+ colItem,
2208
+ dashTitle,
2209
+ confirmedValue,
2210
+ pendingValue
2211
+ }) {
2212
+ const title = dashTitle || "Dashboard";
1896
2213
  const confirmed = confirmedValue || "confirmed";
1897
2214
  const pending = pendingValue || "pending";
1898
- const safeCustomer = colCustomer || "customer_name";
1899
- const safeItem = colItem || "-";
1900
- const colTotalFull = colTotal || "total";
2215
+ const custCol = colCustomer || "customer_name";
2216
+ const itemCol = colItem || "-";
2217
+ const totalCol = colTotal || "total";
2218
+ const cols = [
2219
+ "id",
2220
+ colDate,
2221
+ colStatus,
2222
+ totalCol,
2223
+ ...custCol !== "customer_name" ? [custCol] : ["customer_name"],
2224
+ ...itemCol !== "-" ? [itemCol] : []
2225
+ ].filter(Boolean).join(", ");
1901
2226
  const dataSource = `// src/datasources/myDashboardSource.js
1902
2227
  // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
1903
- // Tabel: ${tableName}
1904
2228
 
1905
2229
  export function createMyDashboardSource(supabase) {
1906
2230
  return {
1907
2231
  async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
1908
- const allQuery = supabase
2232
+ const allQ = supabase
1909
2233
  .from("${tableName}")
1910
- .select("id, ${colDate}, ${colStatus}, ${colTotalFull}${colCustomer !== "customer_name" ? `, ${colCustomer}` : ", customer_name"}${colItem !== "-" ? `, ${colItem}` : ""}")
2234
+ .select("${cols}")
1911
2235
  .gte("${colDate}", fromISO)
1912
2236
  .lte("${colDate}", toISO)
1913
2237
  .order("${colDate}", { ascending: true });
1914
2238
 
1915
- const recentQuery = supabase
2239
+ const recentQ = supabase
1916
2240
  .from("${tableName}")
1917
- .select("id, ${colDate}, ${colStatus}, ${colTotalFull}${colCustomer !== "customer_name" ? `, ${colCustomer}` : ", customer_name"}${colItem !== "-" ? `, ${colItem}` : ""}")
2241
+ .select("${cols}")
1918
2242
  .gte("${colDate}", fromISO)
1919
2243
  .lte("${colDate}", toISO)
1920
2244
  .order("${colDate}", { ascending: false })
1921
2245
  .limit(10);
1922
2246
 
1923
2247
  if (statusScope && statusScope !== "all") {
1924
- recentQuery.eq("${colStatus}", statusScope);
2248
+ recentQ.eq("${colStatus}", statusScope);
1925
2249
  }
1926
2250
 
1927
- const [allRes, recentRes] = await Promise.all([allQuery, recentQuery]);
1928
-
2251
+ const [all, recent] = await Promise.all([allQ, recentQ]);
1929
2252
  return {
1930
- bookings: allRes.data || [], // semua transaksi (untuk chart & stats)
1931
- recent: recentRes.data || [], // 10 terbaru (untuk tabel)
2253
+ bookings: all.data || [],
2254
+ recent: recent.data || [],
1932
2255
  packageLocales: [],
1933
2256
  staticCounts: { packages: 0, sections: 0 },
1934
2257
  };
1935
2258
  },
1936
2259
 
1937
2260
  subscribeLiveUpdate(onEvent) {
1938
- const ch = supabase
1939
- .channel("rdb-dashboard-live")
1940
- .on("postgres_changes", { event: "*", schema: "public", table: "${tableName}" }, onEvent)
2261
+ const ch = supabase.channel("rdb-${tableName}-live")
2262
+ .on("postgres_changes",
2263
+ { event: "*", schema: "public", table: "${tableName}" }, onEvent)
1941
2264
  .subscribe();
1942
2265
  return () => supabase.removeChannel(ch);
1943
2266
  },
@@ -1945,15 +2268,13 @@ export function createMyDashboardSource(supabase) {
1945
2268
  }`;
1946
2269
  const adapter = `// src/adapters/myDashboardAdapter.js
1947
2270
  // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
1948
- // Mapping kolom: status="${colStatus}", total="${colTotalFull}", tanggal="${colDate}"
1949
2271
 
1950
2272
  import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
1951
2273
 
1952
2274
  export function createEmptyMyDashboardData() {
1953
2275
  return {
1954
- stats: { bookingsConfirm: 0, revenueConfirm: 0 },
1955
- charts: { dailyTrends: [], statusDistribution: [],
1956
- audienceDistribution: [], topPackages: [] },
2276
+ stats: { bookingsConfirm: 0, revenueConfirm: 0, avgRevenue: 0, conversionRate: 0 },
2277
+ charts: { dailyTrends: [], statusDistribution: [], audienceDistribution: [], topPackages: [] },
1957
2278
  table: { recentBookings: [] },
1958
2279
  };
1959
2280
  }
@@ -1967,51 +2288,45 @@ export function adaptMyDashboardData({ raw, range, dateLocale, labels }) {
1967
2288
  let confirmed = 0, revenue = 0;
1968
2289
 
1969
2290
  (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);
2291
+ const st = String(row["${colStatus}"] || "").toLowerCase();
2292
+ const amt = toNumber(row["${totalCol}"]);
2293
+ const dk = String(row["${colDate}"] || "").slice(0, 10);
1973
2294
 
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++; }
2295
+ statusMap.set(st, (statusMap.get(st) || 0) + 1);
2296
+ const b = dayMap.get(dk);
2297
+ if (b) {
2298
+ if (st === "${confirmed}") { b.count++; b.revenue += amt; }
2299
+ if (st === "${pending}") { b.pendingCount++; }
1979
2300
  }
1980
- if (status === "${confirmed}") { confirmed++; revenue += amount; }
2301
+ if (st === "${confirmed}") { confirmed++; revenue += amt; }
1981
2302
  });
1982
2303
 
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
-
2304
+ const total = (raw.bookings || []).length;
1987
2305
  return {
1988
2306
  stats: {
1989
2307
  bookingsConfirm: confirmed,
1990
2308
  revenueConfirm: revenue,
1991
- avgRevenue,
1992
- conversionRate,
2309
+ avgRevenue: confirmed > 0 ? Math.round(revenue / confirmed) : 0,
2310
+ conversionRate: total > 0 ? Math.round((confirmed / total) * 100) : 0,
1993
2311
  },
1994
2312
  charts: {
1995
2313
  dailyTrends: buckets,
1996
2314
  statusDistribution: Array.from(statusMap.entries())
1997
2315
  .sort((a, b) => b[1] - a[1])
1998
- .map(([status, count]) => ({
1999
- status, count,
2000
- label: labels?.formatStatusLabel?.(status) || status,
2001
- })),
2316
+ .map(([s, c]) => ({ status: s, count: c, label: labels?.formatStatusLabel?.(s) || s })),
2002
2317
  audienceDistribution: [],
2003
2318
  topPackages: [],
2004
2319
  },
2005
2320
  table: {
2006
2321
  recentBookings: (raw.recent || []).map(row => ({
2007
- id: row.id,
2008
- createdAt: row["${colDate}"],
2009
- customerName: row["${colCustomer}"] || "-",
2010
- packageName: ${colItem !== "-" ? `row["${colItem}"] || "-"` : '"-"'},
2322
+ id: row.id,
2323
+ createdAt: row["${colDate}"],
2324
+ customerName: row["${custCol}"] || "-",
2325
+ packageName: ${itemCol !== "-" ? `row["${itemCol}"] || "-"` : '"-"'},
2011
2326
  audienceLabel: "-",
2012
- totalIDR: toNumber(row["${colTotalFull}"]),
2013
- status: String(row["${colStatus}"] || "pending").toLowerCase(),
2014
- statusLabel: labels?.formatStatusLabel?.(row["${colStatus}"]) || row["${colStatus}"],
2327
+ totalIDR: toNumber(row["${totalCol}"]),
2328
+ status: String(row["${colStatus}"] || "").toLowerCase(),
2329
+ statusLabel: labels?.formatStatusLabel?.(row["${colStatus}"]) || row["${colStatus}"] || "-",
2015
2330
  })),
2016
2331
  },
2017
2332
  };
@@ -2021,151 +2336,120 @@ export function adaptMyDashboardData({ raw, range, dateLocale, labels }) {
2021
2336
 
2022
2337
  export const myDashboardConfig = {
2023
2338
  id: "my.custom.dashboard",
2024
- defaultFilters: {
2025
- statusScope: "${confirmed}",
2026
- daysPreset: 30,
2027
- sortPkgBy: "bookings",
2028
- sortPkgDir: "desc",
2029
- },
2339
+ defaultFilters: { statusScope: "${confirmed}", daysPreset: 30,
2340
+ sortPkgBy: "bookings", sortPkgDir: "desc" },
2030
2341
  widgets: {
2031
2342
  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" },
2343
+ { id:"orders", label:"confirmedBookings", icon:"TrendingUp",
2344
+ valueKey:"bookingsConfirm", format:"number", accentColor:"blue" },
2345
+ { id:"revenue", label:"confirmedRevenue", icon:"DollarSign",
2346
+ valueKey:"revenueConfirm", format:"currency", accentColor:"green" },
2347
+ { id:"avg", label:"avgRevenue", icon:"Users",
2348
+ valueKey:"avgRevenue", format:"currency", accentColor:"violet" },
2349
+ { id:"conversion", label:"conversionRate", icon:"PieChart",
2350
+ valueKey:"conversionRate", format:"percent", accentColor:"orange" },
2040
2351
  ],
2041
2352
  charts: [
2042
- { id: "trend", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
2043
- { id: "status", type: "statusPie", label: "statusDistribution", icon: "PieChart" },
2353
+ { id:"trend", type:"dailyArea", label:"dailyTrends", icon:"BarChart3" },
2354
+ { id:"status", type:"statusPie", label:"statusDistribution", icon:"PieChart" },
2044
2355
  ],
2045
2356
  table: {
2046
- id: "recentTx", label: "recentBookings", icon: "Calendar",
2047
- emptyLabel: "noRecentBookings",
2357
+ id:"recentTx", label:"recentBookings", icon:"Calendar", emptyLabel:"noRecentBookings",
2048
2358
  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" },
2359
+ { id:"date", label:"date", accessor:"createdAt", type:"date" },
2360
+ { id:"customer", label:"customer", accessor:"customerName" },${itemCol !== "-" ? `
2361
+ { id:"item", label:"package", accessor:"packageName" },` : ""}
2362
+ { id:"total", label:"total", accessor:"totalIDR", type:"currency" },
2363
+ { id:"status", label:"status", accessor:"statusLabel",
2364
+ type:"statusBadge", statusAccessor:"status" },
2055
2365
  ],
2056
2366
  },
2057
2367
  },
2058
2368
  };`;
2059
2369
  const dashboard = `// src/pages/admin/Dashboard.jsx
2060
2370
  // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
2061
- // Salin file ini ke halaman dashboard kamu.
2062
2371
 
2063
2372
  import React from "react";
2064
2373
  import { supabase } from "../../lib/supabaseClient.js";
2065
2374
  import {
2066
- ReusableDashboardView,
2067
- useReusableDashboard,
2068
- createDashboardConfig,
2375
+ ReusableDashboardView, useReusableDashboard, createDashboardConfig,
2069
2376
  } from "@rozaqi02/reusable-dashboard";
2070
2377
 
2071
- // Import 3 file yang digenerate wizard
2072
- import { myDashboardConfig } from "./myDashboardConfig";
2073
- import { createMyDashboardSource } from "./myDashboardSource";
2074
- import { adaptMyDashboardData, createEmptyMyDashboardData } from "./myDashboardAdapter";
2378
+ // Tiga file hasil generate wizard
2379
+ import { myDashboardConfig } from "../../config/myDashboardConfig";
2380
+ import { createMyDashboardSource } from "../../datasources/myDashboardSource";
2381
+ import { adaptMyDashboardData, createEmptyMyDashboardData } from "../../adapters/myDashboardAdapter";
2075
2382
 
2076
- // Data source \u2014 dibuat sekali di luar komponen
2077
2383
  const source = createMyDashboardSource(supabase);
2078
2384
 
2079
- // Label UI \u2014 sesuaikan teks dengan bahasa/konteks bisnis kamu
2080
2385
  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",
2386
+ title: "${title}", refresh: "Refresh", liveUpdate: "Live update",
2387
+ loadFailed: "Gagal memuat data.", retry: "Coba Lagi",
2388
+ confirmedOnly: "Selesai", pendingOnly: "Proses", allStatus: "Semua Status",
2389
+ showPendingOverlay: "Tampilkan pending", reset: "Reset",
2390
+ confirmedBookings: "Total Transaksi", confirmedRevenue: "Total Pendapatan",
2391
+ avgRevenue: "Rata-rata / Transaksi", conversionRate: "Conversion Rate",
2392
+ dailyTrends: "Tren Harian", statusDistribution: "Distribusi Status",
2393
+ recentBookings: "Transaksi Terbaru", noRecentBookings: "Belum ada transaksi",
2394
+ date: "Tanggal", customer: "Pelanggan", package: "Item",
2395
+ total: "Total", status: "Status",
2396
+ bookingsMetric: "Transaksi", revenueMetric: "Pendapatan",
2106
2397
  confirmedBookingMetric: "Transaksi (Selesai)",
2107
2398
  confirmedRevenueMetric: "Pendapatan (Selesai)",
2108
2399
  dayLabel: n => n + " hari",
2109
- formatStatusLabel: s =>
2110
- ({ "${confirmed}": "Selesai", "${pending}": "Proses" })[s] || (s || "-"),
2400
+ formatStatusLabel: s => ({ "${confirmed}":"Selesai", "${pending}":"Proses" })[s] || s || "-",
2111
2401
  formatAudienceLabel: v => v || "-",
2112
2402
  };
2113
2403
 
2114
- // Kemas semua jadi 1 objek
2115
2404
  const dashboardConfig = createDashboardConfig({
2116
- widgetConfig: myDashboardConfig,
2117
- dataSource: source,
2118
- adapter: adaptMyDashboardData,
2119
- createEmptyState: createEmptyMyDashboardData,
2120
- languageCode: "id",
2121
- dateLocale: "id-ID",
2122
- labels,
2405
+ widgetConfig: myDashboardConfig, dataSource: source,
2406
+ adapter: adaptMyDashboardData, createEmptyState: createEmptyMyDashboardData,
2407
+ languageCode: "id", dateLocale: "id-ID", labels,
2123
2408
  });
2124
2409
 
2125
2410
  export default function Dashboard() {
2126
2411
  const state = useReusableDashboard({ ...dashboardConfig, labels });
2127
2412
  return (
2128
2413
  <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}
2414
+ config={dashboardConfig.config} labels={labels}
2415
+ loading={state.loading} error={state.error}
2416
+ filters={state.filters} onFilterChange={state.updateFilter}
2417
+ onResetFilters={state.resetFilters} onRefresh={state.refresh}
2418
+ data={state.data} dateLocale={dashboardConfig.dateLocale}
2139
2419
  liveUpdateEnabled={state.liveUpdateEnabled}
2140
- supabase={supabase}
2141
- dashboardConfig={dashboardConfig}
2420
+ supabase={supabase} dashboardConfig={dashboardConfig}
2142
2421
  />
2143
2422
  );
2144
2423
  }`;
2145
2424
  return { dataSource, adapter, widgetConfig, dashboard };
2146
2425
  }
2147
2426
  function SetupWizard({ issues = [], onDismiss, supabase }) {
2427
+ var _a, _b;
2148
2428
  const [dismissed, setDismissed] = useState6(false);
2149
2429
  const [step, setStep] = useState6(0);
2150
2430
  const [detecting, setDetecting] = useState6(true);
2151
2431
  const [tables, setTables] = useState6([]);
2152
2432
  const [columns, setColumns] = useState6({});
2433
+ const [sampleData, setSampleData] = useState6({});
2434
+ const [statusValues, setStatusValues] = useState6({});
2153
2435
  const [supabaseOk, setSupabaseOk] = useState6(false);
2154
2436
  const [detectionDone, setDetectionDone] = useState6(false);
2155
- const [userHasDashboard, setUserHasDashboard] = useState6(null);
2437
+ const [tableBlocked, setTableBlocked] = useState6(false);
2156
2438
  const [selectedTable, setSelectedTable] = useState6("");
2157
2439
  const [colDate, setColDate] = useState6("");
2158
2440
  const [colStatus, setColStatus] = useState6("");
2159
2441
  const [colTotal, setColTotal] = useState6("");
2160
2442
  const [colCustomer, setColCustomer] = useState6("");
2161
2443
  const [colItem, setColItem] = useState6("");
2162
- const [confirmedVal, setConfirmedVal] = useState6("confirmed");
2163
- const [pendingVal, setPendingVal] = useState6("pending");
2444
+ const [confirmedVal, setConfirmedVal] = useState6("");
2445
+ const [pendingVal, setPendingVal] = useState6("");
2164
2446
  const [dashTitle, setDashTitle] = useState6("Dashboard");
2165
- const [loadingColumns, setLoadingColumns] = useState6(false);
2447
+ const [loadingCols, setLoadingCols] = useState6(false);
2448
+ const [colsBlocked, setColsBlocked] = useState6(false);
2166
2449
  const [generatedCode, setGeneratedCode] = useState6(null);
2167
- const [activeCodeTab, setActiveCodeTab] = useState6("dataSource");
2450
+ const [activeTab, setActiveTab] = useState6("dashboard");
2168
2451
  const [copied, setCopied] = useState6("");
2452
+ const [showRls, setShowRls] = useState6(false);
2169
2453
  useEffect3(() => {
2170
2454
  if (!supabase) {
2171
2455
  setDetecting(false);
@@ -2174,388 +2458,376 @@ function SetupWizard({ issues = [], onDismiss, supabase }) {
2174
2458
  }
2175
2459
  (async () => {
2176
2460
  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));
2461
+ const { data: d1, error: e1 } = await supabase.from("information_schema.tables").select("table_name").eq("table_schema", "public").eq("table_type", "BASE TABLE").order("table_name");
2462
+ if (!e1 && (d1 == null ? void 0 : d1.length)) {
2463
+ setTables(d1.map((r) => r.table_name));
2180
2464
  setSupabaseOk(true);
2465
+ return;
2466
+ }
2467
+ const { data: d2, error: e2 } = await supabase.rpc("rdb_get_tables");
2468
+ if (!e2 && Array.isArray(d2)) {
2469
+ setTables(d2.map((r) => typeof r === "string" ? r : r.table_name || r));
2470
+ setSupabaseOk(true);
2471
+ return;
2472
+ }
2473
+ const { error: ping } = await supabase.from("_rdb_wizard_ping_").select("*").limit(1);
2474
+ if (!ping || ping.code === "42P01" || ping.code === "PGRST116" || ping.code === "PGRST200" || ping.message && (ping.message.includes("does not exist") || ping.message.includes("not found") || ping.message.includes("relation"))) {
2475
+ setSupabaseOk(true);
2476
+ setTableBlocked(true);
2181
2477
  } 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
- }
2478
+ setSupabaseOk(!!supabase);
2479
+ setTableBlocked(true);
2192
2480
  }
2193
2481
  } catch {
2194
- setSupabaseOk(false);
2482
+ setSupabaseOk(!!supabase);
2483
+ setTableBlocked(true);
2195
2484
  } finally {
2196
2485
  setDetecting(false);
2197
2486
  setDetectionDone(true);
2198
2487
  }
2199
2488
  })();
2200
2489
  }, [supabase]);
2201
- const loadColumns = useCallback5(async (tbl) => {
2202
- if (!supabase || !tbl || columns[tbl]) return;
2203
- setLoadingColumns(true);
2490
+ const analyzeTable = useCallback5(async (tbl) => {
2491
+ var _a2;
2492
+ if (!supabase || !tbl) return;
2493
+ if (columns[tbl]) {
2494
+ autoDetect(tbl, columns[tbl]);
2495
+ return;
2496
+ }
2497
+ setLoadingCols(true);
2498
+ setColsBlocked(false);
2204
2499
  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;
2500
+ let cols = null;
2501
+ const { data: ca, error: ea } = await supabase.from("information_schema.columns").select("column_name, data_type").eq("table_schema", "public").eq("table_name", tbl).order("ordinal_position");
2502
+ if (!ea && (ca == null ? void 0 : ca.length)) cols = ca;
2503
+ if (!cols) {
2504
+ const { data: sb, error: eb } = await supabase.from(tbl).select("*").limit(1);
2505
+ if (!eb && (sb == null ? void 0 : sb.length) > 0)
2506
+ cols = Object.keys(sb[0]).map((k) => ({ column_name: k, data_type: typeof sb[0][k] }));
2213
2507
  }
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;
2508
+ if (cols) {
2509
+ setColumns((p) => ({ ...p, [tbl]: cols }));
2510
+ autoDetect(tbl, cols);
2511
+ } else {
2512
+ setColsBlocked(true);
2219
2513
  }
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;
2514
+ const { data: sample2 } = await supabase.from(tbl).select("*").limit(5);
2515
+ if ((sample2 == null ? void 0 : sample2.length) > 0) {
2516
+ setSampleData((p) => ({ ...p, [tbl]: sample2 }));
2517
+ const statusCol = (_a2 = (cols || []).find(
2518
+ (c) => ["status", "state", "kondisi", "order_status"].includes(c.column_name.toLowerCase())
2519
+ )) == null ? void 0 : _a2.column_name;
2520
+ if (statusCol) {
2521
+ const uniqVals = [...new Set(sample2.map((r) => r[statusCol]).filter(Boolean))];
2522
+ setStatusValues((p) => ({ ...p, [tbl]: { [statusCol]: uniqVals } }));
2523
+ const vals = uniqVals.map((v) => String(v).toLowerCase());
2524
+ const cfm = uniqVals.find((v) => ["confirmed", "selesai", "lunas", "paid", "done", "completed", "approve"].includes(String(v).toLowerCase()));
2525
+ const pnd = uniqVals.find((v) => ["pending", "proses", "menunggu", "processing", "waiting", "draft"].includes(String(v).toLowerCase()));
2526
+ if (cfm) setConfirmedVal(String(cfm));
2527
+ if (pnd) setPendingVal(String(pnd));
2528
+ }
2229
2529
  }
2230
- setColumns((prev) => ({ ...prev, [tbl]: [] }));
2231
2530
  } catch {
2232
- setColumns((prev) => ({ ...prev, [tbl]: [] }));
2531
+ setColsBlocked(true);
2233
2532
  } finally {
2234
- setLoadingColumns(false);
2533
+ setLoadingCols(false);
2235
2534
  }
2236
2535
  }, [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) || "";
2536
+ function autoDetect(tbl, cols) {
2537
+ const f = (opts) => {
2538
+ var _a2;
2539
+ return ((_a2 = cols.find((c) => opts.includes(c.column_name.toLowerCase()))) == null ? void 0 : _a2.column_name) || "";
2241
2540
  };
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"]));
2541
+ setColDate(f(["created_at", "tanggal", "date", "transaction_date", "order_date", "waktu"]));
2542
+ setColStatus(f(["status", "state", "kondisi", "order_status", "payment_status"]));
2543
+ setColTotal(f(["total", "total_idr", "total_amount", "total_price", "harga_total", "amount", "nominal", "harga", "grand_total"]));
2544
+ setColCustomer(f(["customer_name", "nama_pelanggan", "nama", "name", "client_name", "buyer_name", "pelanggan"]));
2545
+ setColItem(f(["service_type", "item_name", "product_name", "package_name", "layanan", "produk", "jenis", "paket"]));
2247
2546
  }
2248
- function copyCode(text, label) {
2249
- navigator.clipboard.writeText(text).then(() => {
2250
- setCopied(label);
2251
- setTimeout(() => setCopied(""), 2e3);
2547
+ function copyCode(text, key) {
2548
+ var _a2;
2549
+ (_a2 = navigator.clipboard) == null ? void 0 : _a2.writeText(text).then(() => {
2550
+ setCopied(key);
2551
+ setTimeout(() => setCopied(""), 2200);
2252
2552
  });
2253
2553
  }
2254
2554
  function handleDismiss() {
2255
2555
  setDismissed(true);
2256
- if (onDismiss) onDismiss();
2556
+ onDismiss == null ? void 0 : onDismiss();
2257
2557
  }
2258
2558
  function handleGenerate() {
2259
2559
  if (!selectedTable || !colDate || !colStatus || !colTotal) return;
2260
- const code = generateCode({
2261
- tableName: selectedTable,
2560
+ setGeneratedCode(generateCode({
2561
+ tableName: selectedTable.trim(),
2262
2562
  colDate,
2263
2563
  colStatus,
2264
2564
  colTotal,
2265
2565
  colCustomer: colCustomer || "customer_name",
2266
2566
  colItem: colItem || "-",
2267
2567
  dashTitle,
2268
- confirmedValue: confirmedVal,
2269
- pendingValue: pendingVal
2270
- });
2271
- setGeneratedCode(code);
2568
+ confirmedValue: confirmedVal || "confirmed",
2569
+ pendingValue: pendingVal || "pending"
2570
+ }));
2272
2571
  setStep(3);
2273
2572
  }
2274
2573
  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",
2574
+ const preset = detectPreset(tables);
2575
+ const tblCols = selectedTable ? columns[selectedTable] || [] : [];
2576
+ const colNames = tblCols.map((c) => c.column_name);
2577
+ const sample = selectedTable ? sampleData[selectedTable] || [] : [];
2578
+ const stVals = selectedTable && colStatus ? ((_a = statusValues[selectedTable]) == null ? void 0 : _a[colStatus]) || [] : [];
2579
+ const canGen = selectedTable.trim() && colDate && colStatus && colTotal;
2580
+ const TAB_INFO = {
2581
+ dashboard: { label: "Dashboard.jsx", path: "src/pages/admin/Dashboard.jsx" },
2582
+ dataSource: { label: "myDashboardSource.js", path: "src/datasources/myDashboardSource.js" },
2583
+ adapter: { label: "myDashboardAdapter.js", path: "src/adapters/myDashboardAdapter.js" },
2584
+ widgetConfig: { label: "myDashboardConfig.js", path: "src/config/myDashboardConfig.js" }
2585
+ };
2586
+ function PreviewTable({ rows, cols: previewCols }) {
2587
+ if (!(rows == null ? void 0 : rows.length) || !(previewCols == null ? void 0 : previewCols.length)) return null;
2588
+ const validCols = previewCols.filter((c) => {
2589
+ var _a2;
2590
+ return c && ((_a2 = rows[0]) == null ? void 0 : _a2[c]) !== void 0;
2591
+ });
2592
+ if (!validCols.length) return null;
2593
+ return /* @__PURE__ */ React17.createElement("div", { className: "rdb-table-wrapper", style: { marginTop: 8, fontSize: "0.75rem" } }, /* @__PURE__ */ React17.createElement("table", { className: "rdb-table", style: { fontSize: "0.75rem" } }, /* @__PURE__ */ React17.createElement("thead", null, /* @__PURE__ */ React17.createElement("tr", null, validCols.map((c) => /* @__PURE__ */ React17.createElement("th", { key: c, style: { padding: "4px 8px", fontSize: "0.7rem" } }, c)))), /* @__PURE__ */ React17.createElement("tbody", null, rows.slice(0, 3).map((row, i) => /* @__PURE__ */ React17.createElement("tr", { key: i }, validCols.map((c) => /* @__PURE__ */ React17.createElement("td", { key: c, style: { padding: "3px 8px" } }, String(row[c] ?? "-").slice(0, 30))))))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { marginTop: 4, color: "var(--rdb-text-muted)" } }, "Preview 3 baris data nyata dari database kamu"));
2594
+ }
2595
+ return /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-overlay", role: "dialog", "aria-modal": "true" }, /* @__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 Wizard"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption" }, detecting ? "Menghubungi Supabase\u2026" : supabaseOk ? tables.length > 0 ? `\u2705 Tersambung \xB7 ${tables.length} tabel ditemukan` : "\u2705 Tersambung \xB7 ketik nama tabel di Step 2" : "Ikuti langkah di bawah untuk menghubungkan Supabase"))), /* @__PURE__ */ React17.createElement("button", { type: "button", className: "rdb-wizard-close", onClick: handleDismiss }, "\u2715")), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-steps" }, ["1. Koneksi", "2. Pilih Tabel", "3. Konfirmasi", "4. Kode Jadi"].map((lbl, i) => /* @__PURE__ */ React17.createElement(
2596
+ "button",
2282
2597
  {
2283
- className: "rdb-wizard-overlay",
2284
- role: "dialog",
2285
- "aria-modal": "true",
2286
- "aria-label": "Setup Wizard @rozaqi02/reusable-dashboard"
2598
+ key: i,
2599
+ type: "button",
2600
+ className: `rdb-wizard-step-btn ${step === i ? "rdb-wizard-step-active" : ""}`,
2601
+ onClick: () => setStep(i)
2287
2602
  },
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(
2603
+ step > i ? "\u2705 " : "",
2604
+ lbl
2605
+ ))), /* @__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 yang 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: 16 } }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 10 } }, "\u{1F50D} Status koneksi Supabase:"), 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" }, supabaseOk ? "Supabase tersambung" : "Tidak tersambung \u2014 tambahkan prop supabase={supabase} 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 ? `Wizard membaca ${tables.length} tabel langsung dari database` : "Daftar tabel belum bisa terbaca (butuh izin \u2014 lihat SQL di bawah)")))), supabaseOk && tableBlocked && /* @__PURE__ */ React17.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-alert", style: { marginBottom: 8 } }, /* @__PURE__ */ React17.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u{1F511}"), /* @__PURE__ */ React17.createElement("div", null, /* @__PURE__ */ React17.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Agar wizard bisa baca daftar tabel otomatis"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Jalankan SQL ini ", /* @__PURE__ */ React17.createElement("strong", null, "sekali"), " di Supabase SQL Editor, lalu refresh halaman. Atau lewati dan ketik nama tabel manual di Step 2."))), /* @__PURE__ */ React17.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React17.createElement(
2606
+ "button",
2607
+ {
2608
+ type: "button",
2609
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2610
+ style: { position: "absolute", top: 6, right: 6 },
2611
+ onClick: () => copyCode(SQL_GRANT_SCHEMA, "grant")
2612
+ },
2613
+ copied === "grant" ? "\u2705" : "\u{1F4CB}",
2614
+ " Salin"
2615
+ ), /* @__PURE__ */ React17.createElement("pre", { className: "rdb-wizard-code", style: { fontSize: "0.72rem" } }, SQL_GRANT_SCHEMA))), supabaseOk && /* @__PURE__ */ React17.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ React17.createElement(
2616
+ "button",
2617
+ {
2618
+ type: "button",
2619
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2620
+ onClick: () => setShowRls((r) => !r)
2621
+ },
2622
+ showRls ? "\u25B2 Sembunyikan" : "\u25BC Lihat",
2623
+ " panduan RLS \u2014 agar data tabel bisa terbaca dashboard"
2624
+ ), showRls && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-flow", style: { marginTop: 10 } }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-body", style: { fontWeight: 600, marginBottom: 6 } }, "Apa itu RLS?"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 10 } }, "Row Level Security (RLS) adalah fitur Supabase yang mengontrol siapa yang boleh membaca data. Jika aktif tanpa aturan, dashboard tidak bisa mengambil data apapun."), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 6 } }, "SQL \u2014 ganti ", /* @__PURE__ */ React17.createElement("code", null, "nama_tabel"), " dengan nama tabelmu:"), /* @__PURE__ */ React17.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React17.createElement(
2625
+ "button",
2626
+ {
2627
+ type: "button",
2628
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2629
+ style: { position: "absolute", top: 6, right: 6 },
2630
+ onClick: () => copyCode(sqlRls("nama_tabel"), "rls")
2631
+ },
2632
+ copied === "rls" ? "\u2705" : "\u{1F4CB}",
2633
+ " Salin"
2634
+ ), /* @__PURE__ */ React17.createElement("pre", { className: "rdb-wizard-code", style: { fontSize: "0.7rem" } }, sqlRls("nama_tabel"))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { marginTop: 8, color: "var(--rdb-text-muted)" } }, "\u{1F4A1} ", /* @__PURE__ */ React17.createElement("strong", null, "Development:"), " pakai Opsi 1.", /* @__PURE__ */ React17.createElement("strong", null, " Production:"), " pakai Opsi 2 atau 3."))), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ React17.createElement(
2635
+ "button",
2636
+ {
2637
+ type: "button",
2638
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2639
+ onClick: handleDismiss
2640
+ },
2641
+ "Lanjutkan tanpa wizard"
2642
+ ), supabaseOk && /* @__PURE__ */ React17.createElement(
2643
+ "button",
2644
+ {
2645
+ type: "button",
2646
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2647
+ onClick: () => setStep(1)
2648
+ },
2649
+ "Lanjut \u2192 Pilih Tabel"
2650
+ ))), 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 } }, "Klik tabel yang berisi data transaksi bisnis kamu. Wizard akan", /* @__PURE__ */ React17.createElement("strong", null, " otomatis membaca kolom dan sample data"), " dari tabel tersebut."), tables.length > 0 && /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 16 } }, tables.map((tbl) => {
2651
+ var _a2, _b2;
2652
+ const isSelected = selectedTable === tbl;
2653
+ const hasSample = ((_a2 = sampleData[tbl]) == null ? void 0 : _a2.length) > 0;
2654
+ const hasColumns = ((_b2 = columns[tbl]) == null ? void 0 : _b2.length) > 0;
2655
+ return /* @__PURE__ */ React17.createElement(
2326
2656
  "button",
2327
2657
  {
2328
2658
  key: tbl,
2329
2659
  type: "button",
2330
2660
  onClick: () => {
2331
2661
  setSelectedTable(tbl);
2332
- loadColumns(tbl);
2662
+ analyzeTable(tbl);
2333
2663
  },
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" }
2664
+ className: `rdb-btn rdb-btn-sm ${isSelected ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2665
+ style: { fontFamily: "monospace", fontSize: "0.82rem" }
2530
2666
  },
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
- );
2667
+ isSelected ? "\u2705 " : "",
2668
+ tbl,
2669
+ isSelected && hasColumns ? ` (${columns[tbl].length} kolom)` : ""
2670
+ );
2671
+ })), /* @__PURE__ */ React17.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ React17.createElement("label", { className: "rdb-label" }, tables.length > 0 ? "Atau ketik nama tabel lain:" : "Ketik nama tabel kamu:"), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React17.createElement(
2672
+ "input",
2673
+ {
2674
+ className: "rdb-input",
2675
+ value: selectedTable,
2676
+ onChange: (e) => setSelectedTable(e.target.value.replace(/[,\s].*/, "")),
2677
+ placeholder: "cth: orders / transaksi / bookings",
2678
+ style: { maxWidth: 280 }
2679
+ }
2680
+ ), /* @__PURE__ */ React17.createElement(
2681
+ "button",
2682
+ {
2683
+ type: "button",
2684
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2685
+ disabled: !selectedTable || loadingCols,
2686
+ onClick: () => analyzeTable(selectedTable)
2687
+ },
2688
+ loadingCols ? "Menganalisis\u2026" : "Analisis tabel"
2689
+ ))), selectedTable && !loadingCols && /* @__PURE__ */ React17.createElement(React17.Fragment, null, colNames.length > 0 && /* @__PURE__ */ React17.createElement(React17.Fragment, null, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-tip", style: { marginBottom: 8 } }, "\u2705 ", /* @__PURE__ */ React17.createElement("strong", null, colNames.length, " kolom"), " ditemukan.", sample.length > 0 && ` ${sample.length} baris sample data terbaca.`, " ", "Kolom sudah otomatis diisi di step berikutnya."), sample.length > 0 && /* @__PURE__ */ React17.createElement(
2690
+ PreviewTable,
2691
+ {
2692
+ rows: sample,
2693
+ cols: [colDate, colStatus, colTotal, colCustomer].filter(Boolean)
2694
+ }
2695
+ )), colsBlocked && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-alert" }, /* @__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 } }, "Kolom tidak bisa terbaca otomatis"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Kemungkinan RLS aktif. Kamu tetap bisa lanjut dan ketik nama kolom manual di step berikutnya.")))), /* @__PURE__ */ React17.createElement("div", { style: { marginBottom: 12, marginTop: 12 } }, /* @__PURE__ */ React17.createElement("label", { className: "rdb-label" }, "Judul dashboard:"), /* @__PURE__ */ React17.createElement(
2696
+ "input",
2697
+ {
2698
+ className: "rdb-input",
2699
+ value: dashTitle,
2700
+ style: { maxWidth: 280 },
2701
+ onChange: (e) => setDashTitle(e.target.value),
2702
+ placeholder: "cth: Dashboard Laundry / Dashboard Klinik"
2703
+ }
2704
+ )), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ React17.createElement(
2705
+ "button",
2706
+ {
2707
+ type: "button",
2708
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2709
+ onClick: () => setStep(0)
2710
+ },
2711
+ "\u2190 Kembali"
2712
+ ), /* @__PURE__ */ React17.createElement(
2713
+ "button",
2714
+ {
2715
+ type: "button",
2716
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2717
+ disabled: !selectedTable || loadingCols,
2718
+ onClick: () => setStep(2)
2719
+ },
2720
+ "Lanjut \u2192 Konfirmasi Kolom"
2721
+ ))), 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 } }, "Konfirmasi \u2014 Tabel: ", /* @__PURE__ */ React17.createElement("code", { style: { fontSize: "0.9em" } }, selectedTable))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 16 } }, "Wizard sudah mengisi kolom secara otomatis. Periksa sebentar dan sesuaikan jika ada yang salah."), colNames.length > 0 && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-supabase-reader", style: { marginBottom: 16 } }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 6 } }, "Semua kolom di tabel ", selectedTable, ":"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-table-chips" }, colNames.map((c) => /* @__PURE__ */ React17.createElement("span", { key: c, className: "rdb-wizard-chip", style: { cursor: "default" } }, c)))), /* @__PURE__ */ React17.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginBottom: 16 } }, [
2722
+ { label: "Kolom Tanggal *", hint: "Tipe timestamp/date", val: colDate, set: setColDate, ph: "created_at" },
2723
+ { label: "Kolom Status *", hint: "Berisi nilai selesai/pending", val: colStatus, set: setColStatus, ph: "status" },
2724
+ { label: "Kolom Total (uang) *", hint: "Integer Rupiah", val: colTotal, set: setColTotal, ph: "total_amount" },
2725
+ { label: "Kolom Nama Pelanggan", hint: "Opsional", val: colCustomer, set: setColCustomer, ph: "customer_name (opsional)" },
2726
+ { label: "Kolom Nama Item / Layanan", hint: "Opsional", val: colItem, set: setColItem, ph: "product_name (opsional)" }
2727
+ ].map(({ label, hint, val, set, ph }) => /* @__PURE__ */ React17.createElement("div", { key: label }, /* @__PURE__ */ React17.createElement("label", { className: "rdb-label" }, label), colNames.length > 0 ? /* @__PURE__ */ React17.createElement("select", { className: "rdb-select", value: val, onChange: (e) => set(e.target.value) }, /* @__PURE__ */ React17.createElement("option", { value: "" }, "\u2014 pilih \u2014"), colNames.map((c) => /* @__PURE__ */ React17.createElement("option", { key: c, value: c }, c))) : /* @__PURE__ */ React17.createElement("input", { className: "rdb-input", value: val, onChange: (e) => set(e.target.value), placeholder: ph }), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { marginTop: 2 } }, hint)))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-flow", style: { marginBottom: 16 } }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 4 } }, "Nilai status yang ditemukan di data kamu:"), stVals.length > 0 ? /* @__PURE__ */ React17.createElement(React17.Fragment, null, /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { color: "var(--rdb-text-muted)", marginBottom: 8 } }, "Wizard membaca nilai unik dari kolom ", /* @__PURE__ */ React17.createElement("strong", null, colStatus), ". Klik nilai yang sesuai:"), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 12 } }, stVals.map((v) => /* @__PURE__ */ React17.createElement("div", { key: v, style: { display: "flex", gap: 4, alignItems: "center" } }, /* @__PURE__ */ React17.createElement(
2728
+ "button",
2729
+ {
2730
+ type: "button",
2731
+ className: `rdb-btn rdb-btn-sm ${confirmedVal === String(v) ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2732
+ onClick: () => setConfirmedVal(String(v))
2733
+ },
2734
+ confirmedVal === String(v) ? "\u2705 " : "",
2735
+ "Selesai = ",
2736
+ /* @__PURE__ */ React17.createElement("strong", null, String(v))
2737
+ ), /* @__PURE__ */ React17.createElement(
2738
+ "button",
2739
+ {
2740
+ type: "button",
2741
+ className: `rdb-btn rdb-btn-sm ${pendingVal === String(v) ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2742
+ onClick: () => setPendingVal(String(v))
2743
+ },
2744
+ pendingVal === String(v) ? "\u2705 " : "",
2745
+ "Proses = ",
2746
+ /* @__PURE__ */ React17.createElement("strong", null, String(v))
2747
+ ))))) : /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { color: "var(--rdb-text-muted)", marginBottom: 8 } }, "Nilai status tidak terbaca otomatis. Ketik manual:"), /* @__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(
2748
+ "input",
2749
+ {
2750
+ className: "rdb-input",
2751
+ value: confirmedVal,
2752
+ onChange: (e) => setConfirmedVal(e.target.value),
2753
+ placeholder: "confirmed"
2754
+ }
2755
+ )), /* @__PURE__ */ React17.createElement("div", null, /* @__PURE__ */ React17.createElement("label", { className: "rdb-label" }, 'Nilai "Pending/Proses"'), /* @__PURE__ */ React17.createElement(
2756
+ "input",
2757
+ {
2758
+ className: "rdb-input",
2759
+ value: pendingVal,
2760
+ onChange: (e) => setPendingVal(e.target.value),
2761
+ placeholder: "pending"
2762
+ }
2763
+ )))), sample.length > 0 && colDate && colStatus && colTotal && /* @__PURE__ */ React17.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 4 } }, "Preview data nyata sesuai mapping kamu:"), /* @__PURE__ */ React17.createElement(
2764
+ PreviewTable,
2765
+ {
2766
+ rows: sample,
2767
+ cols: [colDate, colStatus, colTotal, colCustomer, colItem].filter((c) => c && c !== "-")
2768
+ }
2769
+ )), !canGen && /* @__PURE__ */ React17.createElement("div", { className: "rdb-error-banner", style: { marginBottom: 12 } }, "Isi Kolom Tanggal, Status, dan Total untuk melanjutkan."), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ React17.createElement(
2770
+ "button",
2771
+ {
2772
+ type: "button",
2773
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2774
+ onClick: () => setStep(1)
2775
+ },
2776
+ "\u2190 Kembali"
2777
+ ), /* @__PURE__ */ React17.createElement(
2778
+ "button",
2779
+ {
2780
+ type: "button",
2781
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2782
+ disabled: !canGen,
2783
+ onClick: handleGenerate
2784
+ },
2785
+ "\u2728 Generate Kode \u2192"
2786
+ ))), 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 Sudah Jadi!")), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-tip", style: { marginBottom: 12 } }, /* @__PURE__ */ React17.createElement("strong", null, "4 file digenerate."), " Klik tab file \u2192 Salin \u2192 Simpan ke lokasi yang tertera di bawah tab. Tidak perlu mengubah isi kode \u2014 langsung save dan jalankan."), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-issues", style: { marginBottom: 12 } }, Object.entries(TAB_INFO).map(([key, info], i) => /* @__PURE__ */ React17.createElement("div", { key, className: "rdb-wizard-issue-item" }, /* @__PURE__ */ React17.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700, minWidth: 18 } }, i + 1, "."), /* @__PURE__ */ React17.createElement("span", { className: "rdb-body" }, "Salin ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, info.label), " \u2192 simpan ke ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, info.path)))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ React17.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700, minWidth: 18 } }, "5."), /* @__PURE__ */ React17.createElement("span", { className: "rdb-body" }, "Di ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "src/index.css"), ", tambahkan:", " ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, '@import "../node_modules/@rozaqi02/reusable-dashboard/dist/index.css";'))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ React17.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700, minWidth: 18 } }, "6."), /* @__PURE__ */ React17.createElement("span", { className: "rdb-body" }, "Restart dev server (", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "npm start"), ") \u2192 buka halaman dashboard \u2192 selesai!"))), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 4, flexWrap: "wrap", marginBottom: 4 } }, Object.entries(TAB_INFO).map(([key, info]) => /* @__PURE__ */ React17.createElement(
2787
+ "button",
2788
+ {
2789
+ key,
2790
+ type: "button",
2791
+ onClick: () => setActiveTab(key),
2792
+ className: `rdb-btn rdb-btn-sm ${activeTab === key ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2793
+ style: { fontFamily: "monospace", fontSize: "0.73rem" }
2794
+ },
2795
+ info.label
2796
+ ))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { color: "var(--rdb-text-muted)", marginBottom: 6 } }, "\u{1F4C1} Simpan ke: ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, (_b = TAB_INFO[activeTab]) == null ? void 0 : _b.path)), /* @__PURE__ */ React17.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React17.createElement(
2797
+ "button",
2798
+ {
2799
+ type: "button",
2800
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2801
+ style: { position: "absolute", top: 8, right: 8, zIndex: 1 },
2802
+ onClick: () => copyCode(generatedCode[activeTab], activeTab)
2803
+ },
2804
+ copied === activeTab ? "\u2705 Tersalin!" : "\u{1F4CB} Salin kode"
2805
+ ), /* @__PURE__ */ React17.createElement("pre", { className: "rdb-wizard-code", style: { maxHeight: 280, overflow: "auto" } }, generatedCode[activeTab])), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-alert", style: { marginTop: 10, marginBottom: 4 } }, /* @__PURE__ */ React17.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u26A0\uFE0F"), /* @__PURE__ */ React17.createElement("div", null, /* @__PURE__ */ React17.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Data tidak muncul di dashboard?"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Jalankan SQL ini di Supabase SQL Editor:"), /* @__PURE__ */ React17.createElement("div", { style: { position: "relative", marginTop: 6 } }, /* @__PURE__ */ React17.createElement(
2806
+ "button",
2807
+ {
2808
+ type: "button",
2809
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2810
+ style: { position: "absolute", top: 4, right: 4 },
2811
+ onClick: () => copyCode(`ALTER TABLE public.${selectedTable} DISABLE ROW LEVEL SECURITY;`, "rlsquick")
2812
+ },
2813
+ copied === "rlsquick" ? "\u2705" : "\u{1F4CB}"
2814
+ ), /* @__PURE__ */ React17.createElement("pre", { className: "rdb-wizard-code", style: { fontSize: "0.72rem" } }, `ALTER TABLE public.${selectedTable} DISABLE ROW LEVEL SECURITY;`)))), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 10 } }, /* @__PURE__ */ React17.createElement(
2815
+ "button",
2816
+ {
2817
+ type: "button",
2818
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2819
+ onClick: () => setStep(2)
2820
+ },
2821
+ "\u2190 Edit mapping"
2822
+ ), /* @__PURE__ */ React17.createElement(
2823
+ "button",
2824
+ {
2825
+ type: "button",
2826
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2827
+ onClick: handleDismiss
2828
+ },
2829
+ "Tutup wizard \u2713"
2830
+ )))))));
2559
2831
  }
2560
2832
  SetupWizard.propTypes = {
2561
2833
  issues: PropTypes17.arrayOf(PropTypes17.string),
@@ -2687,6 +2959,152 @@ ReusableDashboardView.propTypes = {
2687
2959
  dashboardConfig: PropTypes18.object
2688
2960
  };
2689
2961
 
2962
+ // src/presentation/AutoDashboard.jsx
2963
+ import React19 from "react";
2964
+ import PropTypes19 from "prop-types";
2965
+ var DEFAULT_LABELS = {
2966
+ title: "Dashboard",
2967
+ refresh: "Muat ulang",
2968
+ liveUpdate: "Live",
2969
+ loadFailed: "Gagal memuat data dashboard.",
2970
+ retry: "Coba lagi",
2971
+ confirmedOnly: "Hanya berhasil",
2972
+ pendingOnly: "Hanya menunggu",
2973
+ allStatus: "Semua status",
2974
+ showPendingOverlay: "Tampilkan overlay menunggu",
2975
+ allAudience: "Semua segmen",
2976
+ audienceDomestic: "Domestik",
2977
+ audienceForeign: "Asing",
2978
+ customDate: "Custom",
2979
+ reset: "Reset",
2980
+ topSort: "Urutkan item",
2981
+ sortBookings: "Jumlah",
2982
+ sortRevenue: "Nilai",
2983
+ sortDesc: "Turun",
2984
+ sortAsc: "Naik",
2985
+ confirmedBookings: "Transaksi Berhasil",
2986
+ confirmedRevenue: "Pendapatan (Berhasil)",
2987
+ avgRevenue: "Rata-rata Nilai / Transaksi",
2988
+ conversionRate: "Tingkat Konversi",
2989
+ dailyTrends: "Tren Harian",
2990
+ statusDistribution: "Distribusi Status",
2991
+ audienceDistribution: "Distribusi Segmen",
2992
+ topPackages: "Item Teratas",
2993
+ recentBookings: "Transaksi Terbaru",
2994
+ date: "Tanggal",
2995
+ customer: "Pelanggan",
2996
+ package: "Item",
2997
+ audience: "Segmen",
2998
+ total: "Total",
2999
+ status: "Status",
3000
+ noRecentBookings: "Belum ada transaksi terbaru",
3001
+ unknownAudience: "Tidak diketahui"
3002
+ };
3003
+ function buildLabels(overrides = {}) {
3004
+ const labels = { ...DEFAULT_LABELS, ...overrides };
3005
+ labels.dayLabel = overrides.dayLabel || ((count) => count ? `${count} hari` : "Custom");
3006
+ labels.formatStatusLabel = overrides.formatStatusLabel || ((status) => {
3007
+ const s = String(status || "pending");
3008
+ return s.charAt(0).toUpperCase() + s.slice(1);
3009
+ });
3010
+ labels.formatAudienceLabel = overrides.formatAudienceLabel || ((value) => {
3011
+ if (value === "domestic") return labels.audienceDomestic;
3012
+ if (value === "foreign") return labels.audienceForeign;
3013
+ if (!value || value === "unknown") return labels.unknownAudience;
3014
+ return String(value);
3015
+ });
3016
+ return labels;
3017
+ }
3018
+ function AutoDashboard({
3019
+ supabase,
3020
+ table,
3021
+ columns,
3022
+ confirmedValue = "confirmed",
3023
+ pendingValue = "pending",
3024
+ title = "Dashboard",
3025
+ labels: labelOverrides,
3026
+ languageCode = "id",
3027
+ dateLocale = "id-ID"
3028
+ }) {
3029
+ const isConfigured = Boolean(supabase && table && (columns == null ? void 0 : columns.date));
3030
+ const labels = React19.useMemo(
3031
+ () => buildLabels({ ...labelOverrides, title }),
3032
+ [labelOverrides, title]
3033
+ );
3034
+ const widgetConfig = React19.useMemo(
3035
+ () => createUniversalWidgetConfig(columns || {}),
3036
+ [columns]
3037
+ );
3038
+ const dataSource = React19.useMemo(() => {
3039
+ if (!isConfigured) return null;
3040
+ return createUniversalSource(supabase, { table, columns });
3041
+ }, [isConfigured, supabase, table, columns]);
3042
+ const adapter = React19.useMemo(
3043
+ () => (args) => adaptUniversalData({ ...args, options: { confirmedValue, pendingValue } }),
3044
+ [confirmedValue, pendingValue]
3045
+ );
3046
+ const dashboard = useReusableDashboard({
3047
+ config: widgetConfig,
3048
+ dataSource: dataSource || { fetchDashboardSnapshot: null },
3049
+ adapter,
3050
+ createEmptyState: createEmptyUniversalData,
3051
+ languageCode,
3052
+ dateLocale,
3053
+ labels
3054
+ });
3055
+ if (!isConfigured) {
3056
+ const issues = [];
3057
+ if (!supabase) issues.push("Prop 'supabase' belum diisi (Supabase client).");
3058
+ if (!table) issues.push("Prop 'table' belum diisi (nama tabel sumber data).");
3059
+ if (!(columns == null ? void 0 : columns.date))
3060
+ issues.push("Prop 'columns.date' belum diisi (kolom timestamp).");
3061
+ return /* @__PURE__ */ React19.createElement("div", { className: "rdb-view" }, /* @__PURE__ */ React19.createElement(SetupWizard, { issues, supabase, onDismiss: () => {
3062
+ } }));
3063
+ }
3064
+ return /* @__PURE__ */ React19.createElement(
3065
+ ReusableDashboardView,
3066
+ {
3067
+ config: widgetConfig,
3068
+ labels,
3069
+ loading: dashboard.loading,
3070
+ error: dashboard.error,
3071
+ filters: dashboard.filters,
3072
+ onFilterChange: dashboard.updateFilter,
3073
+ onResetFilters: dashboard.resetFilters,
3074
+ onRefresh: dashboard.refresh,
3075
+ data: dashboard.data,
3076
+ dateLocale,
3077
+ liveUpdateEnabled: dashboard.liveUpdateEnabled,
3078
+ supabase
3079
+ }
3080
+ );
3081
+ }
3082
+ AutoDashboard.propTypes = {
3083
+ /** Supabase client instance (WAJIB). */
3084
+ supabase: PropTypes19.object,
3085
+ /** Nama tabel sumber data (WAJIB), mis. "bookings" / "orders". */
3086
+ table: PropTypes19.string,
3087
+ /** Pemetaan kolom: { date, status, total, customer, item, audience }. date WAJIB. */
3088
+ columns: PropTypes19.shape({
3089
+ date: PropTypes19.string,
3090
+ status: PropTypes19.string,
3091
+ total: PropTypes19.string,
3092
+ customer: PropTypes19.string,
3093
+ item: PropTypes19.string,
3094
+ audience: PropTypes19.string
3095
+ }),
3096
+ /** Nilai status yang dihitung "berhasil" (default "confirmed"). */
3097
+ confirmedValue: PropTypes19.string,
3098
+ /** Nilai status yang dihitung "menunggu" (default "pending"). */
3099
+ pendingValue: PropTypes19.string,
3100
+ /** Judul dashboard. */
3101
+ title: PropTypes19.string,
3102
+ /** Override sebagian/seluruh label UI. */
3103
+ labels: PropTypes19.object,
3104
+ languageCode: PropTypes19.string,
3105
+ dateLocale: PropTypes19.string
3106
+ };
3107
+
2690
3108
  // src/utils/labels.js
2691
3109
  function createDashboardLabels(t) {
2692
3110
  const labels = {
@@ -2807,6 +3225,7 @@ function createDashboardLabels(t) {
2807
3225
  return labels;
2808
3226
  }
2809
3227
  export {
3228
+ AutoDashboard,
2810
3229
  Badge,
2811
3230
  Button,
2812
3231
  ChartCard,
@@ -2828,6 +3247,7 @@ export {
2828
3247
  adaptCidikaDashboardData,
2829
3248
  adaptDummyUmkmData,
2830
3249
  adaptTokoSepatuData,
3250
+ adaptUniversalData,
2831
3251
  buildDayBuckets,
2832
3252
  cidikaWidgetConfig,
2833
3253
  createCidikaSupabaseSource,
@@ -2837,7 +3257,10 @@ export {
2837
3257
  createEmptyDashboardData,
2838
3258
  createEmptyDummyUmkmData,
2839
3259
  createEmptyTokoSepatuData,
3260
+ createEmptyUniversalData,
2840
3261
  createTokoSepatuSupabaseSource,
3262
+ createUniversalSource,
3263
+ createUniversalWidgetConfig,
2841
3264
  dummyUmkmWidgetConfig,
2842
3265
  formatDate,
2843
3266
  formatIDR,