@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.cjs CHANGED
@@ -29,6 +29,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // src/index.js
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ AutoDashboard: () => AutoDashboard,
32
33
  Badge: () => Badge,
33
34
  Button: () => Button,
34
35
  ChartCard: () => ChartCard,
@@ -50,6 +51,7 @@ __export(index_exports, {
50
51
  adaptCidikaDashboardData: () => adaptCidikaDashboardData,
51
52
  adaptDummyUmkmData: () => adaptDummyUmkmData,
52
53
  adaptTokoSepatuData: () => adaptTokoSepatuData,
54
+ adaptUniversalData: () => adaptUniversalData,
53
55
  buildDayBuckets: () => buildDayBuckets,
54
56
  cidikaWidgetConfig: () => cidikaWidgetConfig,
55
57
  createCidikaSupabaseSource: () => createCidikaSupabaseSource,
@@ -59,7 +61,10 @@ __export(index_exports, {
59
61
  createEmptyDashboardData: () => createEmptyDashboardData,
60
62
  createEmptyDummyUmkmData: () => createEmptyDummyUmkmData,
61
63
  createEmptyTokoSepatuData: () => createEmptyTokoSepatuData,
64
+ createEmptyUniversalData: () => createEmptyUniversalData,
62
65
  createTokoSepatuSupabaseSource: () => createTokoSepatuSupabaseSource,
66
+ createUniversalSource: () => createUniversalSource,
67
+ createUniversalWidgetConfig: () => createUniversalWidgetConfig,
63
68
  dummyUmkmWidgetConfig: () => dummyUmkmWidgetConfig,
64
69
  formatDate: () => formatDate,
65
70
  formatIDR: () => formatIDR,
@@ -387,6 +392,111 @@ function validateDashboardConfig(dashboardConfig) {
387
392
  return { valid: issues.length === 0, issues };
388
393
  }
389
394
 
395
+ // src/config/createUniversalWidgetConfig.js
396
+ function createUniversalWidgetConfig(columns = {}, opts = {}) {
397
+ const hasAudience = Boolean(columns.audience);
398
+ const hasItem = Boolean(columns.item);
399
+ const hasCustomer = Boolean(columns.customer);
400
+ const hasTotal = Boolean(columns.total);
401
+ const charts = [
402
+ { id: "dailyTrends", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
403
+ { id: "statusDistribution", type: "statusPie", label: "statusDistribution", icon: "PieChart" }
404
+ ];
405
+ if (hasAudience) {
406
+ charts.push({
407
+ id: "audienceDistribution",
408
+ type: "audiencePie",
409
+ label: "audienceDistribution",
410
+ icon: "Users"
411
+ });
412
+ }
413
+ if (hasItem) {
414
+ charts.push({
415
+ id: "topPackages",
416
+ type: "topPackagesBar",
417
+ label: "topPackages",
418
+ icon: "TrendingUp"
419
+ });
420
+ }
421
+ const tableColumns = [
422
+ { id: "date", label: "date", accessor: "createdAt", type: "date" }
423
+ ];
424
+ if (hasCustomer) {
425
+ tableColumns.push({ id: "customer", label: "customer", accessor: "customerName" });
426
+ }
427
+ if (hasItem) {
428
+ tableColumns.push({ id: "package", label: "package", accessor: "packageName" });
429
+ }
430
+ if (hasAudience) {
431
+ tableColumns.push({ id: "audience", label: "audience", accessor: "audienceLabel" });
432
+ }
433
+ if (hasTotal) {
434
+ tableColumns.push({ id: "total", label: "total", accessor: "totalIDR", type: "currency" });
435
+ }
436
+ tableColumns.push({
437
+ id: "status",
438
+ label: "status",
439
+ accessor: "statusLabel",
440
+ type: "statusBadge",
441
+ statusAccessor: "status"
442
+ });
443
+ return {
444
+ id: opts.id || "universal.dashboard",
445
+ defaultFilters: {
446
+ statusScope: "confirmed",
447
+ includePendingOverlay: false,
448
+ audience: "",
449
+ daysPreset: 30,
450
+ sortPkgBy: "bookings",
451
+ sortPkgDir: "desc"
452
+ },
453
+ widgets: {
454
+ stats: [
455
+ {
456
+ id: "bookingsConfirm",
457
+ label: "confirmedBookings",
458
+ icon: "TrendingUp",
459
+ valueKey: "bookingsConfirm",
460
+ format: "number",
461
+ accentColor: "blue"
462
+ },
463
+ {
464
+ id: "revenueConfirm",
465
+ label: "confirmedRevenue",
466
+ icon: "DollarSign",
467
+ valueKey: "revenueConfirm",
468
+ format: "currency",
469
+ accentColor: "green"
470
+ },
471
+ {
472
+ id: "avgRevenue",
473
+ label: "avgRevenue",
474
+ icon: "Users",
475
+ valueKey: "avgRevenue",
476
+ format: "currency",
477
+ accentColor: "violet"
478
+ },
479
+ {
480
+ id: "conversionRate",
481
+ label: "conversionRate",
482
+ icon: "PieChart",
483
+ valueKey: "conversionRate",
484
+ format: "percent",
485
+ accentColor: "orange"
486
+ }
487
+ ],
488
+ charts,
489
+ table: {
490
+ id: "recentBookings",
491
+ label: "recentBookings",
492
+ icon: "Calendar",
493
+ emptyLabel: "noRecentBookings",
494
+ columns: tableColumns
495
+ }
496
+ }
497
+ };
498
+ }
499
+
390
500
  // src/utils/formatters.js
391
501
  function formatIDR(value) {
392
502
  try {
@@ -792,6 +902,131 @@ function adaptTokoSepatuData({ raw, filters, range, dateLocale, labels }) {
792
902
  };
793
903
  }
794
904
 
905
+ // src/data-adapter/universalAdapter.js
906
+ function createEmptyUniversalData() {
907
+ return {
908
+ stats: {
909
+ bookingsConfirm: 0,
910
+ bookingsPending: 0,
911
+ revenueConfirm: 0,
912
+ avgRevenue: 0,
913
+ conversionRate: 0
914
+ },
915
+ charts: {
916
+ dailyTrends: [],
917
+ statusDistribution: [],
918
+ audienceDistribution: [],
919
+ topPackages: []
920
+ },
921
+ table: {
922
+ recentBookings: []
923
+ }
924
+ };
925
+ }
926
+ function adaptUniversalData({
927
+ raw,
928
+ filters = {},
929
+ range,
930
+ dateLocale = "id-ID",
931
+ labels = {},
932
+ options = {}
933
+ }) {
934
+ if (!raw) return createEmptyUniversalData();
935
+ const confirmedValue = String(options.confirmedValue || "confirmed").toLowerCase();
936
+ const pendingValue = String(options.pendingValue || "pending").toLowerCase();
937
+ const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
938
+ const dayLookup = new Map(dailyBuckets.map((bucket) => [bucket.dateKey, bucket]));
939
+ const statusMap = /* @__PURE__ */ new Map();
940
+ const audienceMap = /* @__PURE__ */ new Map();
941
+ const itemCountMap = /* @__PURE__ */ new Map();
942
+ const itemRevenueMap = /* @__PURE__ */ new Map();
943
+ let bookingsConfirm = 0;
944
+ let bookingsPending = 0;
945
+ let revenueConfirm = 0;
946
+ const safeStatusLabel = typeof labels.formatStatusLabel === "function" ? labels.formatStatusLabel : (s) => String(s);
947
+ const safeAudienceLabel = typeof labels.formatAudienceLabel === "function" ? labels.formatAudienceLabel : (a) => String(a);
948
+ (raw.bookings || []).forEach((row) => {
949
+ const dayKey = String(row.created_at || "").slice(0, 10);
950
+ const status = String(row.status || pendingValue).toLowerCase();
951
+ const total = toNumber(row.total_idr);
952
+ const audience = row.audience || "unknown";
953
+ const item = row.item || "-";
954
+ statusMap.set(status, (statusMap.get(status) || 0) + 1);
955
+ audienceMap.set(audience, (audienceMap.get(audience) || 0) + 1);
956
+ const bucket = dayLookup.get(dayKey);
957
+ if (bucket) {
958
+ if (status === confirmedValue) {
959
+ bucket.count += 1;
960
+ bucket.revenue += total;
961
+ }
962
+ if (status === pendingValue) {
963
+ bucket.pendingCount += 1;
964
+ }
965
+ }
966
+ if (status === confirmedValue) {
967
+ bookingsConfirm += 1;
968
+ revenueConfirm += total;
969
+ itemCountMap.set(item, (itemCountMap.get(item) || 0) + 1);
970
+ itemRevenueMap.set(item, (itemRevenueMap.get(item) || 0) + total);
971
+ } else if (status === pendingValue) {
972
+ bookingsPending += 1;
973
+ }
974
+ });
975
+ const statusDistribution = sortMapEntries(statusMap, "desc").map(
976
+ ([status, count]) => ({
977
+ status,
978
+ label: safeStatusLabel(status),
979
+ count
980
+ })
981
+ );
982
+ const audienceDistribution = sortMapEntries(audienceMap, "desc").map(
983
+ ([audience, count]) => ({
984
+ audience,
985
+ label: safeAudienceLabel(audience),
986
+ count
987
+ })
988
+ );
989
+ const metricMap = filters.sortPkgBy === "revenue" ? itemRevenueMap : itemCountMap;
990
+ const topPackages = sortMapEntries(metricMap, filters.sortPkgDir || "desc").slice(0, 5).map(([item, value]) => ({
991
+ packageId: item,
992
+ name: item,
993
+ value: toNumber(value)
994
+ }));
995
+ const recentBookings = (raw.recent || []).map((row) => {
996
+ const status = String(row.status || pendingValue).toLowerCase();
997
+ return {
998
+ id: row.id,
999
+ createdAt: row.created_at,
1000
+ customerName: row.customer_name || "-",
1001
+ packageName: row.item || "-",
1002
+ audienceLabel: safeAudienceLabel(row.audience),
1003
+ totalIDR: toNumber(row.total_idr),
1004
+ status,
1005
+ statusLabel: safeStatusLabel(status)
1006
+ };
1007
+ });
1008
+ const avgRevenue = bookingsConfirm > 0 ? Math.round(revenueConfirm / bookingsConfirm) : 0;
1009
+ const conversionRate = bookingsConfirm + bookingsPending > 0 ? Math.round(bookingsConfirm / (bookingsConfirm + bookingsPending) * 100) : 0;
1010
+ return {
1011
+ stats: {
1012
+ bookingsConfirm,
1013
+ bookingsPending,
1014
+ revenueConfirm,
1015
+ avgRevenue,
1016
+ conversionRate
1017
+ },
1018
+ charts: {
1019
+ dailyTrends: dailyBuckets,
1020
+ statusDistribution,
1021
+ audienceDistribution,
1022
+ topPackages
1023
+ },
1024
+ table: {
1025
+ recentBookings
1026
+ }
1027
+ };
1028
+ }
1029
+
795
1030
  // src/data-source/cidikaSupabaseSource.js
796
1031
  function ensureNoError(response, message) {
797
1032
  if (response == null ? void 0 : response.error) {
@@ -965,6 +1200,84 @@ function createTokoSepatuSupabaseSource(supabase) {
965
1200
  };
966
1201
  }
967
1202
 
1203
+ // src/data-source/universalSupabaseSource.js
1204
+ function createUniversalSource(supabase, { table, columns = {} } = {}) {
1205
+ if (!supabase) {
1206
+ throw new Error("createUniversalSource: prop 'supabase' wajib diisi.");
1207
+ }
1208
+ if (!table) {
1209
+ throw new Error("createUniversalSource: opsi 'table' wajib diisi.");
1210
+ }
1211
+ if (!columns.date) {
1212
+ throw new Error(
1213
+ "createUniversalSource: columns.date wajib diisi (kolom timestamp)."
1214
+ );
1215
+ }
1216
+ const map = {
1217
+ date: columns.date,
1218
+ status: columns.status || null,
1219
+ total: columns.total || null,
1220
+ customer: columns.customer || null,
1221
+ item: columns.item || null,
1222
+ audience: columns.audience || null
1223
+ };
1224
+ const selectCols = Array.from(
1225
+ /* @__PURE__ */ new Set(["id", ...Object.values(map).filter(Boolean)])
1226
+ ).join(", ");
1227
+ const normalize = (row) => ({
1228
+ id: row.id ?? row[map.date],
1229
+ created_at: row[map.date],
1230
+ status: map.status ? row[map.status] : "confirmed",
1231
+ total_idr: map.total ? row[map.total] : 0,
1232
+ item: map.item ? row[map.item] : null,
1233
+ customer_name: map.customer ? row[map.customer] : null,
1234
+ audience: map.audience ? row[map.audience] : "unknown"
1235
+ });
1236
+ return {
1237
+ async fetchDashboardSnapshot({ fromISO, toISO, audience, statusScope }) {
1238
+ const baseQuery = () => supabase.from(table).select(selectCols).gte(map.date, fromISO).lte(map.date, toISO);
1239
+ const bookingsQuery = baseQuery().order(map.date, { ascending: true });
1240
+ if (audience && map.audience) bookingsQuery.eq(map.audience, audience);
1241
+ const recentQuery = baseQuery().order(map.date, { ascending: false }).limit(10);
1242
+ if (audience && map.audience) recentQuery.eq(map.audience, audience);
1243
+ if (statusScope && statusScope !== "all" && map.status) {
1244
+ recentQuery.eq(map.status, statusScope);
1245
+ }
1246
+ const [bookingsRes, recentRes] = await Promise.all([
1247
+ bookingsQuery,
1248
+ recentQuery
1249
+ ]);
1250
+ if (bookingsRes == null ? void 0 : bookingsRes.error) {
1251
+ const err = new Error(
1252
+ `Gagal membaca tabel "${table}". Periksa nama tabel/kolom & RLS policy.`
1253
+ );
1254
+ err.cause = bookingsRes.error;
1255
+ throw err;
1256
+ }
1257
+ const bookings = (bookingsRes.data || []).map(normalize);
1258
+ const recent = (recentRes.error ? [] : recentRes.data || []).map(
1259
+ normalize
1260
+ );
1261
+ return {
1262
+ bookings,
1263
+ recent,
1264
+ packageLocales: [],
1265
+ staticCounts: {}
1266
+ };
1267
+ },
1268
+ subscribeLiveUpdate(onEvent) {
1269
+ const channel = supabase.channel(`reusable-dashboard-universal-${table}`).on(
1270
+ "postgres_changes",
1271
+ { event: "*", schema: "public", table },
1272
+ onEvent
1273
+ ).subscribe();
1274
+ return () => {
1275
+ supabase.removeChannel(channel);
1276
+ };
1277
+ }
1278
+ };
1279
+ }
1280
+
968
1281
  // src/hooks/useReusableDashboard.js
969
1282
  var import_react2 = require("react");
970
1283
 
@@ -1913,80 +2226,95 @@ var import_prop_types18 = __toESM(require("prop-types"), 1);
1913
2226
  var import_react19 = __toESM(require("react"), 1);
1914
2227
  var import_prop_types17 = __toESM(require("prop-types"), 1);
1915
2228
  var PRESET_SIGNATURES = {
1916
- cidika: {
1917
- name: "Cidika Travel",
1918
- tables: ["bookings", "packages", "package_locales"]
1919
- },
1920
- tokoSepatu: {
1921
- name: "Toko Sepatu / E-Commerce",
1922
- tables: ["orders", "products", "customers"]
1923
- }
2229
+ cidika: { name: "Cidika Travel", tables: ["bookings", "packages", "package_locales"] },
2230
+ tokoSepatu: { name: "Toko Sepatu / E-Commerce", tables: ["orders", "products", "customers"] }
1924
2231
  };
1925
2232
  function detectPreset(tables) {
1926
- if (!tables || tables.length === 0) return null;
1927
- const tSet = new Set(tables);
1928
- if (PRESET_SIGNATURES.cidika.tables.every((t) => tSet.has(t))) return "cidika";
1929
- if (PRESET_SIGNATURES.tokoSepatu.tables.every((t) => tSet.has(t))) return "tokoSepatu";
2233
+ if (!(tables == null ? void 0 : tables.length)) return null;
2234
+ const s = new Set(tables);
2235
+ if (PRESET_SIGNATURES.cidika.tables.every((t) => s.has(t))) return "cidika";
2236
+ if (PRESET_SIGNATURES.tokoSepatu.tables.every((t) => s.has(t))) return "tokoSepatu";
1930
2237
  return null;
1931
2238
  }
1932
- function generateCode(mapping) {
1933
- const {
1934
- tableName,
1935
- colDate,
1936
- colStatus,
1937
- colTotal,
1938
- colCustomer,
1939
- colItem,
1940
- dashTitle,
1941
- confirmedValue,
1942
- pendingValue
1943
- } = mapping;
1944
- const safeTitle = dashTitle || "Dashboard";
2239
+ var SQL_GRANT_SCHEMA = `GRANT SELECT ON information_schema.tables TO anon;
2240
+ GRANT SELECT ON information_schema.columns TO anon;`;
2241
+ var sqlRls = (tbl) => `-- Pilih salah satu sesuai kebutuhan (jalankan di Supabase SQL Editor):
2242
+
2243
+ -- Opsi 1: Untuk DEVELOPMENT \u2014 nonaktifkan RLS (paling cepat)
2244
+ ALTER TABLE public.${tbl} DISABLE ROW LEVEL SECURITY;
2245
+
2246
+ -- Opsi 2: Untuk PRODUCTION \u2014 izinkan semua orang baca
2247
+ ALTER TABLE public.${tbl} ENABLE ROW LEVEL SECURITY;
2248
+ CREATE POLICY "dashboard_read" ON public.${tbl}
2249
+ FOR SELECT TO anon USING (true);
2250
+
2251
+ -- Opsi 3: Untuk PRODUCTION \u2014 hanya user yang login
2252
+ ALTER TABLE public.${tbl} ENABLE ROW LEVEL SECURITY;
2253
+ CREATE POLICY "dashboard_auth" ON public.${tbl}
2254
+ FOR SELECT TO authenticated USING (true);`;
2255
+ function generateCode({
2256
+ tableName,
2257
+ colDate,
2258
+ colStatus,
2259
+ colTotal,
2260
+ colCustomer,
2261
+ colItem,
2262
+ dashTitle,
2263
+ confirmedValue,
2264
+ pendingValue
2265
+ }) {
2266
+ const title = dashTitle || "Dashboard";
1945
2267
  const confirmed = confirmedValue || "confirmed";
1946
2268
  const pending = pendingValue || "pending";
1947
- const safeCustomer = colCustomer || "customer_name";
1948
- const safeItem = colItem || "-";
1949
- const colTotalFull = colTotal || "total";
2269
+ const custCol = colCustomer || "customer_name";
2270
+ const itemCol = colItem || "-";
2271
+ const totalCol = colTotal || "total";
2272
+ const cols = [
2273
+ "id",
2274
+ colDate,
2275
+ colStatus,
2276
+ totalCol,
2277
+ ...custCol !== "customer_name" ? [custCol] : ["customer_name"],
2278
+ ...itemCol !== "-" ? [itemCol] : []
2279
+ ].filter(Boolean).join(", ");
1950
2280
  const dataSource = `// src/datasources/myDashboardSource.js
1951
2281
  // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
1952
- // Tabel: ${tableName}
1953
2282
 
1954
2283
  export function createMyDashboardSource(supabase) {
1955
2284
  return {
1956
2285
  async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
1957
- const allQuery = supabase
2286
+ const allQ = supabase
1958
2287
  .from("${tableName}")
1959
- .select("id, ${colDate}, ${colStatus}, ${colTotalFull}${colCustomer !== "customer_name" ? `, ${colCustomer}` : ", customer_name"}${colItem !== "-" ? `, ${colItem}` : ""}")
2288
+ .select("${cols}")
1960
2289
  .gte("${colDate}", fromISO)
1961
2290
  .lte("${colDate}", toISO)
1962
2291
  .order("${colDate}", { ascending: true });
1963
2292
 
1964
- const recentQuery = supabase
2293
+ const recentQ = supabase
1965
2294
  .from("${tableName}")
1966
- .select("id, ${colDate}, ${colStatus}, ${colTotalFull}${colCustomer !== "customer_name" ? `, ${colCustomer}` : ", customer_name"}${colItem !== "-" ? `, ${colItem}` : ""}")
2295
+ .select("${cols}")
1967
2296
  .gte("${colDate}", fromISO)
1968
2297
  .lte("${colDate}", toISO)
1969
2298
  .order("${colDate}", { ascending: false })
1970
2299
  .limit(10);
1971
2300
 
1972
2301
  if (statusScope && statusScope !== "all") {
1973
- recentQuery.eq("${colStatus}", statusScope);
2302
+ recentQ.eq("${colStatus}", statusScope);
1974
2303
  }
1975
2304
 
1976
- const [allRes, recentRes] = await Promise.all([allQuery, recentQuery]);
1977
-
2305
+ const [all, recent] = await Promise.all([allQ, recentQ]);
1978
2306
  return {
1979
- bookings: allRes.data || [], // semua transaksi (untuk chart & stats)
1980
- recent: recentRes.data || [], // 10 terbaru (untuk tabel)
2307
+ bookings: all.data || [],
2308
+ recent: recent.data || [],
1981
2309
  packageLocales: [],
1982
2310
  staticCounts: { packages: 0, sections: 0 },
1983
2311
  };
1984
2312
  },
1985
2313
 
1986
2314
  subscribeLiveUpdate(onEvent) {
1987
- const ch = supabase
1988
- .channel("rdb-dashboard-live")
1989
- .on("postgres_changes", { event: "*", schema: "public", table: "${tableName}" }, onEvent)
2315
+ const ch = supabase.channel("rdb-${tableName}-live")
2316
+ .on("postgres_changes",
2317
+ { event: "*", schema: "public", table: "${tableName}" }, onEvent)
1990
2318
  .subscribe();
1991
2319
  return () => supabase.removeChannel(ch);
1992
2320
  },
@@ -1994,15 +2322,13 @@ export function createMyDashboardSource(supabase) {
1994
2322
  }`;
1995
2323
  const adapter = `// src/adapters/myDashboardAdapter.js
1996
2324
  // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
1997
- // Mapping kolom: status="${colStatus}", total="${colTotalFull}", tanggal="${colDate}"
1998
2325
 
1999
2326
  import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
2000
2327
 
2001
2328
  export function createEmptyMyDashboardData() {
2002
2329
  return {
2003
- stats: { bookingsConfirm: 0, revenueConfirm: 0 },
2004
- charts: { dailyTrends: [], statusDistribution: [],
2005
- audienceDistribution: [], topPackages: [] },
2330
+ stats: { bookingsConfirm: 0, revenueConfirm: 0, avgRevenue: 0, conversionRate: 0 },
2331
+ charts: { dailyTrends: [], statusDistribution: [], audienceDistribution: [], topPackages: [] },
2006
2332
  table: { recentBookings: [] },
2007
2333
  };
2008
2334
  }
@@ -2016,51 +2342,45 @@ export function adaptMyDashboardData({ raw, range, dateLocale, labels }) {
2016
2342
  let confirmed = 0, revenue = 0;
2017
2343
 
2018
2344
  (raw.bookings || []).forEach(row => {
2019
- const status = String(row["${colStatus}"] || "pending").toLowerCase();
2020
- const amount = toNumber(row["${colTotalFull}"]);
2021
- const dayKey = String(row["${colDate}"] || "").slice(0, 10);
2345
+ const st = String(row["${colStatus}"] || "").toLowerCase();
2346
+ const amt = toNumber(row["${totalCol}"]);
2347
+ const dk = String(row["${colDate}"] || "").slice(0, 10);
2022
2348
 
2023
- statusMap.set(status, (statusMap.get(status) || 0) + 1);
2024
- const bucket = dayMap.get(dayKey);
2025
- if (bucket) {
2026
- if (status === "${confirmed}") { bucket.count++; bucket.revenue += amount; }
2027
- if (status === "${pending}") { bucket.pendingCount++; }
2349
+ statusMap.set(st, (statusMap.get(st) || 0) + 1);
2350
+ const b = dayMap.get(dk);
2351
+ if (b) {
2352
+ if (st === "${confirmed}") { b.count++; b.revenue += amt; }
2353
+ if (st === "${pending}") { b.pendingCount++; }
2028
2354
  }
2029
- if (status === "${confirmed}") { confirmed++; revenue += amount; }
2355
+ if (st === "${confirmed}") { confirmed++; revenue += amt; }
2030
2356
  });
2031
2357
 
2032
- const avgRevenue = confirmed > 0 ? Math.round(revenue / confirmed) : 0;
2033
- const totalTx = (raw.bookings || []).length;
2034
- const conversionRate = totalTx > 0 ? Math.round((confirmed / totalTx) * 100) : 0;
2035
-
2358
+ const total = (raw.bookings || []).length;
2036
2359
  return {
2037
2360
  stats: {
2038
2361
  bookingsConfirm: confirmed,
2039
2362
  revenueConfirm: revenue,
2040
- avgRevenue,
2041
- conversionRate,
2363
+ avgRevenue: confirmed > 0 ? Math.round(revenue / confirmed) : 0,
2364
+ conversionRate: total > 0 ? Math.round((confirmed / total) * 100) : 0,
2042
2365
  },
2043
2366
  charts: {
2044
2367
  dailyTrends: buckets,
2045
2368
  statusDistribution: Array.from(statusMap.entries())
2046
2369
  .sort((a, b) => b[1] - a[1])
2047
- .map(([status, count]) => ({
2048
- status, count,
2049
- label: labels?.formatStatusLabel?.(status) || status,
2050
- })),
2370
+ .map(([s, c]) => ({ status: s, count: c, label: labels?.formatStatusLabel?.(s) || s })),
2051
2371
  audienceDistribution: [],
2052
2372
  topPackages: [],
2053
2373
  },
2054
2374
  table: {
2055
2375
  recentBookings: (raw.recent || []).map(row => ({
2056
- id: row.id,
2057
- createdAt: row["${colDate}"],
2058
- customerName: row["${colCustomer}"] || "-",
2059
- packageName: ${colItem !== "-" ? `row["${colItem}"] || "-"` : '"-"'},
2376
+ id: row.id,
2377
+ createdAt: row["${colDate}"],
2378
+ customerName: row["${custCol}"] || "-",
2379
+ packageName: ${itemCol !== "-" ? `row["${itemCol}"] || "-"` : '"-"'},
2060
2380
  audienceLabel: "-",
2061
- totalIDR: toNumber(row["${colTotalFull}"]),
2062
- status: String(row["${colStatus}"] || "pending").toLowerCase(),
2063
- statusLabel: labels?.formatStatusLabel?.(row["${colStatus}"]) || row["${colStatus}"],
2381
+ totalIDR: toNumber(row["${totalCol}"]),
2382
+ status: String(row["${colStatus}"] || "").toLowerCase(),
2383
+ statusLabel: labels?.formatStatusLabel?.(row["${colStatus}"]) || row["${colStatus}"] || "-",
2064
2384
  })),
2065
2385
  },
2066
2386
  };
@@ -2070,151 +2390,120 @@ export function adaptMyDashboardData({ raw, range, dateLocale, labels }) {
2070
2390
 
2071
2391
  export const myDashboardConfig = {
2072
2392
  id: "my.custom.dashboard",
2073
- defaultFilters: {
2074
- statusScope: "${confirmed}",
2075
- daysPreset: 30,
2076
- sortPkgBy: "bookings",
2077
- sortPkgDir: "desc",
2078
- },
2393
+ defaultFilters: { statusScope: "${confirmed}", daysPreset: 30,
2394
+ sortPkgBy: "bookings", sortPkgDir: "desc" },
2079
2395
  widgets: {
2080
2396
  stats: [
2081
- { id: "orders", label: "confirmedBookings", icon: "TrendingUp",
2082
- valueKey: "bookingsConfirm", format: "number", accentColor: "blue" },
2083
- { id: "revenue", label: "confirmedRevenue", icon: "DollarSign",
2084
- valueKey: "revenueConfirm", format: "currency", accentColor: "green" },
2085
- { id: "avg", label: "avgRevenue", icon: "Users",
2086
- valueKey: "avgRevenue", format: "currency", accentColor: "violet" },
2087
- { id: "conversion", label: "conversionRate", icon: "PieChart",
2088
- valueKey: "conversionRate", format: "percent", accentColor: "orange" },
2397
+ { id:"orders", label:"confirmedBookings", icon:"TrendingUp",
2398
+ valueKey:"bookingsConfirm", format:"number", accentColor:"blue" },
2399
+ { id:"revenue", label:"confirmedRevenue", icon:"DollarSign",
2400
+ valueKey:"revenueConfirm", format:"currency", accentColor:"green" },
2401
+ { id:"avg", label:"avgRevenue", icon:"Users",
2402
+ valueKey:"avgRevenue", format:"currency", accentColor:"violet" },
2403
+ { id:"conversion", label:"conversionRate", icon:"PieChart",
2404
+ valueKey:"conversionRate", format:"percent", accentColor:"orange" },
2089
2405
  ],
2090
2406
  charts: [
2091
- { id: "trend", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
2092
- { id: "status", type: "statusPie", label: "statusDistribution", icon: "PieChart" },
2407
+ { id:"trend", type:"dailyArea", label:"dailyTrends", icon:"BarChart3" },
2408
+ { id:"status", type:"statusPie", label:"statusDistribution", icon:"PieChart" },
2093
2409
  ],
2094
2410
  table: {
2095
- id: "recentTx", label: "recentBookings", icon: "Calendar",
2096
- emptyLabel: "noRecentBookings",
2411
+ id:"recentTx", label:"recentBookings", icon:"Calendar", emptyLabel:"noRecentBookings",
2097
2412
  columns: [
2098
- { id: "date", label: "date", accessor: "createdAt", type: "date" },
2099
- { id: "customer", label: "customer", accessor: "customerName" },${colItem !== "-" ? `
2100
- { id: "item", label: "package", accessor: "packageName" },` : ""}
2101
- { id: "total", label: "total", accessor: "totalIDR", type: "currency" },
2102
- { id: "status", label: "status", accessor: "statusLabel",
2103
- type: "statusBadge", statusAccessor: "status" },
2413
+ { id:"date", label:"date", accessor:"createdAt", type:"date" },
2414
+ { id:"customer", label:"customer", accessor:"customerName" },${itemCol !== "-" ? `
2415
+ { id:"item", label:"package", accessor:"packageName" },` : ""}
2416
+ { id:"total", label:"total", accessor:"totalIDR", type:"currency" },
2417
+ { id:"status", label:"status", accessor:"statusLabel",
2418
+ type:"statusBadge", statusAccessor:"status" },
2104
2419
  ],
2105
2420
  },
2106
2421
  },
2107
2422
  };`;
2108
2423
  const dashboard = `// src/pages/admin/Dashboard.jsx
2109
2424
  // AUTO-GENERATED oleh Setup Wizard @rozaqi02/reusable-dashboard
2110
- // Salin file ini ke halaman dashboard kamu.
2111
2425
 
2112
2426
  import React from "react";
2113
2427
  import { supabase } from "../../lib/supabaseClient.js";
2114
2428
  import {
2115
- ReusableDashboardView,
2116
- useReusableDashboard,
2117
- createDashboardConfig,
2429
+ ReusableDashboardView, useReusableDashboard, createDashboardConfig,
2118
2430
  } from "@rozaqi02/reusable-dashboard";
2119
2431
 
2120
- // Import 3 file yang digenerate wizard
2121
- import { myDashboardConfig } from "./myDashboardConfig";
2122
- import { createMyDashboardSource } from "./myDashboardSource";
2123
- import { adaptMyDashboardData, createEmptyMyDashboardData } from "./myDashboardAdapter";
2432
+ // Tiga file hasil generate wizard
2433
+ import { myDashboardConfig } from "../../config/myDashboardConfig";
2434
+ import { createMyDashboardSource } from "../../datasources/myDashboardSource";
2435
+ import { adaptMyDashboardData, createEmptyMyDashboardData } from "../../adapters/myDashboardAdapter";
2124
2436
 
2125
- // Data source \u2014 dibuat sekali di luar komponen
2126
2437
  const source = createMyDashboardSource(supabase);
2127
2438
 
2128
- // Label UI \u2014 sesuaikan teks dengan bahasa/konteks bisnis kamu
2129
2439
  const labels = {
2130
- title: "${safeTitle}",
2131
- refresh: "Refresh",
2132
- liveUpdate: "Live update",
2133
- loadFailed: "Gagal memuat data.",
2134
- retry: "Coba Lagi",
2135
- confirmedOnly: "Selesai",
2136
- pendingOnly: "Proses",
2137
- allStatus: "Semua Status",
2138
- showPendingOverlay: "Tampilkan pending",
2139
- reset: "Reset",
2140
- confirmedBookings: "Total Transaksi",
2141
- confirmedRevenue: "Total Pendapatan",
2142
- avgRevenue: "Rata-rata / Transaksi",
2143
- conversionRate: "Conversion Rate",
2144
- dailyTrends: "Tren Harian",
2145
- statusDistribution: "Distribusi Status",
2146
- recentBookings: "Transaksi Terbaru",
2147
- noRecentBookings: "Belum ada transaksi",
2148
- date: "Tanggal",
2149
- customer: "Pelanggan",
2150
- package: "Item",
2151
- total: "Total",
2152
- status: "Status",
2153
- bookingsMetric: "Transaksi",
2154
- revenueMetric: "Pendapatan",
2440
+ title: "${title}", refresh: "Refresh", liveUpdate: "Live update",
2441
+ loadFailed: "Gagal memuat data.", retry: "Coba Lagi",
2442
+ confirmedOnly: "Selesai", pendingOnly: "Proses", allStatus: "Semua Status",
2443
+ showPendingOverlay: "Tampilkan pending", reset: "Reset",
2444
+ confirmedBookings: "Total Transaksi", confirmedRevenue: "Total Pendapatan",
2445
+ avgRevenue: "Rata-rata / Transaksi", conversionRate: "Conversion Rate",
2446
+ dailyTrends: "Tren Harian", statusDistribution: "Distribusi Status",
2447
+ recentBookings: "Transaksi Terbaru", noRecentBookings: "Belum ada transaksi",
2448
+ date: "Tanggal", customer: "Pelanggan", package: "Item",
2449
+ total: "Total", status: "Status",
2450
+ bookingsMetric: "Transaksi", revenueMetric: "Pendapatan",
2155
2451
  confirmedBookingMetric: "Transaksi (Selesai)",
2156
2452
  confirmedRevenueMetric: "Pendapatan (Selesai)",
2157
2453
  dayLabel: n => n + " hari",
2158
- formatStatusLabel: s =>
2159
- ({ "${confirmed}": "Selesai", "${pending}": "Proses" })[s] || (s || "-"),
2454
+ formatStatusLabel: s => ({ "${confirmed}":"Selesai", "${pending}":"Proses" })[s] || s || "-",
2160
2455
  formatAudienceLabel: v => v || "-",
2161
2456
  };
2162
2457
 
2163
- // Kemas semua jadi 1 objek
2164
2458
  const dashboardConfig = createDashboardConfig({
2165
- widgetConfig: myDashboardConfig,
2166
- dataSource: source,
2167
- adapter: adaptMyDashboardData,
2168
- createEmptyState: createEmptyMyDashboardData,
2169
- languageCode: "id",
2170
- dateLocale: "id-ID",
2171
- labels,
2459
+ widgetConfig: myDashboardConfig, dataSource: source,
2460
+ adapter: adaptMyDashboardData, createEmptyState: createEmptyMyDashboardData,
2461
+ languageCode: "id", dateLocale: "id-ID", labels,
2172
2462
  });
2173
2463
 
2174
2464
  export default function Dashboard() {
2175
2465
  const state = useReusableDashboard({ ...dashboardConfig, labels });
2176
2466
  return (
2177
2467
  <ReusableDashboardView
2178
- config={dashboardConfig.config}
2179
- labels={labels}
2180
- loading={state.loading}
2181
- error={state.error}
2182
- filters={state.filters}
2183
- onFilterChange={state.updateFilter}
2184
- onResetFilters={state.resetFilters}
2185
- onRefresh={state.refresh}
2186
- data={state.data}
2187
- dateLocale={dashboardConfig.dateLocale}
2468
+ config={dashboardConfig.config} labels={labels}
2469
+ loading={state.loading} error={state.error}
2470
+ filters={state.filters} onFilterChange={state.updateFilter}
2471
+ onResetFilters={state.resetFilters} onRefresh={state.refresh}
2472
+ data={state.data} dateLocale={dashboardConfig.dateLocale}
2188
2473
  liveUpdateEnabled={state.liveUpdateEnabled}
2189
- supabase={supabase}
2190
- dashboardConfig={dashboardConfig}
2474
+ supabase={supabase} dashboardConfig={dashboardConfig}
2191
2475
  />
2192
2476
  );
2193
2477
  }`;
2194
2478
  return { dataSource, adapter, widgetConfig, dashboard };
2195
2479
  }
2196
2480
  function SetupWizard({ issues = [], onDismiss, supabase }) {
2481
+ var _a, _b;
2197
2482
  const [dismissed, setDismissed] = (0, import_react19.useState)(false);
2198
2483
  const [step, setStep] = (0, import_react19.useState)(0);
2199
2484
  const [detecting, setDetecting] = (0, import_react19.useState)(true);
2200
2485
  const [tables, setTables] = (0, import_react19.useState)([]);
2201
2486
  const [columns, setColumns] = (0, import_react19.useState)({});
2487
+ const [sampleData, setSampleData] = (0, import_react19.useState)({});
2488
+ const [statusValues, setStatusValues] = (0, import_react19.useState)({});
2202
2489
  const [supabaseOk, setSupabaseOk] = (0, import_react19.useState)(false);
2203
2490
  const [detectionDone, setDetectionDone] = (0, import_react19.useState)(false);
2204
- const [userHasDashboard, setUserHasDashboard] = (0, import_react19.useState)(null);
2491
+ const [tableBlocked, setTableBlocked] = (0, import_react19.useState)(false);
2205
2492
  const [selectedTable, setSelectedTable] = (0, import_react19.useState)("");
2206
2493
  const [colDate, setColDate] = (0, import_react19.useState)("");
2207
2494
  const [colStatus, setColStatus] = (0, import_react19.useState)("");
2208
2495
  const [colTotal, setColTotal] = (0, import_react19.useState)("");
2209
2496
  const [colCustomer, setColCustomer] = (0, import_react19.useState)("");
2210
2497
  const [colItem, setColItem] = (0, import_react19.useState)("");
2211
- const [confirmedVal, setConfirmedVal] = (0, import_react19.useState)("confirmed");
2212
- const [pendingVal, setPendingVal] = (0, import_react19.useState)("pending");
2498
+ const [confirmedVal, setConfirmedVal] = (0, import_react19.useState)("");
2499
+ const [pendingVal, setPendingVal] = (0, import_react19.useState)("");
2213
2500
  const [dashTitle, setDashTitle] = (0, import_react19.useState)("Dashboard");
2214
- const [loadingColumns, setLoadingColumns] = (0, import_react19.useState)(false);
2501
+ const [loadingCols, setLoadingCols] = (0, import_react19.useState)(false);
2502
+ const [colsBlocked, setColsBlocked] = (0, import_react19.useState)(false);
2215
2503
  const [generatedCode, setGeneratedCode] = (0, import_react19.useState)(null);
2216
- const [activeCodeTab, setActiveCodeTab] = (0, import_react19.useState)("dataSource");
2504
+ const [activeTab, setActiveTab] = (0, import_react19.useState)("dashboard");
2217
2505
  const [copied, setCopied] = (0, import_react19.useState)("");
2506
+ const [showRls, setShowRls] = (0, import_react19.useState)(false);
2218
2507
  (0, import_react19.useEffect)(() => {
2219
2508
  if (!supabase) {
2220
2509
  setDetecting(false);
@@ -2223,388 +2512,376 @@ function SetupWizard({ issues = [], onDismiss, supabase }) {
2223
2512
  }
2224
2513
  (async () => {
2225
2514
  try {
2226
- const { data: rpcData, error: rpcErr } = await supabase.rpc("rdb_get_tables");
2227
- if (!rpcErr && Array.isArray(rpcData)) {
2228
- setTables(rpcData.map((r) => typeof r === "string" ? r : r.table_name || r.name || r));
2515
+ 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");
2516
+ if (!e1 && (d1 == null ? void 0 : d1.length)) {
2517
+ setTables(d1.map((r) => r.table_name));
2229
2518
  setSupabaseOk(true);
2519
+ return;
2520
+ }
2521
+ const { data: d2, error: e2 } = await supabase.rpc("rdb_get_tables");
2522
+ if (!e2 && Array.isArray(d2)) {
2523
+ setTables(d2.map((r) => typeof r === "string" ? r : r.table_name || r));
2524
+ setSupabaseOk(true);
2525
+ return;
2526
+ }
2527
+ const { error: ping } = await supabase.from("_rdb_wizard_ping_").select("*").limit(1);
2528
+ 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"))) {
2529
+ setSupabaseOk(true);
2530
+ setTableBlocked(true);
2230
2531
  } else {
2231
- const { data: schemaData, error: schemaErr } = await supabase.from("information_schema.tables").select("table_name").eq("table_schema", "public").eq("table_type", "BASE TABLE").order("table_name");
2232
- if (!schemaErr && schemaData) {
2233
- setTables(schemaData.map((r) => r.table_name));
2234
- setSupabaseOk(true);
2235
- } else {
2236
- const { error: pingErr } = await supabase.from("_rdb_ping_").select("*").limit(1);
2237
- const connected = !pingErr || pingErr.code === "42P01" || pingErr.code === "PGRST116" || (pingErr.message || "").includes("does not exist");
2238
- setSupabaseOk(connected);
2239
- setTables([]);
2240
- }
2532
+ setSupabaseOk(!!supabase);
2533
+ setTableBlocked(true);
2241
2534
  }
2242
2535
  } catch {
2243
- setSupabaseOk(false);
2536
+ setSupabaseOk(!!supabase);
2537
+ setTableBlocked(true);
2244
2538
  } finally {
2245
2539
  setDetecting(false);
2246
2540
  setDetectionDone(true);
2247
2541
  }
2248
2542
  })();
2249
2543
  }, [supabase]);
2250
- const loadColumns = (0, import_react19.useCallback)(async (tbl) => {
2251
- if (!supabase || !tbl || columns[tbl]) return;
2252
- setLoadingColumns(true);
2544
+ const analyzeTable = (0, import_react19.useCallback)(async (tbl) => {
2545
+ var _a2;
2546
+ if (!supabase || !tbl) return;
2547
+ if (columns[tbl]) {
2548
+ autoDetect(tbl, columns[tbl]);
2549
+ return;
2550
+ }
2551
+ setLoadingCols(true);
2552
+ setColsBlocked(false);
2253
2553
  try {
2254
- const { data: rpcCols, error: rpcErr } = await supabase.rpc("rdb_get_columns", { p_table: tbl });
2255
- if (!rpcErr && Array.isArray(rpcCols)) {
2256
- const colObjs = rpcCols.map(
2257
- (c) => typeof c === "string" ? { column_name: c, data_type: "unknown" } : { column_name: c.column_name || c.name || c, data_type: c.data_type || "unknown" }
2258
- );
2259
- setColumns((prev) => ({ ...prev, [tbl]: colObjs }));
2260
- autoDetectCols(colObjs);
2261
- return;
2554
+ let cols = null;
2555
+ 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");
2556
+ if (!ea && (ca == null ? void 0 : ca.length)) cols = ca;
2557
+ if (!cols) {
2558
+ const { data: sb, error: eb } = await supabase.from(tbl).select("*").limit(1);
2559
+ if (!eb && (sb == null ? void 0 : sb.length) > 0)
2560
+ cols = Object.keys(sb[0]).map((k) => ({ column_name: k, data_type: typeof sb[0][k] }));
2262
2561
  }
2263
- const { data: schemaCols, error: schemaErr } = await supabase.from("information_schema.columns").select("column_name, data_type").eq("table_schema", "public").eq("table_name", tbl).order("ordinal_position");
2264
- if (!schemaErr && schemaCols) {
2265
- setColumns((prev) => ({ ...prev, [tbl]: schemaCols }));
2266
- autoDetectCols(schemaCols);
2267
- return;
2562
+ if (cols) {
2563
+ setColumns((p) => ({ ...p, [tbl]: cols }));
2564
+ autoDetect(tbl, cols);
2565
+ } else {
2566
+ setColsBlocked(true);
2268
2567
  }
2269
- const { data: sampleRow, error: sampleErr } = await supabase.from(tbl).select("*").limit(1);
2270
- if (!sampleErr && sampleRow && sampleRow.length > 0) {
2271
- const colObjs = Object.keys(sampleRow[0]).map((k) => ({
2272
- column_name: k,
2273
- data_type: typeof sampleRow[0][k]
2274
- }));
2275
- setColumns((prev) => ({ ...prev, [tbl]: colObjs }));
2276
- autoDetectCols(colObjs);
2277
- return;
2568
+ const { data: sample2 } = await supabase.from(tbl).select("*").limit(5);
2569
+ if ((sample2 == null ? void 0 : sample2.length) > 0) {
2570
+ setSampleData((p) => ({ ...p, [tbl]: sample2 }));
2571
+ const statusCol = (_a2 = (cols || []).find(
2572
+ (c) => ["status", "state", "kondisi", "order_status"].includes(c.column_name.toLowerCase())
2573
+ )) == null ? void 0 : _a2.column_name;
2574
+ if (statusCol) {
2575
+ const uniqVals = [...new Set(sample2.map((r) => r[statusCol]).filter(Boolean))];
2576
+ setStatusValues((p) => ({ ...p, [tbl]: { [statusCol]: uniqVals } }));
2577
+ const vals = uniqVals.map((v) => String(v).toLowerCase());
2578
+ const cfm = uniqVals.find((v) => ["confirmed", "selesai", "lunas", "paid", "done", "completed", "approve"].includes(String(v).toLowerCase()));
2579
+ const pnd = uniqVals.find((v) => ["pending", "proses", "menunggu", "processing", "waiting", "draft"].includes(String(v).toLowerCase()));
2580
+ if (cfm) setConfirmedVal(String(cfm));
2581
+ if (pnd) setPendingVal(String(pnd));
2582
+ }
2278
2583
  }
2279
- setColumns((prev) => ({ ...prev, [tbl]: [] }));
2280
2584
  } catch {
2281
- setColumns((prev) => ({ ...prev, [tbl]: [] }));
2585
+ setColsBlocked(true);
2282
2586
  } finally {
2283
- setLoadingColumns(false);
2587
+ setLoadingCols(false);
2284
2588
  }
2285
2589
  }, [supabase, columns]);
2286
- function autoDetectCols(colObjs) {
2287
- const find = (candidates) => {
2288
- var _a;
2289
- return ((_a = colObjs.find((c) => candidates.includes(c.column_name.toLowerCase()))) == null ? void 0 : _a.column_name) || "";
2590
+ function autoDetect(tbl, cols) {
2591
+ const f = (opts) => {
2592
+ var _a2;
2593
+ return ((_a2 = cols.find((c) => opts.includes(c.column_name.toLowerCase()))) == null ? void 0 : _a2.column_name) || "";
2290
2594
  };
2291
- setColDate(find(["created_at", "tanggal", "date", "transaction_date", "order_date", "waktu"]));
2292
- setColStatus(find(["status", "state", "kondisi", "order_status", "payment_status"]));
2293
- setColTotal(find(["total", "total_idr", "total_amount", "total_price", "harga_total", "amount", "nominal", "harga", "biaya"]));
2294
- setColCustomer(find(["customer_name", "nama_pelanggan", "nama", "name", "client_name", "buyer_name", "pelanggan"]));
2295
- setColItem(find(["service_type", "item_name", "product_name", "package_name", "layanan", "produk", "nama_layanan", "jenis", "nama_produk"]));
2595
+ setColDate(f(["created_at", "tanggal", "date", "transaction_date", "order_date", "waktu"]));
2596
+ setColStatus(f(["status", "state", "kondisi", "order_status", "payment_status"]));
2597
+ setColTotal(f(["total", "total_idr", "total_amount", "total_price", "harga_total", "amount", "nominal", "harga", "grand_total"]));
2598
+ setColCustomer(f(["customer_name", "nama_pelanggan", "nama", "name", "client_name", "buyer_name", "pelanggan"]));
2599
+ setColItem(f(["service_type", "item_name", "product_name", "package_name", "layanan", "produk", "jenis", "paket"]));
2296
2600
  }
2297
- function copyCode(text, label) {
2298
- navigator.clipboard.writeText(text).then(() => {
2299
- setCopied(label);
2300
- setTimeout(() => setCopied(""), 2e3);
2601
+ function copyCode(text, key) {
2602
+ var _a2;
2603
+ (_a2 = navigator.clipboard) == null ? void 0 : _a2.writeText(text).then(() => {
2604
+ setCopied(key);
2605
+ setTimeout(() => setCopied(""), 2200);
2301
2606
  });
2302
2607
  }
2303
2608
  function handleDismiss() {
2304
2609
  setDismissed(true);
2305
- if (onDismiss) onDismiss();
2610
+ onDismiss == null ? void 0 : onDismiss();
2306
2611
  }
2307
2612
  function handleGenerate() {
2308
2613
  if (!selectedTable || !colDate || !colStatus || !colTotal) return;
2309
- const code = generateCode({
2310
- tableName: selectedTable,
2614
+ setGeneratedCode(generateCode({
2615
+ tableName: selectedTable.trim(),
2311
2616
  colDate,
2312
2617
  colStatus,
2313
2618
  colTotal,
2314
2619
  colCustomer: colCustomer || "customer_name",
2315
2620
  colItem: colItem || "-",
2316
2621
  dashTitle,
2317
- confirmedValue: confirmedVal,
2318
- pendingValue: pendingVal
2319
- });
2320
- setGeneratedCode(code);
2622
+ confirmedValue: confirmedVal || "confirmed",
2623
+ pendingValue: pendingVal || "pending"
2624
+ }));
2321
2625
  setStep(3);
2322
2626
  }
2323
2627
  if (dismissed) return null;
2324
- const detectedPreset = detectPreset(tables);
2325
- const tableColumns = selectedTable ? columns[selectedTable] || [] : [];
2326
- const colNames = tableColumns.map((c) => c.column_name);
2327
- const mappingValid = selectedTable && colDate && colStatus && colTotal;
2328
- const stepLabels = ["Deteksi", "Pilih Tabel", "Mapping Kolom", "Kode Siap Pakai"];
2329
- return /* @__PURE__ */ import_react19.default.createElement(
2330
- "div",
2628
+ const preset = detectPreset(tables);
2629
+ const tblCols = selectedTable ? columns[selectedTable] || [] : [];
2630
+ const colNames = tblCols.map((c) => c.column_name);
2631
+ const sample = selectedTable ? sampleData[selectedTable] || [] : [];
2632
+ const stVals = selectedTable && colStatus ? ((_a = statusValues[selectedTable]) == null ? void 0 : _a[colStatus]) || [] : [];
2633
+ const canGen = selectedTable.trim() && colDate && colStatus && colTotal;
2634
+ const TAB_INFO = {
2635
+ dashboard: { label: "Dashboard.jsx", path: "src/pages/admin/Dashboard.jsx" },
2636
+ dataSource: { label: "myDashboardSource.js", path: "src/datasources/myDashboardSource.js" },
2637
+ adapter: { label: "myDashboardAdapter.js", path: "src/adapters/myDashboardAdapter.js" },
2638
+ widgetConfig: { label: "myDashboardConfig.js", path: "src/config/myDashboardConfig.js" }
2639
+ };
2640
+ function PreviewTable({ rows, cols: previewCols }) {
2641
+ if (!(rows == null ? void 0 : rows.length) || !(previewCols == null ? void 0 : previewCols.length)) return null;
2642
+ const validCols = previewCols.filter((c) => {
2643
+ var _a2;
2644
+ return c && ((_a2 = rows[0]) == null ? void 0 : _a2[c]) !== void 0;
2645
+ });
2646
+ if (!validCols.length) return null;
2647
+ return /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-table-wrapper", style: { marginTop: 8, fontSize: "0.75rem" } }, /* @__PURE__ */ import_react19.default.createElement("table", { className: "rdb-table", style: { fontSize: "0.75rem" } }, /* @__PURE__ */ import_react19.default.createElement("thead", null, /* @__PURE__ */ import_react19.default.createElement("tr", null, validCols.map((c) => /* @__PURE__ */ import_react19.default.createElement("th", { key: c, style: { padding: "4px 8px", fontSize: "0.7rem" } }, c)))), /* @__PURE__ */ import_react19.default.createElement("tbody", null, rows.slice(0, 3).map((row, i) => /* @__PURE__ */ import_react19.default.createElement("tr", { key: i }, validCols.map((c) => /* @__PURE__ */ import_react19.default.createElement("td", { key: c, style: { padding: "3px 8px" } }, String(row[c] ?? "-").slice(0, 30))))))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 4, color: "var(--rdb-text-muted)" } }, "Preview 3 baris data nyata dari database kamu"));
2648
+ }
2649
+ return /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-overlay", role: "dialog", "aria-modal": "true" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-modal" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-header" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-header-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-logo" }, "\u{1F6E0}\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-h2", style: { margin: 0 } }, "Setup Dashboard Wizard"), /* @__PURE__ */ import_react19.default.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__ */ import_react19.default.createElement("button", { type: "button", className: "rdb-wizard-close", onClick: handleDismiss }, "\u2715")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-steps" }, ["1. Koneksi", "2. Pilih Tabel", "3. Konfirmasi", "4. Kode Jadi"].map((lbl, i) => /* @__PURE__ */ import_react19.default.createElement(
2650
+ "button",
2331
2651
  {
2332
- className: "rdb-wizard-overlay",
2333
- role: "dialog",
2334
- "aria-modal": "true",
2335
- "aria-label": "Setup Wizard @rozaqi02/reusable-dashboard"
2652
+ key: i,
2653
+ type: "button",
2654
+ className: `rdb-wizard-step-btn ${step === i ? "rdb-wizard-step-active" : ""}`,
2655
+ onClick: () => setStep(i)
2336
2656
  },
2337
- /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-modal" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-header" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-header-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-logo" }, "\u{1F6E0}\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-h2", style: { margin: 0 } }, "Setup Dashboard"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption" }, detecting ? "Mendeteksi kondisi project\u2026" : supabaseOk ? `Supabase tersambung \xB7 ${tables.length} tabel ditemukan` : "Panduan konfigurasi @rozaqi02/reusable-dashboard"))), /* @__PURE__ */ import_react19.default.createElement(
2338
- "button",
2339
- {
2340
- type: "button",
2341
- className: "rdb-wizard-close",
2342
- onClick: handleDismiss,
2343
- title: "Tutup"
2344
- },
2345
- "\u2715"
2346
- )), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-steps" }, stepLabels.map((label, i) => /* @__PURE__ */ import_react19.default.createElement(
2347
- "button",
2348
- {
2349
- key: i,
2350
- type: "button",
2351
- className: `rdb-wizard-step-btn ${step === i ? "rdb-wizard-step-active" : ""}`,
2352
- onClick: () => {
2353
- if (i < step || i === step + 1 && step < 2) setStep(i);
2354
- }
2355
- },
2356
- /* @__PURE__ */ import_react19.default.createElement("span", null, step > i ? "\u2705" : i + 1, "."),
2357
- /* @__PURE__ */ import_react19.default.createElement("span", null, label)
2358
- ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-body" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-section" }, step === 0 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, issues.length > 0 && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issues", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 6 } }, "Konfigurasi belum lengkap:"), issues.map((iss, i) => /* @__PURE__ */ import_react19.default.createElement("div", { key: i, className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, "\u274C"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, iss)))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 10 } }, "\u{1F50D} Hasil deteksi otomatis:"), detecting ? /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)" } }, "Mendeteksi\u2026") : /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, supabaseOk ? "\u2705" : "\u274C"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Supabase ", supabaseOk ? "tersambung" : "tidak tersambung \u2014 pastikan supabaseClient.js sudah dikonfigurasi dan prop supabase dikirim ke ReusableDashboardView")), supabaseOk && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, tables.length > 0 ? "\u2705" : "\u26A0\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, tables.length > 0 ? `${tables.length} tabel public ditemukan` : "Daftar tabel tidak bisa dibaca otomatis (RLS aktif) \u2014 kamu bisa ketik nama tabel di step berikutnya")), supabaseOk && tables.length > 0 && detectedPreset && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, "\u2705"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Tabel cocok dengan preset ", /* @__PURE__ */ import_react19.default.createElement("strong", null, PRESET_SIGNATURES[detectedPreset].name))))), !supabaseOk && detectionDone && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-alert", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u2139\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Aktifkan koneksi Supabase"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Tambahkan prop ke ReusableDashboardView:", " ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "supabase=", "{supabase}")))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ import_react19.default.createElement(
2359
- "button",
2360
- {
2361
- type: "button",
2362
- className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2363
- onClick: handleDismiss
2364
- },
2365
- "Lanjutkan tanpa wizard"
2366
- ), supabaseOk && /* @__PURE__ */ import_react19.default.createElement(
2367
- "button",
2368
- {
2369
- type: "button",
2370
- className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2371
- onClick: () => setStep(1)
2372
- },
2373
- "Lanjut \u2192 Pilih Tabel"
2374
- ))), step === 1 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "2"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Pilih Tabel Transaksi")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 16 } }, "Pilih tabel yang berisi data transaksi bisnis kamu."), tables.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 8 } }, "Tabel yang ditemukan:"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginBottom: 16 } }, tables.map((tbl) => /* @__PURE__ */ import_react19.default.createElement(
2657
+ step > i ? "\u2705 " : "",
2658
+ lbl
2659
+ ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-body" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-section" }, step === 0 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, issues.length > 0 && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issues", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 6 } }, "Konfigurasi yang belum lengkap:"), issues.map((iss, i) => /* @__PURE__ */ import_react19.default.createElement("div", { key: i, className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, "\u274C"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, iss)))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow", style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 10 } }, "\u{1F50D} Status koneksi Supabase:"), detecting ? /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)" } }, "Mendeteksi\u2026") : /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, supabaseOk ? "\u2705" : "\u274C"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, supabaseOk ? "Supabase tersambung" : "Tidak tersambung \u2014 tambahkan prop supabase={supabase} ke <ReusableDashboardView>")), supabaseOk && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", null, tables.length > 0 ? "\u2705" : "\u26A0\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, tables.length > 0 ? `Wizard membaca ${tables.length} tabel langsung dari database` : "Daftar tabel belum bisa terbaca (butuh izin \u2014 lihat SQL di bawah)")))), supabaseOk && tableBlocked && /* @__PURE__ */ import_react19.default.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-alert", style: { marginBottom: 8 } }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u{1F511}"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Agar wizard bisa baca daftar tabel otomatis"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Jalankan SQL ini ", /* @__PURE__ */ import_react19.default.createElement("strong", null, "sekali"), " di Supabase SQL Editor, lalu refresh halaman. Atau lewati dan ketik nama tabel manual di Step 2."))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ import_react19.default.createElement(
2660
+ "button",
2661
+ {
2662
+ type: "button",
2663
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2664
+ style: { position: "absolute", top: 6, right: 6 },
2665
+ onClick: () => copyCode(SQL_GRANT_SCHEMA, "grant")
2666
+ },
2667
+ copied === "grant" ? "\u2705" : "\u{1F4CB}",
2668
+ " Salin"
2669
+ ), /* @__PURE__ */ import_react19.default.createElement("pre", { className: "rdb-wizard-code", style: { fontSize: "0.72rem" } }, SQL_GRANT_SCHEMA))), supabaseOk && /* @__PURE__ */ import_react19.default.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement(
2670
+ "button",
2671
+ {
2672
+ type: "button",
2673
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2674
+ onClick: () => setShowRls((r) => !r)
2675
+ },
2676
+ showRls ? "\u25B2 Sembunyikan" : "\u25BC Lihat",
2677
+ " panduan RLS \u2014 agar data tabel bisa terbaca dashboard"
2678
+ ), showRls && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow", style: { marginTop: 10 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { fontWeight: 600, marginBottom: 6 } }, "Apa itu RLS?"), /* @__PURE__ */ import_react19.default.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__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 6 } }, "SQL \u2014 ganti ", /* @__PURE__ */ import_react19.default.createElement("code", null, "nama_tabel"), " dengan nama tabelmu:"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ import_react19.default.createElement(
2679
+ "button",
2680
+ {
2681
+ type: "button",
2682
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2683
+ style: { position: "absolute", top: 6, right: 6 },
2684
+ onClick: () => copyCode(sqlRls("nama_tabel"), "rls")
2685
+ },
2686
+ copied === "rls" ? "\u2705" : "\u{1F4CB}",
2687
+ " Salin"
2688
+ ), /* @__PURE__ */ import_react19.default.createElement("pre", { className: "rdb-wizard-code", style: { fontSize: "0.7rem" } }, sqlRls("nama_tabel"))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 8, color: "var(--rdb-text-muted)" } }, "\u{1F4A1} ", /* @__PURE__ */ import_react19.default.createElement("strong", null, "Development:"), " pakai Opsi 1.", /* @__PURE__ */ import_react19.default.createElement("strong", null, " Production:"), " pakai Opsi 2 atau 3."))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ import_react19.default.createElement(
2689
+ "button",
2690
+ {
2691
+ type: "button",
2692
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2693
+ onClick: handleDismiss
2694
+ },
2695
+ "Lanjutkan tanpa wizard"
2696
+ ), supabaseOk && /* @__PURE__ */ import_react19.default.createElement(
2697
+ "button",
2698
+ {
2699
+ type: "button",
2700
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2701
+ onClick: () => setStep(1)
2702
+ },
2703
+ "Lanjut \u2192 Pilih Tabel"
2704
+ ))), step === 1 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "2"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Pilih Tabel Transaksi")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 16 } }, "Klik tabel yang berisi data transaksi bisnis kamu. Wizard akan", /* @__PURE__ */ import_react19.default.createElement("strong", null, " otomatis membaca kolom dan sample data"), " dari tabel tersebut."), tables.length > 0 && /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 16 } }, tables.map((tbl) => {
2705
+ var _a2, _b2;
2706
+ const isSelected = selectedTable === tbl;
2707
+ const hasSample = ((_a2 = sampleData[tbl]) == null ? void 0 : _a2.length) > 0;
2708
+ const hasColumns = ((_b2 = columns[tbl]) == null ? void 0 : _b2.length) > 0;
2709
+ return /* @__PURE__ */ import_react19.default.createElement(
2375
2710
  "button",
2376
2711
  {
2377
2712
  key: tbl,
2378
2713
  type: "button",
2379
2714
  onClick: () => {
2380
2715
  setSelectedTable(tbl);
2381
- loadColumns(tbl);
2716
+ analyzeTable(tbl);
2382
2717
  },
2383
- className: `rdb-btn rdb-btn-sm ${selectedTable === tbl ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2384
- style: { justifyContent: "flex-start", fontFamily: "monospace" }
2385
- },
2386
- selectedTable === tbl ? "\u2705 " : "\u25CB ",
2387
- tbl
2388
- )))), tables.length === 0 && /* @__PURE__ */ import_react19.default.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-alert", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u2139\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Daftar tabel tidak bisa terbaca otomatis"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Ini normal \u2014 Supabase memblokir akses ke ", /* @__PURE__ */ import_react19.default.createElement("code", null, "information_schema"), " via API publik. Ketik nama tabel secara manual di bawah."))), /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Nama tabel transaksi kamu"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react19.default.createElement(
2389
- "input",
2390
- {
2391
- className: "rdb-input",
2392
- value: selectedTable,
2393
- onChange: (e) => setSelectedTable(e.target.value),
2394
- placeholder: "cth: orders, transaksi, bookings, penjualan",
2395
- style: { maxWidth: 320 }
2396
- }
2397
- ), /* @__PURE__ */ import_react19.default.createElement(
2398
- "button",
2399
- {
2400
- type: "button",
2401
- className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2402
- disabled: !selectedTable || loadingColumns,
2403
- onClick: () => loadColumns(selectedTable)
2404
- },
2405
- loadingColumns ? "Membaca\u2026" : "Baca kolom"
2406
- ))), tables.length > 0 && !selectedTable && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { color: "var(--rdb-text-muted)", marginBottom: 12 } }, "Atau ketik nama tabel secara manual:", /* @__PURE__ */ import_react19.default.createElement(
2407
- "input",
2408
- {
2409
- className: "rdb-input",
2410
- style: { marginTop: 6, maxWidth: 280 },
2411
- placeholder: "nama tabel lain",
2412
- onChange: (e) => {
2413
- setSelectedTable(e.target.value);
2414
- if (e.target.value) loadColumns(e.target.value);
2415
- }
2416
- }
2417
- )), selectedTable && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip", style: { marginBottom: 12 } }, "Tabel ", /* @__PURE__ */ import_react19.default.createElement("strong", null, selectedTable), " dipilih.", loadingColumns ? " Membaca kolom\u2026" : tableColumns.length > 0 ? ` ${tableColumns.length} kolom ditemukan \u2014 kolom sudah ter-auto-detect di step berikutnya.` : tableColumns.length === 0 && !loadingColumns && columns[selectedTable] !== void 0 ? " Tabel kosong atau tidak ada kolom terbaca \u2014 kamu bisa ketik nama kolom manual di step berikutnya." : ""), /* @__PURE__ */ import_react19.default.createElement("div", { style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Judul dashboard (opsional)"), /* @__PURE__ */ import_react19.default.createElement(
2418
- "input",
2419
- {
2420
- className: "rdb-input",
2421
- value: dashTitle,
2422
- style: { maxWidth: 280 },
2423
- onChange: (e) => setDashTitle(e.target.value),
2424
- placeholder: "cth: Dashboard Laundry Bersih"
2425
- }
2426
- )), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ import_react19.default.createElement(
2427
- "button",
2428
- {
2429
- type: "button",
2430
- className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2431
- onClick: () => setStep(0)
2432
- },
2433
- "\u2190 Kembali"
2434
- ), /* @__PURE__ */ import_react19.default.createElement(
2435
- "button",
2436
- {
2437
- type: "button",
2438
- className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2439
- disabled: !selectedTable || loadingColumns,
2440
- onClick: () => setStep(2)
2441
- },
2442
- "Lanjut \u2192 Mapping Kolom"
2443
- ))), step === 2 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "3"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Mapping Kolom \u2014 Tabel: ", /* @__PURE__ */ import_react19.default.createElement("code", { style: { fontSize: "0.9rem" } }, selectedTable))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 16 } }, "Pilih kolom yang berperan sebagai masing-masing data. Kolom sudah ter-deteksi otomatis \u2014 cukup verifikasi dan sesuaikan jika perlu."), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Tanggal ", /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "red" } }, "*")), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2444
- "select",
2445
- {
2446
- className: "rdb-select",
2447
- value: colDate,
2448
- onChange: (e) => setColDate(e.target.value)
2449
- },
2450
- /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 pilih kolom \u2014"),
2451
- colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2452
- ) : /* @__PURE__ */ import_react19.default.createElement(
2453
- "input",
2454
- {
2455
- className: "rdb-input",
2456
- value: colDate,
2457
- onChange: (e) => setColDate(e.target.value),
2458
- placeholder: "cth: created_at"
2459
- }
2460
- ), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 2 } }, "Tipe timestamp/date")), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Status ", /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "red" } }, "*")), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2461
- "select",
2462
- {
2463
- className: "rdb-select",
2464
- value: colStatus,
2465
- onChange: (e) => setColStatus(e.target.value)
2466
- },
2467
- /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 pilih kolom \u2014"),
2468
- colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2469
- ) : /* @__PURE__ */ import_react19.default.createElement(
2470
- "input",
2471
- {
2472
- className: "rdb-input",
2473
- value: colStatus,
2474
- onChange: (e) => setColStatus(e.target.value),
2475
- placeholder: "cth: status"
2476
- }
2477
- ), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 2 } }, "Nilai selesai/pending")), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Total (uang) ", /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "red" } }, "*")), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2478
- "select",
2479
- {
2480
- className: "rdb-select",
2481
- value: colTotal,
2482
- onChange: (e) => setColTotal(e.target.value)
2483
- },
2484
- /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 pilih kolom \u2014"),
2485
- colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2486
- ) : /* @__PURE__ */ import_react19.default.createElement(
2487
- "input",
2488
- {
2489
- className: "rdb-input",
2490
- value: colTotal,
2491
- onChange: (e) => setColTotal(e.target.value),
2492
- placeholder: "cth: total_amount"
2493
- }
2494
- ), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 2 } }, "Integer Rupiah")), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Nama Pelanggan"), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2495
- "select",
2496
- {
2497
- className: "rdb-select",
2498
- value: colCustomer,
2499
- onChange: (e) => setColCustomer(e.target.value)
2500
- },
2501
- /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 opsional \u2014"),
2502
- colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2503
- ) : /* @__PURE__ */ import_react19.default.createElement(
2504
- "input",
2505
- {
2506
- className: "rdb-input",
2507
- value: colCustomer,
2508
- onChange: (e) => setColCustomer(e.target.value),
2509
- placeholder: "cth: customer_name (opsional)"
2510
- }
2511
- )), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Kolom Nama Item/Layanan"), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(
2512
- "select",
2513
- {
2514
- className: "rdb-select",
2515
- value: colItem,
2516
- onChange: (e) => setColItem(e.target.value)
2517
- },
2518
- /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 opsional \u2014"),
2519
- colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))
2520
- ) : /* @__PURE__ */ import_react19.default.createElement(
2521
- "input",
2522
- {
2523
- className: "rdb-input",
2524
- value: colItem,
2525
- onChange: (e) => setColItem(e.target.value),
2526
- placeholder: "cth: service_type (opsional)"
2527
- }
2528
- ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow", style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 8 } }, "Nilai status di tabel kamu:"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, 'Nilai "Selesai/Confirmed"'), /* @__PURE__ */ import_react19.default.createElement(
2529
- "input",
2530
- {
2531
- className: "rdb-input",
2532
- value: confirmedVal,
2533
- onChange: (e) => setConfirmedVal(e.target.value),
2534
- placeholder: "cth: confirmed, selesai, lunas"
2535
- }
2536
- )), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, 'Nilai "Pending/Proses"'), /* @__PURE__ */ import_react19.default.createElement(
2537
- "input",
2538
- {
2539
- className: "rdb-input",
2540
- value: pendingVal,
2541
- onChange: (e) => setPendingVal(e.target.value),
2542
- placeholder: "cth: pending, proses, menunggu"
2543
- }
2544
- )))), !mappingValid && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-error-banner", style: { marginBottom: 12 } }, "Pilih minimal kolom Tanggal, Status, dan Total untuk generate kode."), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ import_react19.default.createElement(
2545
- "button",
2546
- {
2547
- type: "button",
2548
- className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2549
- onClick: () => setStep(1)
2550
- },
2551
- "\u2190 Kembali"
2552
- ), /* @__PURE__ */ import_react19.default.createElement(
2553
- "button",
2554
- {
2555
- type: "button",
2556
- className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2557
- disabled: !mappingValid,
2558
- onClick: handleGenerate
2559
- },
2560
- "\u2728 Generate Kode \u2192"
2561
- ))), step === 3 && generatedCode && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Kode Siap Pakai")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip", style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("strong", null, "4 file sudah digenerate."), " Salin masing-masing ke project kamu. Tidak perlu memahami logika di dalamnya \u2014 cukup save dan jalankan."), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issues", style: { marginBottom: 16 } }, [
2562
- ["myDashboardSource.js", "src/datasources/", "File koneksi ke Supabase"],
2563
- ["myDashboardAdapter.js", "src/adapters/", "Transformasi data \u2192 format dashboard"],
2564
- ["myDashboardConfig.js", "src/config/ (atau langsung di Dashboard.jsx)", "Konfigurasi widget"],
2565
- ["Dashboard.jsx", "src/pages/admin/", "Ganti halaman dashboard lama"]
2566
- ].map(([file, path, desc], i) => /* @__PURE__ */ import_react19.default.createElement("div", { key: i, className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700 } }, i + 1, "."), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Simpan ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, file), " ke", " ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, path), " \u2014 ", desc))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700 } }, "5."), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Import CSS di ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "src/index.css"), ":", " ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, '@import "../node_modules/@rozaqi02/reusable-dashboard/dist/index.css";')))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 4, marginBottom: 8, flexWrap: "wrap" } }, [
2567
- ["dataSource", "myDashboardSource.js"],
2568
- ["adapter", "myDashboardAdapter.js"],
2569
- ["widgetConfig", "myDashboardConfig.js"],
2570
- ["dashboard", "Dashboard.jsx"]
2571
- ].map(([key, label]) => /* @__PURE__ */ import_react19.default.createElement(
2572
- "button",
2573
- {
2574
- key,
2575
- type: "button",
2576
- onClick: () => setActiveCodeTab(key),
2577
- className: `rdb-btn rdb-btn-sm ${activeCodeTab === key ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2578
- style: { fontFamily: "monospace", fontSize: "0.75rem" }
2718
+ className: `rdb-btn rdb-btn-sm ${isSelected ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2719
+ style: { fontFamily: "monospace", fontSize: "0.82rem" }
2579
2720
  },
2580
- label
2581
- ))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ import_react19.default.createElement(
2582
- "button",
2583
- {
2584
- type: "button",
2585
- onClick: () => copyCode(generatedCode[activeCodeTab], activeCodeTab),
2586
- className: "rdb-btn rdb-btn-sm rdb-btn-secondary",
2587
- style: { position: "absolute", top: 8, right: 8, zIndex: 1 }
2588
- },
2589
- copied === activeCodeTab ? "\u2705 Disalin!" : "\u{1F4CB} Salin"
2590
- ), /* @__PURE__ */ import_react19.default.createElement("pre", { className: "rdb-wizard-code", style: { maxHeight: 320, overflow: "auto" } }, generatedCode[activeCodeTab])), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip", style: { marginTop: 12 } }, "Setelah semua file tersimpan dan dev server direstart, wizard tidak akan muncul lagi."), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 12 } }, /* @__PURE__ */ import_react19.default.createElement(
2591
- "button",
2592
- {
2593
- type: "button",
2594
- className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2595
- onClick: () => setStep(2)
2596
- },
2597
- "\u2190 Edit mapping"
2598
- ), /* @__PURE__ */ import_react19.default.createElement(
2599
- "button",
2600
- {
2601
- type: "button",
2602
- className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2603
- onClick: handleDismiss
2604
- },
2605
- "Tutup wizard \u2713"
2606
- ))))))
2607
- );
2721
+ isSelected ? "\u2705 " : "",
2722
+ tbl,
2723
+ isSelected && hasColumns ? ` (${columns[tbl].length} kolom)` : ""
2724
+ );
2725
+ })), /* @__PURE__ */ import_react19.default.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, tables.length > 0 ? "Atau ketik nama tabel lain:" : "Ketik nama tabel kamu:"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react19.default.createElement(
2726
+ "input",
2727
+ {
2728
+ className: "rdb-input",
2729
+ value: selectedTable,
2730
+ onChange: (e) => setSelectedTable(e.target.value.replace(/[,\s].*/, "")),
2731
+ placeholder: "cth: orders / transaksi / bookings",
2732
+ style: { maxWidth: 280 }
2733
+ }
2734
+ ), /* @__PURE__ */ import_react19.default.createElement(
2735
+ "button",
2736
+ {
2737
+ type: "button",
2738
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2739
+ disabled: !selectedTable || loadingCols,
2740
+ onClick: () => analyzeTable(selectedTable)
2741
+ },
2742
+ loadingCols ? "Menganalisis\u2026" : "Analisis tabel"
2743
+ ))), selectedTable && !loadingCols && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, colNames.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip", style: { marginBottom: 8 } }, "\u2705 ", /* @__PURE__ */ import_react19.default.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__ */ import_react19.default.createElement(
2744
+ PreviewTable,
2745
+ {
2746
+ rows: sample,
2747
+ cols: [colDate, colStatus, colTotal, colCustomer].filter(Boolean)
2748
+ }
2749
+ )), colsBlocked && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-alert" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u2139\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Kolom tidak bisa terbaca otomatis"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Kemungkinan RLS aktif. Kamu tetap bisa lanjut dan ketik nama kolom manual di step berikutnya.")))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { marginBottom: 12, marginTop: 12 } }, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, "Judul dashboard:"), /* @__PURE__ */ import_react19.default.createElement(
2750
+ "input",
2751
+ {
2752
+ className: "rdb-input",
2753
+ value: dashTitle,
2754
+ style: { maxWidth: 280 },
2755
+ onChange: (e) => setDashTitle(e.target.value),
2756
+ placeholder: "cth: Dashboard Laundry / Dashboard Klinik"
2757
+ }
2758
+ )), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ import_react19.default.createElement(
2759
+ "button",
2760
+ {
2761
+ type: "button",
2762
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2763
+ onClick: () => setStep(0)
2764
+ },
2765
+ "\u2190 Kembali"
2766
+ ), /* @__PURE__ */ import_react19.default.createElement(
2767
+ "button",
2768
+ {
2769
+ type: "button",
2770
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2771
+ disabled: !selectedTable || loadingCols,
2772
+ onClick: () => setStep(2)
2773
+ },
2774
+ "Lanjut \u2192 Konfirmasi Kolom"
2775
+ ))), step === 2 && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "3"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Konfirmasi \u2014 Tabel: ", /* @__PURE__ */ import_react19.default.createElement("code", { style: { fontSize: "0.9em" } }, selectedTable))), /* @__PURE__ */ import_react19.default.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__ */ import_react19.default.createElement("div", { className: "rdb-wizard-supabase-reader", style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 6 } }, "Semua kolom di tabel ", selectedTable, ":"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-table-chips" }, colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("span", { key: c, className: "rdb-wizard-chip", style: { cursor: "default" } }, c)))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginBottom: 16 } }, [
2776
+ { label: "Kolom Tanggal *", hint: "Tipe timestamp/date", val: colDate, set: setColDate, ph: "created_at" },
2777
+ { label: "Kolom Status *", hint: "Berisi nilai selesai/pending", val: colStatus, set: setColStatus, ph: "status" },
2778
+ { label: "Kolom Total (uang) *", hint: "Integer Rupiah", val: colTotal, set: setColTotal, ph: "total_amount" },
2779
+ { label: "Kolom Nama Pelanggan", hint: "Opsional", val: colCustomer, set: setColCustomer, ph: "customer_name (opsional)" },
2780
+ { label: "Kolom Nama Item / Layanan", hint: "Opsional", val: colItem, set: setColItem, ph: "product_name (opsional)" }
2781
+ ].map(({ label, hint, val, set, ph }) => /* @__PURE__ */ import_react19.default.createElement("div", { key: label }, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, label), colNames.length > 0 ? /* @__PURE__ */ import_react19.default.createElement("select", { className: "rdb-select", value: val, onChange: (e) => set(e.target.value) }, /* @__PURE__ */ import_react19.default.createElement("option", { value: "" }, "\u2014 pilih \u2014"), colNames.map((c) => /* @__PURE__ */ import_react19.default.createElement("option", { key: c, value: c }, c))) : /* @__PURE__ */ import_react19.default.createElement("input", { className: "rdb-input", value: val, onChange: (e) => set(e.target.value), placeholder: ph }), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 2 } }, hint)))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-flow", style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 4 } }, "Nilai status yang ditemukan di data kamu:"), stVals.length > 0 ? /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { color: "var(--rdb-text-muted)", marginBottom: 8 } }, "Wizard membaca nilai unik dari kolom ", /* @__PURE__ */ import_react19.default.createElement("strong", null, colStatus), ". Klik nilai yang sesuai:"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 12 } }, stVals.map((v) => /* @__PURE__ */ import_react19.default.createElement("div", { key: v, style: { display: "flex", gap: 4, alignItems: "center" } }, /* @__PURE__ */ import_react19.default.createElement(
2782
+ "button",
2783
+ {
2784
+ type: "button",
2785
+ className: `rdb-btn rdb-btn-sm ${confirmedVal === String(v) ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2786
+ onClick: () => setConfirmedVal(String(v))
2787
+ },
2788
+ confirmedVal === String(v) ? "\u2705 " : "",
2789
+ "Selesai = ",
2790
+ /* @__PURE__ */ import_react19.default.createElement("strong", null, String(v))
2791
+ ), /* @__PURE__ */ import_react19.default.createElement(
2792
+ "button",
2793
+ {
2794
+ type: "button",
2795
+ className: `rdb-btn rdb-btn-sm ${pendingVal === String(v) ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2796
+ onClick: () => setPendingVal(String(v))
2797
+ },
2798
+ pendingVal === String(v) ? "\u2705 " : "",
2799
+ "Proses = ",
2800
+ /* @__PURE__ */ import_react19.default.createElement("strong", null, String(v))
2801
+ ))))) : /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { color: "var(--rdb-text-muted)", marginBottom: 8 } }, "Nilai status tidak terbaca otomatis. Ketik manual:"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, 'Nilai "Selesai/Confirmed"'), /* @__PURE__ */ import_react19.default.createElement(
2802
+ "input",
2803
+ {
2804
+ className: "rdb-input",
2805
+ value: confirmedVal,
2806
+ onChange: (e) => setConfirmedVal(e.target.value),
2807
+ placeholder: "confirmed"
2808
+ }
2809
+ )), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("label", { className: "rdb-label" }, 'Nilai "Pending/Proses"'), /* @__PURE__ */ import_react19.default.createElement(
2810
+ "input",
2811
+ {
2812
+ className: "rdb-input",
2813
+ value: pendingVal,
2814
+ onChange: (e) => setPendingVal(e.target.value),
2815
+ placeholder: "pending"
2816
+ }
2817
+ )))), sample.length > 0 && colDate && colStatus && colTotal && /* @__PURE__ */ import_react19.default.createElement("div", { style: { marginBottom: 16 } }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 4 } }, "Preview data nyata sesuai mapping kamu:"), /* @__PURE__ */ import_react19.default.createElement(
2818
+ PreviewTable,
2819
+ {
2820
+ rows: sample,
2821
+ cols: [colDate, colStatus, colTotal, colCustomer, colItem].filter((c) => c && c !== "-")
2822
+ }
2823
+ )), !canGen && /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-error-banner", style: { marginBottom: 12 } }, "Isi Kolom Tanggal, Status, dan Total untuk melanjutkan."), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" } }, /* @__PURE__ */ import_react19.default.createElement(
2824
+ "button",
2825
+ {
2826
+ type: "button",
2827
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2828
+ onClick: () => setStep(1)
2829
+ },
2830
+ "\u2190 Kembali"
2831
+ ), /* @__PURE__ */ import_react19.default.createElement(
2832
+ "button",
2833
+ {
2834
+ type: "button",
2835
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2836
+ disabled: !canGen,
2837
+ onClick: handleGenerate
2838
+ },
2839
+ "\u2728 Generate Kode \u2192"
2840
+ ))), step === 3 && generatedCode && /* @__PURE__ */ import_react19.default.createElement(import_react19.default.Fragment, null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-step-num" }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Kode Sudah Jadi!")), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-tip", style: { marginBottom: 12 } }, /* @__PURE__ */ import_react19.default.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__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issues", style: { marginBottom: 12 } }, Object.entries(TAB_INFO).map(([key, info], i) => /* @__PURE__ */ import_react19.default.createElement("div", { key, className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700, minWidth: 18 } }, i + 1, "."), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Salin ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, info.label), " \u2192 simpan ke ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, info.path)))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700, minWidth: 18 } }, "5."), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Di ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "src/index.css"), ", tambahkan:", " ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, '@import "../node_modules/@rozaqi02/reusable-dashboard/dist/index.css";'))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-issue-item" }, /* @__PURE__ */ import_react19.default.createElement("span", { style: { color: "var(--rdb-blue-500)", fontWeight: 700, minWidth: 18 } }, "6."), /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-body" }, "Restart dev server (", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, "npm start"), ") \u2192 buka halaman dashboard \u2192 selesai!"))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 4, flexWrap: "wrap", marginBottom: 4 } }, Object.entries(TAB_INFO).map(([key, info]) => /* @__PURE__ */ import_react19.default.createElement(
2841
+ "button",
2842
+ {
2843
+ key,
2844
+ type: "button",
2845
+ onClick: () => setActiveTab(key),
2846
+ className: `rdb-btn rdb-btn-sm ${activeTab === key ? "rdb-btn-primary" : "rdb-btn-secondary"}`,
2847
+ style: { fontFamily: "monospace", fontSize: "0.73rem" }
2848
+ },
2849
+ info.label
2850
+ ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { color: "var(--rdb-text-muted)", marginBottom: 6 } }, "\u{1F4C1} Simpan ke: ", /* @__PURE__ */ import_react19.default.createElement("code", { className: "rdb-wizard-code-inline" }, (_b = TAB_INFO[activeTab]) == null ? void 0 : _b.path)), /* @__PURE__ */ import_react19.default.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ import_react19.default.createElement(
2851
+ "button",
2852
+ {
2853
+ type: "button",
2854
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2855
+ style: { position: "absolute", top: 8, right: 8, zIndex: 1 },
2856
+ onClick: () => copyCode(generatedCode[activeTab], activeTab)
2857
+ },
2858
+ copied === activeTab ? "\u2705 Tersalin!" : "\u{1F4CB} Salin kode"
2859
+ ), /* @__PURE__ */ import_react19.default.createElement("pre", { className: "rdb-wizard-code", style: { maxHeight: 280, overflow: "auto" } }, generatedCode[activeTab])), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-wizard-alert", style: { marginTop: 10, marginBottom: 4 } }, /* @__PURE__ */ import_react19.default.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u26A0\uFE0F"), /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Data tidak muncul di dashboard?"), /* @__PURE__ */ import_react19.default.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Jalankan SQL ini di Supabase SQL Editor:"), /* @__PURE__ */ import_react19.default.createElement("div", { style: { position: "relative", marginTop: 6 } }, /* @__PURE__ */ import_react19.default.createElement(
2860
+ "button",
2861
+ {
2862
+ type: "button",
2863
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2864
+ style: { position: "absolute", top: 4, right: 4 },
2865
+ onClick: () => copyCode(`ALTER TABLE public.${selectedTable} DISABLE ROW LEVEL SECURITY;`, "rlsquick")
2866
+ },
2867
+ copied === "rlsquick" ? "\u2705" : "\u{1F4CB}"
2868
+ ), /* @__PURE__ */ import_react19.default.createElement("pre", { className: "rdb-wizard-code", style: { fontSize: "0.72rem" } }, `ALTER TABLE public.${selectedTable} DISABLE ROW LEVEL SECURITY;`)))), /* @__PURE__ */ import_react19.default.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 10 } }, /* @__PURE__ */ import_react19.default.createElement(
2869
+ "button",
2870
+ {
2871
+ type: "button",
2872
+ className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
2873
+ onClick: () => setStep(2)
2874
+ },
2875
+ "\u2190 Edit mapping"
2876
+ ), /* @__PURE__ */ import_react19.default.createElement(
2877
+ "button",
2878
+ {
2879
+ type: "button",
2880
+ className: "rdb-btn rdb-btn-primary rdb-btn-sm",
2881
+ onClick: handleDismiss
2882
+ },
2883
+ "Tutup wizard \u2713"
2884
+ )))))));
2608
2885
  }
2609
2886
  SetupWizard.propTypes = {
2610
2887
  issues: import_prop_types17.default.arrayOf(import_prop_types17.default.string),
@@ -2736,6 +3013,152 @@ ReusableDashboardView.propTypes = {
2736
3013
  dashboardConfig: import_prop_types18.default.object
2737
3014
  };
2738
3015
 
3016
+ // src/presentation/AutoDashboard.jsx
3017
+ var import_react21 = __toESM(require("react"), 1);
3018
+ var import_prop_types19 = __toESM(require("prop-types"), 1);
3019
+ var DEFAULT_LABELS = {
3020
+ title: "Dashboard",
3021
+ refresh: "Muat ulang",
3022
+ liveUpdate: "Live",
3023
+ loadFailed: "Gagal memuat data dashboard.",
3024
+ retry: "Coba lagi",
3025
+ confirmedOnly: "Hanya berhasil",
3026
+ pendingOnly: "Hanya menunggu",
3027
+ allStatus: "Semua status",
3028
+ showPendingOverlay: "Tampilkan overlay menunggu",
3029
+ allAudience: "Semua segmen",
3030
+ audienceDomestic: "Domestik",
3031
+ audienceForeign: "Asing",
3032
+ customDate: "Custom",
3033
+ reset: "Reset",
3034
+ topSort: "Urutkan item",
3035
+ sortBookings: "Jumlah",
3036
+ sortRevenue: "Nilai",
3037
+ sortDesc: "Turun",
3038
+ sortAsc: "Naik",
3039
+ confirmedBookings: "Transaksi Berhasil",
3040
+ confirmedRevenue: "Pendapatan (Berhasil)",
3041
+ avgRevenue: "Rata-rata Nilai / Transaksi",
3042
+ conversionRate: "Tingkat Konversi",
3043
+ dailyTrends: "Tren Harian",
3044
+ statusDistribution: "Distribusi Status",
3045
+ audienceDistribution: "Distribusi Segmen",
3046
+ topPackages: "Item Teratas",
3047
+ recentBookings: "Transaksi Terbaru",
3048
+ date: "Tanggal",
3049
+ customer: "Pelanggan",
3050
+ package: "Item",
3051
+ audience: "Segmen",
3052
+ total: "Total",
3053
+ status: "Status",
3054
+ noRecentBookings: "Belum ada transaksi terbaru",
3055
+ unknownAudience: "Tidak diketahui"
3056
+ };
3057
+ function buildLabels(overrides = {}) {
3058
+ const labels = { ...DEFAULT_LABELS, ...overrides };
3059
+ labels.dayLabel = overrides.dayLabel || ((count) => count ? `${count} hari` : "Custom");
3060
+ labels.formatStatusLabel = overrides.formatStatusLabel || ((status) => {
3061
+ const s = String(status || "pending");
3062
+ return s.charAt(0).toUpperCase() + s.slice(1);
3063
+ });
3064
+ labels.formatAudienceLabel = overrides.formatAudienceLabel || ((value) => {
3065
+ if (value === "domestic") return labels.audienceDomestic;
3066
+ if (value === "foreign") return labels.audienceForeign;
3067
+ if (!value || value === "unknown") return labels.unknownAudience;
3068
+ return String(value);
3069
+ });
3070
+ return labels;
3071
+ }
3072
+ function AutoDashboard({
3073
+ supabase,
3074
+ table,
3075
+ columns,
3076
+ confirmedValue = "confirmed",
3077
+ pendingValue = "pending",
3078
+ title = "Dashboard",
3079
+ labels: labelOverrides,
3080
+ languageCode = "id",
3081
+ dateLocale = "id-ID"
3082
+ }) {
3083
+ const isConfigured = Boolean(supabase && table && (columns == null ? void 0 : columns.date));
3084
+ const labels = import_react21.default.useMemo(
3085
+ () => buildLabels({ ...labelOverrides, title }),
3086
+ [labelOverrides, title]
3087
+ );
3088
+ const widgetConfig = import_react21.default.useMemo(
3089
+ () => createUniversalWidgetConfig(columns || {}),
3090
+ [columns]
3091
+ );
3092
+ const dataSource = import_react21.default.useMemo(() => {
3093
+ if (!isConfigured) return null;
3094
+ return createUniversalSource(supabase, { table, columns });
3095
+ }, [isConfigured, supabase, table, columns]);
3096
+ const adapter = import_react21.default.useMemo(
3097
+ () => (args) => adaptUniversalData({ ...args, options: { confirmedValue, pendingValue } }),
3098
+ [confirmedValue, pendingValue]
3099
+ );
3100
+ const dashboard = useReusableDashboard({
3101
+ config: widgetConfig,
3102
+ dataSource: dataSource || { fetchDashboardSnapshot: null },
3103
+ adapter,
3104
+ createEmptyState: createEmptyUniversalData,
3105
+ languageCode,
3106
+ dateLocale,
3107
+ labels
3108
+ });
3109
+ if (!isConfigured) {
3110
+ const issues = [];
3111
+ if (!supabase) issues.push("Prop 'supabase' belum diisi (Supabase client).");
3112
+ if (!table) issues.push("Prop 'table' belum diisi (nama tabel sumber data).");
3113
+ if (!(columns == null ? void 0 : columns.date))
3114
+ issues.push("Prop 'columns.date' belum diisi (kolom timestamp).");
3115
+ return /* @__PURE__ */ import_react21.default.createElement("div", { className: "rdb-view" }, /* @__PURE__ */ import_react21.default.createElement(SetupWizard, { issues, supabase, onDismiss: () => {
3116
+ } }));
3117
+ }
3118
+ return /* @__PURE__ */ import_react21.default.createElement(
3119
+ ReusableDashboardView,
3120
+ {
3121
+ config: widgetConfig,
3122
+ labels,
3123
+ loading: dashboard.loading,
3124
+ error: dashboard.error,
3125
+ filters: dashboard.filters,
3126
+ onFilterChange: dashboard.updateFilter,
3127
+ onResetFilters: dashboard.resetFilters,
3128
+ onRefresh: dashboard.refresh,
3129
+ data: dashboard.data,
3130
+ dateLocale,
3131
+ liveUpdateEnabled: dashboard.liveUpdateEnabled,
3132
+ supabase
3133
+ }
3134
+ );
3135
+ }
3136
+ AutoDashboard.propTypes = {
3137
+ /** Supabase client instance (WAJIB). */
3138
+ supabase: import_prop_types19.default.object,
3139
+ /** Nama tabel sumber data (WAJIB), mis. "bookings" / "orders". */
3140
+ table: import_prop_types19.default.string,
3141
+ /** Pemetaan kolom: { date, status, total, customer, item, audience }. date WAJIB. */
3142
+ columns: import_prop_types19.default.shape({
3143
+ date: import_prop_types19.default.string,
3144
+ status: import_prop_types19.default.string,
3145
+ total: import_prop_types19.default.string,
3146
+ customer: import_prop_types19.default.string,
3147
+ item: import_prop_types19.default.string,
3148
+ audience: import_prop_types19.default.string
3149
+ }),
3150
+ /** Nilai status yang dihitung "berhasil" (default "confirmed"). */
3151
+ confirmedValue: import_prop_types19.default.string,
3152
+ /** Nilai status yang dihitung "menunggu" (default "pending"). */
3153
+ pendingValue: import_prop_types19.default.string,
3154
+ /** Judul dashboard. */
3155
+ title: import_prop_types19.default.string,
3156
+ /** Override sebagian/seluruh label UI. */
3157
+ labels: import_prop_types19.default.object,
3158
+ languageCode: import_prop_types19.default.string,
3159
+ dateLocale: import_prop_types19.default.string
3160
+ };
3161
+
2739
3162
  // src/utils/labels.js
2740
3163
  function createDashboardLabels(t) {
2741
3164
  const labels = {
@@ -2857,6 +3280,7 @@ function createDashboardLabels(t) {
2857
3280
  }
2858
3281
  // Annotate the CommonJS export names for ESM import in node:
2859
3282
  0 && (module.exports = {
3283
+ AutoDashboard,
2860
3284
  Badge,
2861
3285
  Button,
2862
3286
  ChartCard,
@@ -2878,6 +3302,7 @@ function createDashboardLabels(t) {
2878
3302
  adaptCidikaDashboardData,
2879
3303
  adaptDummyUmkmData,
2880
3304
  adaptTokoSepatuData,
3305
+ adaptUniversalData,
2881
3306
  buildDayBuckets,
2882
3307
  cidikaWidgetConfig,
2883
3308
  createCidikaSupabaseSource,
@@ -2887,7 +3312,10 @@ function createDashboardLabels(t) {
2887
3312
  createEmptyDashboardData,
2888
3313
  createEmptyDummyUmkmData,
2889
3314
  createEmptyTokoSepatuData,
3315
+ createEmptyUniversalData,
2890
3316
  createTokoSepatuSupabaseSource,
3317
+ createUniversalSource,
3318
+ createUniversalWidgetConfig,
2891
3319
  dummyUmkmWidgetConfig,
2892
3320
  formatDate,
2893
3321
  formatIDR,