@rozaqi02/reusable-dashboard 1.1.5 → 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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [1.2.0] - 2026-06-06
6
+
7
+ ### Added
8
+
9
+ 1. `<AutoDashboard>` — komponen plug-and-play untuk domain transaksional. Client cukup memberi prop `supabase`, `table`, dan pemetaan `columns`; data source, adapter, dan widget config dirakit otomatis tanpa menulis file konfigurasi terpisah.
10
+ 2. `createUniversalSource(supabase, { table, columns })` — data source generik yang menormalkan baris ke bentuk kanonik berdasarkan pemetaan kolom runtime.
11
+ 3. `adaptUniversalData` + `createEmptyUniversalData` — adapter generik yang menghasilkan struktur `{ stats, charts, table }` dari data kanonik.
12
+ 4. `createUniversalWidgetConfig(columns)` — membangun widget config secara dinamis sesuai kolom yang dipetakan.
13
+
14
+ ### Notes
15
+
16
+ - Preset `cidika`, `tokoSepatu`, dan `dummyUmkm` tetap tersedia untuk kasus yang butuh kontrol penuh.
17
+ - `<AutoDashboard>` ditujukan untuk data transaksional (waktu + status + nilai). Domain non-transaksional tetap memakai adapter manual.
18
+
5
19
  ## [1.0.0] - 2026-04-15
6
20
 
7
21
  ### Added
package/README.md CHANGED
@@ -4,7 +4,7 @@ Modul dashboard admin reusable untuk aplikasi UMKM berbasis React + Supabase.
4
4
  Mendukung berbagai domain bisnis (travel, toko online, UMKM generik) tanpa menulis ulang komponen.
5
5
  CSS sudah terbundle — tidak perlu konfigurasi Tailwind.
6
6
 
