@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/dist/index.js CHANGED
@@ -309,6 +309,111 @@ function validateDashboardConfig(dashboardConfig) {
309
309
  return { valid: issues.length === 0, issues };
310
310
  }
311
311
 
312
+ // src/config/createUniversalWidgetConfig.js
313
+ function createUniversalWidgetConfig(columns = {}, opts = {}) {
314
+ const hasAudience = Boolean(columns.audience);
315
+ const hasItem = Boolean(columns.item);
316
+ const hasCustomer = Boolean(columns.customer);
317
+ const hasTotal = Boolean(columns.total);
318
+ const charts = [
319
+ { id: "dailyTrends", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
320
+ { id: "statusDistribution", type: "statusPie", label: "statusDistribution", icon: "PieChart" }
321
+ ];
322
+ if (hasAudience) {
323
+ charts.push({
324
+ id: "audienceDistribution",
325
+ type: "audiencePie",
326
+ label: "audienceDistribution",
327
+ icon: "Users"
328
+ });
329
+ }
330
+ if (hasItem) {
331
+ charts.push({
332
+ id: "topPackages",
333
+ type: "topPackagesBar",
334
+ label: "topPackages",
335
+ icon: "TrendingUp"
336
+ });
337
+ }
338
+ const tableColumns = [
339
+ { id: "date", label: "date", accessor: "createdAt", type: "date" }
340
+ ];
341
+ if (hasCustomer) {
342
+ tableColumns.push({ id: "customer", label: "customer", accessor: "customerName" });
343
+ }
344
+ if (hasItem) {
345
+ tableColumns.push({ id: "package", label: "package", accessor: "packageName" });
346
+ }
347
+ if (hasAudience) {
348
+ tableColumns.push({ id: "audience", label: "audience", accessor: "audienceLabel" });
349
+ }
350
+ if (hasTotal) {
351
+ tableColumns.push({ id: "total", label: "total", accessor: "totalIDR", type: "currency" });
352
+ }
353
+ tableColumns.push({
354
+ id: "status",
355
+ label: "status",
356
+ accessor: "statusLabel",
357
+ type: "statusBadge",
358
+ statusAccessor: "status"
359
+ });
360
+ return {
361
+ id: opts.id || "universal.dashboard",
362
+ defaultFilters: {
363
+ statusScope: "confirmed",
364
+ includePendingOverlay: false,
365
+ audience: "",
366
+ daysPreset: 30,
367
+ sortPkgBy: "bookings",
368
+ sortPkgDir: "desc"
369
+ },
370
+ widgets: {
371
+ stats: [
372
+ {
373
+ id: "bookingsConfirm",
374
+ label: "confirmedBookings",
375
+ icon: "TrendingUp",
376
+ valueKey: "bookingsConfirm",
377
+ format: "number",
378
+ accentColor: "blue"
379
+ },
380
+ {
381
+ id: "revenueConfirm",
382
+ label: "confirmedRevenue",
383
+ icon: "DollarSign",
384
+ valueKey: "revenueConfirm",
385
+ format: "currency",
386
+ accentColor: "green"
387
+ },
388
+ {
389
+ id: "avgRevenue",
390
+ label: "avgRevenue",
391
+ icon: "Users",
392
+ valueKey: "avgRevenue",
393
+ format: "currency",
394
+ accentColor: "violet"
395
+ },
396
+ {
397
+ id: "conversionRate",
398
+ label: "conversionRate",
399
+ icon: "PieChart",
400
+ valueKey: "conversionRate",
401
+ format: "percent",
402
+ accentColor: "orange"
403
+ }
404
+ ],
405
+ charts,
406
+ table: {
407
+ id: "recentBookings",
408
+ label: "recentBookings",
409
+ icon: "Calendar",
410
+ emptyLabel: "noRecentBookings",
411
+ columns: tableColumns
412
+ }
413
+ }
414
+ };
415
+ }
416
+
312
417
  // src/utils/formatters.js
313
418
  function formatIDR(value) {
314
419
  try {
@@ -714,6 +819,131 @@ function adaptTokoSepatuData({ raw, filters, range, dateLocale, labels }) {
714
819
  };
715
820
  }
716
821
 
822
+ // src/data-adapter/universalAdapter.js
823
+ function createEmptyUniversalData() {
824
+ return {
825
+ stats: {
826
+ bookingsConfirm: 0,
827
+ bookingsPending: 0,
828
+ revenueConfirm: 0,
829
+ avgRevenue: 0,
830
+ conversionRate: 0
831
+ },
832
+ charts: {
833
+ dailyTrends: [],
834
+ statusDistribution: [],
835
+ audienceDistribution: [],
836
+ topPackages: []
837
+ },
838
+ table: {
839
+ recentBookings: []
840
+ }
841
+ };
842
+ }
843
+ function adaptUniversalData({
844
+ raw,
845
+ filters = {},
846
+ range,
847
+ dateLocale = "id-ID",
848
+ labels = {},
849
+ options = {}
850
+ }) {
851
+ if (!raw) return createEmptyUniversalData();
852
+ const confirmedValue = String(options.confirmedValue || "confirmed").toLowerCase();
853
+ const pendingValue = String(options.pendingValue || "pending").toLowerCase();
854
+ const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
855
+ const dayLookup = new Map(dailyBuckets.map((bucket) => [bucket.dateKey, bucket]));
856
+ const statusMap = /* @__PURE__ */ new Map();
857
+ const audienceMap = /* @__PURE__ */ new Map();
858
+ const itemCountMap = /* @__PURE__ */ new Map();
859
+ const itemRevenueMap = /* @__PURE__ */ new Map();
860
+ let bookingsConfirm = 0;
861
+ let bookingsPending = 0;
862
+ let revenueConfirm = 0;
863
+ const safeStatusLabel = typeof labels.formatStatusLabel === "function" ? labels.formatStatusLabel : (s) => String(s);
864
+ const safeAudienceLabel = typeof labels.formatAudienceLabel === "function" ? labels.formatAudienceLabel : (a) => String(a);
865
+ (raw.bookings || []).forEach((row) => {
866
+ const dayKey = String(row.created_at || "").slice(0, 10);
867
+ const status = String(row.status || pendingValue).toLowerCase();
868
+ const total = toNumber(row.total_idr);
869
+ const audience = row.audience || "unknown";
870
+ const item = row.item || "-";
871
+ statusMap.set(status, (statusMap.get(status) || 0) + 1);
872
+ audienceMap.set(audience, (audienceMap.get(audience) || 0) + 1);
873
+ const bucket = dayLookup.get(dayKey);
874
+ if (bucket) {
875
+ if (status === confirmedValue) {
876
+ bucket.count += 1;
877
+ bucket.revenue += total;
878
+ }
879
+ if (status === pendingValue) {
880
+ bucket.pendingCount += 1;
881
+ }
882
+ }
883
+ if (status === confirmedValue) {
884
+ bookingsConfirm += 1;
885
+ revenueConfirm += total;
886
+ itemCountMap.set(item, (itemCountMap.get(item) || 0) + 1);
887
+ itemRevenueMap.set(item, (itemRevenueMap.get(item) || 0) + total);
888
+ } else if (status === pendingValue) {
889
+ bookingsPending += 1;
890
+ }
891
+ });
892
+ const statusDistribution = sortMapEntries(statusMap, "desc").map(
893
+ ([status, count]) => ({
894
+ status,
895
+ label: safeStatusLabel(status),
896
+ count
897
+ })
898
+ );
899
+ const audienceDistribution = sortMapEntries(audienceMap, "desc").map(
900
+ ([audience, count]) => ({
901
+ audience,
902
+ label: safeAudienceLabel(audience),
903
+ count
904
+ })
905
+ );
906
+ const metricMap = filters.sortPkgBy === "revenue" ? itemRevenueMap : itemCountMap;
907
+ const topPackages = sortMapEntries(metricMap, filters.sortPkgDir || "desc").slice(0, 5).map(([item, value]) => ({
908
+ packageId: item,
909
+ name: item,
910
+ value: toNumber(value)
911
+ }));
912
+ const recentBookings = (raw.recent || []).map((row) => {
913
+ const status = String(row.status || pendingValue).toLowerCase();
914
+ return {
915
+ id: row.id,
916
+ createdAt: row.created_at,
917
+ customerName: row.customer_name || "-",
918
+ packageName: row.item || "-",
919
+ audienceLabel: safeAudienceLabel(row.audience),
920
+ totalIDR: toNumber(row.total_idr),
921
+ status,
922
+ statusLabel: safeStatusLabel(status)
923
+ };
924
+ });
925
+ const avgRevenue = bookingsConfirm > 0 ? Math.round(revenueConfirm / bookingsConfirm) : 0;
926
+ const conversionRate = bookingsConfirm + bookingsPending > 0 ? Math.round(bookingsConfirm / (bookingsConfirm + bookingsPending) * 100) : 0;
927
+ return {
928
+ stats: {
929
+ bookingsConfirm,
930
+ bookingsPending,
931
+ revenueConfirm,
932
+ avgRevenue,
933
+ conversionRate
934
+ },
935
+ charts: {
936
+ dailyTrends: dailyBuckets,
937
+ statusDistribution,
938
+ audienceDistribution,
939
+ topPackages
940
+ },
941
+ table: {
942
+ recentBookings
943
+ }
944
+ };
945
+ }
946
+
717
947
  // src/data-source/cidikaSupabaseSource.js
718
948
  function ensureNoError(response, message) {
719
949
  if (response == null ? void 0 : response.error) {
@@ -887,6 +1117,84 @@ function createTokoSepatuSupabaseSource(supabase) {
887
1117
  };
888
1118
  }
889
1119
 
1120
+ // src/data-source/universalSupabaseSource.js
1121
+ function createUniversalSource(supabase, { table, columns = {} } = {}) {
1122
+ if (!supabase) {
1123
+ throw new Error("createUniversalSource: prop 'supabase' wajib diisi.");
1124
+ }
1125
+ if (!table) {
1126
+ throw new Error("createUniversalSource: opsi 'table' wajib diisi.");
1127
+ }
1128
+ if (!columns.date) {
1129
+ throw new Error(
1130
+ "createUniversalSource: columns.date wajib diisi (kolom timestamp)."
1131
+ );
1132
+ }
1133
+ const map = {
1134
+ date: columns.date,
1135
+ status: columns.status || null,
1136
+ total: columns.total || null,
1137
+ customer: columns.customer || null,
1138
+ item: columns.item || null,
1139
+ audience: columns.audience || null
1140
+ };
1141
+ const selectCols = Array.from(
1142
+ /* @__PURE__ */ new Set(["id", ...Object.values(map).filter(Boolean)])
1143
+ ).join(", ");
1144
+ const normalize = (row) => ({
1145
+ id: row.id ?? row[map.date],
1146
+ created_at: row[map.date],
1147
+ status: map.status ? row[map.status] : "confirmed",
1148
+ total_idr: map.total ? row[map.total] : 0,
1149
+ item: map.item ? row[map.item] : null,
1150
+ customer_name: map.customer ? row[map.customer] : null,
1151
+ audience: map.audience ? row[map.audience] : "unknown"
1152
+ });
1153
+ return {
1154
+ async fetchDashboardSnapshot({ fromISO, toISO, audience, statusScope }) {
1155
+ const baseQuery = () => supabase.from(table).select(selectCols).gte(map.date, fromISO).lte(map.date, toISO);
1156
+ const bookingsQuery = baseQuery().order(map.date, { ascending: true });
1157
+ if (audience && map.audience) bookingsQuery.eq(map.audience, audience);
1158
+ const recentQuery = baseQuery().order(map.date, { ascending: false }).limit(10);
1159
+ if (audience && map.audience) recentQuery.eq(map.audience, audience);
1160
+ if (statusScope && statusScope !== "all" && map.status) {
1161
+ recentQuery.eq(map.status, statusScope);
1162
+ }
1163
+ const [bookingsRes, recentRes] = await Promise.all([
1164
+ bookingsQuery,
1165
+ recentQuery
1166
+ ]);
1167
+ if (bookingsRes == null ? void 0 : bookingsRes.error) {
1168
+ const err = new Error(
1169
+ `Gagal membaca tabel "${table}". Periksa nama tabel/kolom & RLS policy.`
1170
+ );
1171
+ err.cause = bookingsRes.error;
1172
+ throw err;
1173
+ }
1174
+ const bookings = (bookingsRes.data || []).map(normalize);
1175
+ const recent = (recentRes.error ? [] : recentRes.data || []).map(
1176
+ normalize
1177
+ );
1178
+ return {
1179
+ bookings,
1180
+ recent,
1181
+ packageLocales: [],
1182
+ staticCounts: {}
1183
+ };
1184
+ },
1185
+ subscribeLiveUpdate(onEvent) {
1186
+ const channel = supabase.channel(`reusable-dashboard-universal-${table}`).on(
1187
+ "postgres_changes",
1188
+ { event: "*", schema: "public", table },
1189
+ onEvent
1190
+ ).subscribe();
1191
+ return () => {
1192
+ supabase.removeChannel(channel);
1193
+ };
1194
+ }
1195
+ };
1196
+ }
1197
+
890
1198
  // src/hooks/useReusableDashboard.js
891
1199
  import { useCallback, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
892
1200
 
@@ -2162,12 +2470,17 @@ function SetupWizard({ issues = [], onDismiss, supabase }) {
2162
2470
  setSupabaseOk(true);
2163
2471
  return;
2164
2472
  }
2165
- const { error: ping } = await supabase.from("_rdb_").select("*").limit(1);
2166
- const ok = !ping || ping.code === "42P01" || ping.code === "PGRST116" || (ping.message || "").includes("does not exist");
2167
- setSupabaseOk(ok);
2168
- if (ok) setTableBlocked(true);
2473
+ const { error: ping } = await supabase.from("_rdb_wizard_ping_").select("*").limit(1);
2474
+ if (!ping || ping.code === "42P01" || ping.code === "PGRST116" || ping.code === "PGRST200" || ping.message && (ping.message.includes("does not exist") || ping.message.includes("not found") || ping.message.includes("relation"))) {
2475
+ setSupabaseOk(true);
2476
+ setTableBlocked(true);
2477
+ } else {
2478
+ setSupabaseOk(!!supabase);
2479
+ setTableBlocked(true);
2480
+ }
2169
2481
  } catch {
2170
- setSupabaseOk(false);
2482
+ setSupabaseOk(!!supabase);
2483
+ setTableBlocked(true);
2171
2484
  } finally {
2172
2485
  setDetecting(false);
2173
2486
  setDetectionDone(true);
@@ -2646,6 +2959,152 @@ ReusableDashboardView.propTypes = {
2646
2959
  dashboardConfig: PropTypes18.object
2647
2960
  };
2648
2961
 
2962
+ // src/presentation/AutoDashboard.jsx
2963
+ import React19 from "react";
2964
+ import PropTypes19 from "prop-types";
2965
+ var DEFAULT_LABELS = {
2966
+ title: "Dashboard",
2967
+ refresh: "Muat ulang",
2968
+ liveUpdate: "Live",
2969
+ loadFailed: "Gagal memuat data dashboard.",
2970
+ retry: "Coba lagi",
2971
+ confirmedOnly: "Hanya berhasil",
2972
+ pendingOnly: "Hanya menunggu",
2973
+ allStatus: "Semua status",
2974
+ showPendingOverlay: "Tampilkan overlay menunggu",
2975
+ allAudience: "Semua segmen",
2976
+ audienceDomestic: "Domestik",
2977
+ audienceForeign: "Asing",
2978
+ customDate: "Custom",
2979
+ reset: "Reset",
2980
+ topSort: "Urutkan item",
2981
+ sortBookings: "Jumlah",
2982
+ sortRevenue: "Nilai",
2983
+ sortDesc: "Turun",
2984
+ sortAsc: "Naik",
2985
+ confirmedBookings: "Transaksi Berhasil",
2986
+ confirmedRevenue: "Pendapatan (Berhasil)",
2987
+ avgRevenue: "Rata-rata Nilai / Transaksi",
2988
+ conversionRate: "Tingkat Konversi",
2989
+ dailyTrends: "Tren Harian",
2990
+ statusDistribution: "Distribusi Status",
2991
+ audienceDistribution: "Distribusi Segmen",
2992
+ topPackages: "Item Teratas",
2993
+ recentBookings: "Transaksi Terbaru",
2994
+ date: "Tanggal",
2995
+ customer: "Pelanggan",
2996
+ package: "Item",
2997
+ audience: "Segmen",
2998
+ total: "Total",
2999
+ status: "Status",
3000
+ noRecentBookings: "Belum ada transaksi terbaru",
3001
+ unknownAudience: "Tidak diketahui"
3002
+ };
3003
+ function buildLabels(overrides = {}) {
3004
+ const labels = { ...DEFAULT_LABELS, ...overrides };
3005
+ labels.dayLabel = overrides.dayLabel || ((count) => count ? `${count} hari` : "Custom");
3006
+ labels.formatStatusLabel = overrides.formatStatusLabel || ((status) => {
3007
+ const s = String(status || "pending");
3008
+ return s.charAt(0).toUpperCase() + s.slice(1);
3009
+ });
3010
+ labels.formatAudienceLabel = overrides.formatAudienceLabel || ((value) => {
3011
+ if (value === "domestic") return labels.audienceDomestic;
3012
+ if (value === "foreign") return labels.audienceForeign;
3013
+ if (!value || value === "unknown") return labels.unknownAudience;
3014
+ return String(value);
3015
+ });
3016
+ return labels;
3017
+ }
3018
+ function AutoDashboard({
3019
+ supabase,
3020
+ table,
3021
+ columns,
3022
+ confirmedValue = "confirmed",
3023
+ pendingValue = "pending",
3024
+ title = "Dashboard",
3025
+ labels: labelOverrides,
3026
+ languageCode = "id",
3027
+ dateLocale = "id-ID"
3028
+ }) {
3029
+ const isConfigured = Boolean(supabase && table && (columns == null ? void 0 : columns.date));
3030
+ const labels = React19.useMemo(
3031
+ () => buildLabels({ ...labelOverrides, title }),
3032
+ [labelOverrides, title]
3033
+ );
3034
+ const widgetConfig = React19.useMemo(
3035
+ () => createUniversalWidgetConfig(columns || {}),
3036
+ [columns]
3037
+ );
3038
+ const dataSource = React19.useMemo(() => {
3039
+ if (!isConfigured) return null;
3040
+ return createUniversalSource(supabase, { table, columns });
3041
+ }, [isConfigured, supabase, table, columns]);
3042
+ const adapter = React19.useMemo(
3043
+ () => (args) => adaptUniversalData({ ...args, options: { confirmedValue, pendingValue } }),
3044
+ [confirmedValue, pendingValue]
3045
+ );
3046
+ const dashboard = useReusableDashboard({
3047
+ config: widgetConfig,
3048
+ dataSource: dataSource || { fetchDashboardSnapshot: null },
3049
+ adapter,
3050
+ createEmptyState: createEmptyUniversalData,
3051
+ languageCode,
3052
+ dateLocale,
3053
+ labels
3054
+ });
3055
+ if (!isConfigured) {
3056
+ const issues = [];
3057
+ if (!supabase) issues.push("Prop 'supabase' belum diisi (Supabase client).");
3058
+ if (!table) issues.push("Prop 'table' belum diisi (nama tabel sumber data).");
3059
+ if (!(columns == null ? void 0 : columns.date))
3060
+ issues.push("Prop 'columns.date' belum diisi (kolom timestamp).");
3061
+ return /* @__PURE__ */ React19.createElement("div", { className: "rdb-view" }, /* @__PURE__ */ React19.createElement(SetupWizard, { issues, supabase, onDismiss: () => {
3062
+ } }));
3063
+ }
3064
+ return /* @__PURE__ */ React19.createElement(
3065
+ ReusableDashboardView,
3066
+ {
3067
+ config: widgetConfig,
3068
+ labels,
3069
+ loading: dashboard.loading,
3070
+ error: dashboard.error,
3071
+ filters: dashboard.filters,
3072
+ onFilterChange: dashboard.updateFilter,
3073
+ onResetFilters: dashboard.resetFilters,
3074
+ onRefresh: dashboard.refresh,
3075
+ data: dashboard.data,
3076
+ dateLocale,
3077
+ liveUpdateEnabled: dashboard.liveUpdateEnabled,
3078
+ supabase
3079
+ }
3080
+ );
3081
+ }
3082
+ AutoDashboard.propTypes = {
3083
+ /** Supabase client instance (WAJIB). */
3084
+ supabase: PropTypes19.object,
3085
+ /** Nama tabel sumber data (WAJIB), mis. "bookings" / "orders". */
3086
+ table: PropTypes19.string,
3087
+ /** Pemetaan kolom: { date, status, total, customer, item, audience }. date WAJIB. */
3088
+ columns: PropTypes19.shape({
3089
+ date: PropTypes19.string,
3090
+ status: PropTypes19.string,
3091
+ total: PropTypes19.string,
3092
+ customer: PropTypes19.string,
3093
+ item: PropTypes19.string,
3094
+ audience: PropTypes19.string
3095
+ }),
3096
+ /** Nilai status yang dihitung "berhasil" (default "confirmed"). */
3097
+ confirmedValue: PropTypes19.string,
3098
+ /** Nilai status yang dihitung "menunggu" (default "pending"). */
3099
+ pendingValue: PropTypes19.string,
3100
+ /** Judul dashboard. */
3101
+ title: PropTypes19.string,
3102
+ /** Override sebagian/seluruh label UI. */
3103
+ labels: PropTypes19.object,
3104
+ languageCode: PropTypes19.string,
3105
+ dateLocale: PropTypes19.string
3106
+ };
3107
+
2649
3108
  // src/utils/labels.js
2650
3109
  function createDashboardLabels(t) {
2651
3110
  const labels = {
@@ -2766,6 +3225,7 @@ function createDashboardLabels(t) {
2766
3225
  return labels;
2767
3226
  }
2768
3227
  export {
3228
+ AutoDashboard,
2769
3229
  Badge,
2770
3230
  Button,
2771
3231
  ChartCard,
@@ -2787,6 +3247,7 @@ export {
2787
3247
  adaptCidikaDashboardData,
2788
3248
  adaptDummyUmkmData,
2789
3249
  adaptTokoSepatuData,
3250
+ adaptUniversalData,
2790
3251
  buildDayBuckets,
2791
3252
  cidikaWidgetConfig,
2792
3253
  createCidikaSupabaseSource,
@@ -2796,7 +3257,10 @@ export {
2796
3257
  createEmptyDashboardData,
2797
3258
  createEmptyDummyUmkmData,
2798
3259
  createEmptyTokoSepatuData,
3260
+ createEmptyUniversalData,
2799
3261
  createTokoSepatuSupabaseSource,
3262
+ createUniversalSource,
3263
+ createUniversalWidgetConfig,
2800
3264
  dummyUmkmWidgetConfig,
2801
3265
  formatDate,
2802
3266
  formatIDR,