@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/CHANGELOG.md +14 -0
- package/README.md +69 -1
- package/dist/index.cjs +921 -493
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +916 -493
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
1927
|
-
const
|
|
1928
|
-
if (PRESET_SIGNATURES.cidika.tables.every((t) =>
|
|
1929
|
-
if (PRESET_SIGNATURES.tokoSepatu.tables.every((t) =>
|
|
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
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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
|
|
1948
|
-
const
|
|
1949
|
-
const
|
|
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
|
|
2286
|
+
const allQ = supabase
|
|
1958
2287
|
.from("${tableName}")
|
|
1959
|
-
.select("
|
|
2288
|
+
.select("${cols}")
|
|
1960
2289
|
.gte("${colDate}", fromISO)
|
|
1961
2290
|
.lte("${colDate}", toISO)
|
|
1962
2291
|
.order("${colDate}", { ascending: true });
|
|
1963
2292
|
|
|
1964
|
-
const
|
|
2293
|
+
const recentQ = supabase
|
|
1965
2294
|
.from("${tableName}")
|
|
1966
|
-
.select("
|
|
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
|
-
|
|
2302
|
+
recentQ.eq("${colStatus}", statusScope);
|
|
1974
2303
|
}
|
|
1975
2304
|
|
|
1976
|
-
const [
|
|
1977
|
-
|
|
2305
|
+
const [all, recent] = await Promise.all([allQ, recentQ]);
|
|
1978
2306
|
return {
|
|
1979
|
-
bookings:
|
|
1980
|
-
recent:
|
|
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
|
-
.
|
|
1989
|
-
|
|
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
|
|
2020
|
-
const
|
|
2021
|
-
const
|
|
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(
|
|
2024
|
-
const
|
|
2025
|
-
if (
|
|
2026
|
-
if (
|
|
2027
|
-
if (
|
|
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 (
|
|
2355
|
+
if (st === "${confirmed}") { confirmed++; revenue += amt; }
|
|
2030
2356
|
});
|
|
2031
2357
|
|
|
2032
|
-
const
|
|
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(([
|
|
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:
|
|
2057
|
-
createdAt:
|
|
2058
|
-
customerName:
|
|
2059
|
-
packageName:
|
|
2376
|
+
id: row.id,
|
|
2377
|
+
createdAt: row["${colDate}"],
|
|
2378
|
+
customerName: row["${custCol}"] || "-",
|
|
2379
|
+
packageName: ${itemCol !== "-" ? `row["${itemCol}"] || "-"` : '"-"'},
|
|
2060
2380
|
audienceLabel: "-",
|
|
2061
|
-
totalIDR:
|
|
2062
|
-
status:
|
|
2063
|
-
statusLabel:
|
|
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
|
-
|
|
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:
|
|
2082
|
-
valueKey:
|
|
2083
|
-
{ id:
|
|
2084
|
-
valueKey:
|
|
2085
|
-
{ id:
|
|
2086
|
-
valueKey:
|
|
2087
|
-
{ id:
|
|
2088
|
-
valueKey:
|
|
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:
|
|
2092
|
-
{ id:
|
|
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:
|
|
2096
|
-
emptyLabel: "noRecentBookings",
|
|
2411
|
+
id:"recentTx", label:"recentBookings", icon:"Calendar", emptyLabel:"noRecentBookings",
|
|
2097
2412
|
columns: [
|
|
2098
|
-
{ id:
|
|
2099
|
-
{ id:
|
|
2100
|
-
{ id:
|
|
2101
|
-
{ id:
|
|
2102
|
-
{ id:
|
|
2103
|
-
type:
|
|
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
|
-
//
|
|
2121
|
-
import { myDashboardConfig }
|
|
2122
|
-
import { createMyDashboardSource }
|
|
2123
|
-
import { adaptMyDashboardData, createEmptyMyDashboardData } from "
|
|
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: "${
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
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
|
-
|
|
2167
|
-
|
|
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
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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 [
|
|
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)("
|
|
2212
|
-
const [pendingVal, setPendingVal] = (0, import_react19.useState)("
|
|
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 [
|
|
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 [
|
|
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:
|
|
2227
|
-
if (!
|
|
2228
|
-
setTables(
|
|
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
|
-
|
|
2232
|
-
|
|
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(
|
|
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
|
|
2251
|
-
|
|
2252
|
-
|
|
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
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
);
|
|
2259
|
-
|
|
2260
|
-
|
|
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
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2562
|
+
if (cols) {
|
|
2563
|
+
setColumns((p) => ({ ...p, [tbl]: cols }));
|
|
2564
|
+
autoDetect(tbl, cols);
|
|
2565
|
+
} else {
|
|
2566
|
+
setColsBlocked(true);
|
|
2268
2567
|
}
|
|
2269
|
-
const { data:
|
|
2270
|
-
if (
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
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
|
-
|
|
2585
|
+
setColsBlocked(true);
|
|
2282
2586
|
} finally {
|
|
2283
|
-
|
|
2587
|
+
setLoadingCols(false);
|
|
2284
2588
|
}
|
|
2285
2589
|
}, [supabase, columns]);
|
|
2286
|
-
function
|
|
2287
|
-
const
|
|
2288
|
-
var
|
|
2289
|
-
return ((
|
|
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(
|
|
2292
|
-
setColStatus(
|
|
2293
|
-
setColTotal(
|
|
2294
|
-
setColCustomer(
|
|
2295
|
-
setColItem(
|
|
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,
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
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
|
-
|
|
2610
|
+
onDismiss == null ? void 0 : onDismiss();
|
|
2306
2611
|
}
|
|
2307
2612
|
function handleGenerate() {
|
|
2308
2613
|
if (!selectedTable || !colDate || !colStatus || !colTotal) return;
|
|
2309
|
-
|
|
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
|
|
2325
|
-
const
|
|
2326
|
-
const colNames =
|
|
2327
|
-
const
|
|
2328
|
-
const
|
|
2329
|
-
|
|
2330
|
-
|
|
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
|
-
|
|
2333
|
-
|
|
2334
|
-
"
|
|
2335
|
-
|
|
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
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
},
|
|
2345
|
-
"
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
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
|
-
|
|
2716
|
+
analyzeTable(tbl);
|
|
2382
2717
|
},
|
|
2383
|
-
className: `rdb-btn rdb-btn-sm ${
|
|
2384
|
-
style: {
|
|
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
|
-
|
|
2581
|
-
|
|
2582
|
-
"
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
"
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
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,
|