7
- [![version](https://img.shields.io/badge/version-1.1.3-blue)](./CHANGELOG.md)
7
+ [![version](https://img.shields.io/badge/version-1.2.0-blue)](./CHANGELOG.md)
8
8
  [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
9
9
  [![npm](https://img.shields.io/badge/npm-%40rozaqi02%2Freusable--dashboard-red)](https://www.npmjs.com/package/@rozaqi02/reusable-dashboard)
10
10
 
@@ -12,6 +12,7 @@ CSS sudah terbundle — tidak perlu konfigurasi Tailwind.
12
12
 
13
13
  ## Daftar Isi
14
14
 
15
+ 0. [⚡ Cara Tercepat — `<AutoDashboard>` (Plug & Play)](#0-cara-tercepat--autodashboard-plug--play)
15
16
  1. [Instalasi](#1-instalasi)
16
17
  2. [Prasyarat Teknis](#2-prasyarat-teknis)
17
18
  3. [Preset yang Tersedia](#3-preset-yang-tersedia)
@@ -28,6 +29,73 @@ CSS sudah terbundle — tidak perlu konfigurasi Tailwind.
28
29
 
29
30
  ---
30
31
 
32
+ ## 0. Cara Tercepat — `<AutoDashboard>` (Plug & Play)
33
+
34
+ Kalau kamu hanya ingin dashboard jadi **tanpa menulis file config apa pun**, pakai
35
+ komponen `<AutoDashboard>`. Cukup beri tahu nama tabel dan pemetaan kolom — modul
36
+ merakit data source, adapter, dan widget config secara otomatis di balik layar.
37
+
38
+ ```jsx
39
+ import { AutoDashboard } from "@rozaqi02/reusable-dashboard";
40
+ import { supabase } from "./lib/supabaseClient";
41
+
42
+ export default function Dashboard() {
43
+ return (
44
+ <AutoDashboard
45
+ supabase={supabase}
46
+ table="bookings"
47
+ columns={{
48
+ date: "created_at", // WAJIB — kolom timestamp
49
+ status: "status", // opsional — kolom status transaksi
50
+ total: "total_idr", // opsional — kolom nilai uang
51
+ customer: "customer_name", // opsional — nama pelanggan
52
+ item: "package_id", // opsional — nama produk/paket/layanan
53
+ audience: "audience", // opsional — segmen pelanggan
54
+ }}
55
+ confirmedValue="confirmed" // nilai status yang dihitung "berhasil"
56
+ pendingValue="pending" // nilai status yang dihitung "menunggu"
57
+ title="Dashboard Cidika"
58
+ />
59
+ );
60
+ }
61
+ ```
62
+
63
+ Jangan lupa import CSS-nya sekali saja (lihat [Bab 1](#1-instalasi)).
64
+
65
+ ### Props `<AutoDashboard>`
66
+
67
+ | Prop | Tipe | Wajib | Keterangan |
68
+ |------|------|-------|------------|
69
+ | `supabase` | object | ✅ | Instance Supabase client. |
70
+ | `table` | string | ✅ | Nama tabel sumber data, mis. `"bookings"`, `"orders"`. |
71
+ | `columns.date` | string | ✅ | Kolom timestamp untuk tren harian & filter rentang. |
72
+ | `columns.status` | string | — | Kolom status. Tanpa ini semua baris dianggap "berhasil". |
73
+ | `columns.total` | string | — | Kolom nilai uang untuk metrik pendapatan. |
74
+ | `columns.customer` | string | — | Kolom nama pelanggan (muncul di tabel). |
75
+ | `columns.item` | string | — | Kolom produk/paket/layanan (mengaktifkan chart "Item Teratas"). |
76
+ | `columns.audience` | string | — | Kolom segmen (mengaktifkan chart distribusi segmen). |
77
+ | `confirmedValue` | string | — | Nilai status "berhasil" (default `"confirmed"`). |
78
+ | `pendingValue` | string | — | Nilai status "menunggu" (default `"pending"`). |
79
+ | `title` | string | — | Judul dashboard. |
80
+ | `labels` | object | — | Override sebagian/seluruh teks UI (lihat [Bab 9](#9-label--internasionalisasi)). |
81
+ | `dateLocale` | string | — | Locale format tanggal (default `"id-ID"`). |
82
+
83
+ Widget menyesuaikan otomatis: chart "Distribusi Segmen" hanya muncul bila `columns.audience`
84
+ diisi, dan chart "Item Teratas" hanya muncul bila `columns.item` diisi.
85
+
86
+ ### Kapan memakai ini vs preset/adapter manual?
87
+
88
+ `<AutoDashboard>` cocok untuk **data transaksional** — data yang punya waktu, status,
89
+ dan (opsional) nilai uang. Ini mencakup mayoritas UMKM: travel, retail, laundry, klinik,
90
+ restoran, rental, kursus, bengkel, dan sejenisnya.
91
+
92
+ Untuk kasus yang lebih kompleks — gabungan banyak tabel, metrik khusus domain
93
+ (rating bintang, okupansi kamar), atau data non-transaksional (blog/CMS, inventori murni) —
94
+ gunakan pendekatan **adapter manual** ([Bab 7](#7-cara-membuat-adapter-untuk-domain-bisnis-baru))
95
+ atau **preset** ([Bab 3](#3-preset-yang-tersedia)) yang memberi kontrol penuh.
96
+
97
+ ---
98
+
31
99
  ## 1. Instalasi
32
100
 
33
101
  ```bash
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
 
@@ -2211,12 +2524,17 @@ function SetupWizard({ issues = [], onDismiss, supabase }) {
2211
2524
  setSupabaseOk(true);
2212
2525
  return;
2213
2526
  }
2214
- const { error: ping } = await supabase.from("_rdb_").select("*").limit(1);
2215
- const ok = !ping || ping.code === "42P01" || ping.code === "PGRST116" || (ping.message || "").includes("does not exist");
2216
- setSupabaseOk(ok);
2217
- if (ok) setTableBlocked(true);
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);
2531
+ } else {
2532
+ setSupabaseOk(!!supabase);
2533
+ setTableBlocked(true);
2534
+ }
2218
2535
  } catch {
2219
- setSupabaseOk(false);
2536
+ setSupabaseOk(!!supabase);
2537
+ setTableBlocked(true);
2220
2538
  } finally {
2221
2539
  setDetecting(false);
2222
2540
  setDetectionDone(true);
@@ -2695,6 +3013,152 @@ ReusableDashboardView.propTypes = {
2695
3013
  dashboardConfig: import_prop_types18.default.object
2696
3014
  };
2697
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
+
2698
3162
  // src/utils/labels.js
2699
3163
  function createDashboardLabels(t) {
2700
3164
  const labels = {
@@ -2816,6 +3280,7 @@ function createDashboardLabels(t) {
2816
3280
  }
2817
3281
  // Annotate the CommonJS export names for ESM import in node:
2818
3282
  0 && (module.exports = {
3283
+ AutoDashboard,
2819
3284
  Badge,
2820
3285
  Button,
2821
3286
  ChartCard,
@@ -2837,6 +3302,7 @@ function createDashboardLabels(t) {
2837
3302
  adaptCidikaDashboardData,
2838
3303
  adaptDummyUmkmData,
2839
3304
  adaptTokoSepatuData,
3305
+ adaptUniversalData,
2840
3306
  buildDayBuckets,
2841
3307
  cidikaWidgetConfig,
2842
3308
  createCidikaSupabaseSource,
@@ -2846,7 +3312,10 @@ function createDashboardLabels(t) {
2846
3312
  createEmptyDashboardData,
2847
3313
  createEmptyDummyUmkmData,
2848
3314
  createEmptyTokoSepatuData,
3315
+ createEmptyUniversalData,
2849
3316
  createTokoSepatuSupabaseSource,
3317
+ createUniversalSource,
3318
+ createUniversalWidgetConfig,
2850
3319
  dummyUmkmWidgetConfig,
2851
3320
  formatDate,
2852
3321
  formatIDR,