@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 +14 -0
- package/README.md +69 -1
- package/dist/index.cjs +474 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +469 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
[](./CHANGELOG.md)
|
|
8
8
|
[](./LICENSE)
|
|
9
9
|
[](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("
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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(
|
|
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,
|