@rozaqi02/reusable-dashboard 1.1.1 → 1.1.3
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/README.md +1101 -58
- package/dist/index.cjs +437 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +284 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js +434 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -237,6 +237,78 @@ var tokoSepatuWidgetConfig = {
|
|
|
237
237
|
}
|
|
238
238
|
};
|
|
239
239
|
|
|
240
|
+
// src/config/createDashboardConfig.js
|
|
241
|
+
function createDashboardConfig({
|
|
242
|
+
widgetConfig,
|
|
243
|
+
dataSource,
|
|
244
|
+
adapter,
|
|
245
|
+
createEmptyState,
|
|
246
|
+
labels,
|
|
247
|
+
languageCode = "id",
|
|
248
|
+
dateLocale = "id-ID"
|
|
249
|
+
}) {
|
|
250
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
251
|
+
const missing = [];
|
|
252
|
+
if (!widgetConfig) missing.push("widgetConfig");
|
|
253
|
+
if (!dataSource) missing.push("dataSource");
|
|
254
|
+
if (!adapter) missing.push("adapter");
|
|
255
|
+
if (!createEmptyState) missing.push("createEmptyState");
|
|
256
|
+
return {
|
|
257
|
+
// Properti yang langsung dibaca useReusableDashboard
|
|
258
|
+
config: widgetConfig,
|
|
259
|
+
dataSource,
|
|
260
|
+
adapter,
|
|
261
|
+
createEmptyState,
|
|
262
|
+
languageCode,
|
|
263
|
+
dateLocale,
|
|
264
|
+
labels,
|
|
265
|
+
// Metadata untuk setup wizard & validasi
|
|
266
|
+
_meta: {
|
|
267
|
+
isValid: missing.length === 0,
|
|
268
|
+
missing,
|
|
269
|
+
hasDataSource: Boolean(dataSource == null ? void 0 : dataSource.fetchDashboardSnapshot),
|
|
270
|
+
hasRealtimeSupport: Boolean(dataSource == null ? void 0 : dataSource.subscribeLiveUpdate),
|
|
271
|
+
hasLabels: Boolean(labels),
|
|
272
|
+
widgetCount: {
|
|
273
|
+
stats: ((_b = (_a = widgetConfig == null ? void 0 : widgetConfig.widgets) == null ? void 0 : _a.stats) == null ? void 0 : _b.length) ?? 0,
|
|
274
|
+
charts: ((_d = (_c = widgetConfig == null ? void 0 : widgetConfig.widgets) == null ? void 0 : _c.charts) == null ? void 0 : _d.length) ?? 0,
|
|
275
|
+
tableColumns: ((_g = (_f = (_e = widgetConfig == null ? void 0 : widgetConfig.widgets) == null ? void 0 : _e.table) == null ? void 0 : _f.columns) == null ? void 0 : _g.length) ?? 0
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function validateDashboardConfig(dashboardConfig) {
|
|
281
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
282
|
+
const issues = [];
|
|
283
|
+
if (!dashboardConfig) {
|
|
284
|
+
return { valid: false, issues: ["dashboardConfig tidak ditemukan. Pastikan sudah memanggil createDashboardConfig()."] };
|
|
285
|
+
}
|
|
286
|
+
const meta = dashboardConfig == null ? void 0 : dashboardConfig._meta;
|
|
287
|
+
if (!meta) {
|
|
288
|
+
issues.push("Config tidak dibuat melalui createDashboardConfig(). Gunakan factory function ini untuk validasi otomatis.");
|
|
289
|
+
} else {
|
|
290
|
+
if (meta.missing.length > 0) {
|
|
291
|
+
meta.missing.forEach((m) => issues.push(`Properti wajib belum diisi: "${m}"`));
|
|
292
|
+
}
|
|
293
|
+
if (!meta.hasDataSource) {
|
|
294
|
+
issues.push("dataSource.fetchDashboardSnapshot() tidak ditemukan. Pastikan sudah membuat data source yang benar.");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (!((_c = (_b = (_a = dashboardConfig.config) == null ? void 0 : _a.widgets) == null ? void 0 : _b.stats) == null ? void 0 : _c.length)) {
|
|
298
|
+
issues.push("widgetConfig.widgets.stats kosong. Tambahkan minimal 1 stat card.");
|
|
299
|
+
}
|
|
300
|
+
if (!((_f = (_e = (_d = dashboardConfig.config) == null ? void 0 : _d.widgets) == null ? void 0 : _e.charts) == null ? void 0 : _f.length)) {
|
|
301
|
+
issues.push("widgetConfig.widgets.charts kosong. Tambahkan minimal 1 chart.");
|
|
302
|
+
}
|
|
303
|
+
if (!((_j = (_i = (_h = (_g = dashboardConfig.config) == null ? void 0 : _g.widgets) == null ? void 0 : _h.table) == null ? void 0 : _i.columns) == null ? void 0 : _j.length)) {
|
|
304
|
+
issues.push("widgetConfig.widgets.table.columns kosong. Tambahkan minimal 1 kolom tabel.");
|
|
305
|
+
}
|
|
306
|
+
if (!dashboardConfig.labels) {
|
|
307
|
+
issues.push("labels belum diisi. Sediakan objek labels agar teks UI tampil dengan benar.");
|
|
308
|
+
}
|
|
309
|
+
return { valid: issues.length === 0, issues };
|
|
310
|
+
}
|
|
311
|
+
|
|
240
312
|
// src/utils/formatters.js
|
|
241
313
|
function formatIDR(value) {
|
|
242
314
|
try {
|
|
@@ -1521,7 +1593,7 @@ function ChartCard({
|
|
|
1521
1593
|
className = ""
|
|
1522
1594
|
}) {
|
|
1523
1595
|
const content = (() => {
|
|
1524
|
-
if (loading) return /* @__PURE__ */ React12.createElement(SkeletonLoader, {
|
|
1596
|
+
if (loading) return /* @__PURE__ */ React12.createElement(SkeletonLoader, { style: { height: 256 } });
|
|
1525
1597
|
if (widget.type === "dailyArea") {
|
|
1526
1598
|
return renderDailyArea(labels, filters, chartData.dailyTrends);
|
|
1527
1599
|
}
|
|
@@ -1534,9 +1606,9 @@ function ChartCard({
|
|
|
1534
1606
|
if (widget.type === "topPackagesBar") {
|
|
1535
1607
|
return renderTopPackages(chartData.topPackages, labels, filters.sortPkgBy);
|
|
1536
1608
|
}
|
|
1537
|
-
return /* @__PURE__ */ React12.createElement("div", { className: "
|
|
1609
|
+
return /* @__PURE__ */ React12.createElement("div", { className: "rdb-muted" }, "Unsupported chart type.");
|
|
1538
1610
|
})();
|
|
1539
|
-
return /* @__PURE__ */ React12.createElement("div", { className: `card
|
|
1611
|
+
return /* @__PURE__ */ React12.createElement("div", { className: `rdb-card rdb-chart-card ${className}` }, /* @__PURE__ */ React12.createElement(
|
|
1540
1612
|
ChartHeader,
|
|
1541
1613
|
{
|
|
1542
1614
|
title: labels[widget.label] || widget.label,
|
|
@@ -1785,8 +1857,281 @@ DashboardLayout.propTypes = {
|
|
|
1785
1857
|
};
|
|
1786
1858
|
|
|
1787
1859
|
// src/presentation/ReusableDashboardView.jsx
|
|
1788
|
-
import
|
|
1860
|
+
import React18 from "react";
|
|
1861
|
+
import PropTypes18 from "prop-types";
|
|
1862
|
+
|
|
1863
|
+
// src/presentation/SetupWizard.jsx
|
|
1864
|
+
import React17, { useState as useState6 } from "react";
|
|
1789
1865
|
import PropTypes17 from "prop-types";
|
|
1866
|
+
function SetupWizard({ issues = [], onDismiss, supabase }) {
|
|
1867
|
+
const [step, setStep] = useState6(0);
|
|
1868
|
+
const [tables, setTables] = useState6(null);
|
|
1869
|
+
const [loadingTables, setLoadingTables] = useState6(false);
|
|
1870
|
+
const [tableError, setTableError] = useState6("");
|
|
1871
|
+
const [dismissed, setDismissed] = useState6(false);
|
|
1872
|
+
if (dismissed) return null;
|
|
1873
|
+
const steps = [
|
|
1874
|
+
{ id: 0, label: "Overview", icon: "\u{1F3E0}" },
|
|
1875
|
+
{ id: 1, label: "1. Data Source", icon: "\u{1F5C4}\uFE0F" },
|
|
1876
|
+
{ id: 2, label: "2. Adapter", icon: "\u{1F504}" },
|
|
1877
|
+
{ id: 3, label: "3. Widget Config", icon: "\u2699\uFE0F" }
|
|
1878
|
+
];
|
|
1879
|
+
async function handleReadTables() {
|
|
1880
|
+
if (!supabase) {
|
|
1881
|
+
setTableError("Supabase client belum tersambung. Pastikan prop supabase sudah diisi.");
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
setLoadingTables(true);
|
|
1885
|
+
setTableError("");
|
|
1886
|
+
try {
|
|
1887
|
+
const { data, error } = await supabase.from("information_schema.tables").select("table_name").eq("table_schema", "public").eq("table_type", "BASE TABLE").order("table_name");
|
|
1888
|
+
if (error) throw error;
|
|
1889
|
+
setTables((data || []).map((r) => r.table_name));
|
|
1890
|
+
} catch (err) {
|
|
1891
|
+
try {
|
|
1892
|
+
const { data: rpcData } = await supabase.rpc("get_tables");
|
|
1893
|
+
if (rpcData) {
|
|
1894
|
+
setTables(rpcData.map((r) => r.table_name || r));
|
|
1895
|
+
} else {
|
|
1896
|
+
setTableError("Tidak dapat membaca tabel. Pastikan RLS mengizinkan akses atau tambahkan policy SELECT untuk anon.");
|
|
1897
|
+
}
|
|
1898
|
+
} catch {
|
|
1899
|
+
setTableError("Gagal membaca tabel dari Supabase: " + ((err == null ? void 0 : err.message) || "Unknown error"));
|
|
1900
|
+
}
|
|
1901
|
+
} finally {
|
|
1902
|
+
setLoadingTables(false);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function handleDismiss() {
|
|
1906
|
+
setDismissed(true);
|
|
1907
|
+
if (onDismiss) onDismiss();
|
|
1908
|
+
}
|
|
1909
|
+
return /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-overlay", role: "dialog", "aria-modal": "true", "aria-label": "Dashboard Setup Wizard" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-modal" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-header" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-header-title" }, /* @__PURE__ */ React17.createElement("span", { className: "rdb-wizard-logo" }, "\u{1F6E0}\uFE0F"), /* @__PURE__ */ React17.createElement("div", null, /* @__PURE__ */ React17.createElement("div", { className: "rdb-h2", style: { margin: 0 } }, "Setup Dashboard"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption" }, "Panduan konfigurasi modul @rozaqi02/reusable-dashboard"))), /* @__PURE__ */ React17.createElement(
|
|
1910
|
+
"button",
|
|
1911
|
+
{
|
|
1912
|
+
type: "button",
|
|
1913
|
+
className: "rdb-wizard-close",
|
|
1914
|
+
onClick: handleDismiss,
|
|
1915
|
+
"aria-label": "Tutup wizard",
|
|
1916
|
+
title: "Lanjutkan tanpa wizard"
|
|
1917
|
+
},
|
|
1918
|
+
"\u2715"
|
|
1919
|
+
)), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-steps" }, steps.map((s) => /* @__PURE__ */ React17.createElement(
|
|
1920
|
+
"button",
|
|
1921
|
+
{
|
|
1922
|
+
key: s.id,
|
|
1923
|
+
type: "button",
|
|
1924
|
+
className: `rdb-wizard-step-btn ${step === s.id ? "rdb-wizard-step-active" : ""}`,
|
|
1925
|
+
onClick: () => setStep(s.id)
|
|
1926
|
+
},
|
|
1927
|
+
/* @__PURE__ */ React17.createElement("span", null, s.icon),
|
|
1928
|
+
/* @__PURE__ */ React17.createElement("span", null, s.label)
|
|
1929
|
+
))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-body" }, step === 0 && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-section" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-alert" }, /* @__PURE__ */ React17.createElement("span", { className: "rdb-wizard-alert-icon" }, "\u26A0\uFE0F"), /* @__PURE__ */ React17.createElement("div", null, /* @__PURE__ */ React17.createElement("div", { className: "rdb-body", style: { fontWeight: 600 } }, "Dashboard belum terkonfigurasi"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { marginTop: 4 } }, "Ditemukan ", issues.length, " masalah yang perlu diselesaikan sebelum dashboard dapat berjalan."))), issues.length > 0 && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-issues" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 8 } }, "Masalah yang ditemukan:"), issues.map((issue, i) => /* @__PURE__ */ React17.createElement("div", { key: i, className: "rdb-wizard-issue-item" }, /* @__PURE__ */ React17.createElement("span", { className: "rdb-wizard-issue-icon" }, "\u274C"), /* @__PURE__ */ React17.createElement("span", { className: "rdb-body" }, issue)))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-flow" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 12 } }, "Alur konfigurasi yang perlu dilakukan:"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-flow-steps" }, [
|
|
1930
|
+
{ num: "1", label: "Data Source", desc: "Tulis fungsi query ke Supabase" },
|
|
1931
|
+
{ num: "\u2192", label: "", desc: "" },
|
|
1932
|
+
{ num: "2", label: "Adapter", desc: "Petakan data mentah ke format standar" },
|
|
1933
|
+
{ num: "\u2192", label: "", desc: "" },
|
|
1934
|
+
{ num: "3", label: "Widget Config", desc: "Definisikan widget yang ditampilkan" }
|
|
1935
|
+
].map((item, i) => item.num === "\u2192" ? /* @__PURE__ */ React17.createElement("div", { key: i, className: "rdb-wizard-flow-arrow" }, "\u2192") : /* @__PURE__ */ React17.createElement("div", { key: i, className: "rdb-wizard-flow-box" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-flow-num" }, item.num), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-flow-label" }, item.label), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-flow-desc" }, item.desc))))), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 } }, /* @__PURE__ */ React17.createElement("button", { type: "button", className: "rdb-btn rdb-btn-secondary rdb-btn-sm", onClick: handleDismiss }, "Lanjutkan tanpa wizard"), /* @__PURE__ */ React17.createElement("button", { type: "button", className: "rdb-btn rdb-btn-primary rdb-btn-sm", onClick: () => setStep(1) }, "Mulai panduan \u2192"))), step === 1 && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-section" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ React17.createElement("span", { className: "rdb-wizard-step-num" }, "1"), /* @__PURE__ */ React17.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Data Source")), /* @__PURE__ */ React17.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 12 } }, "Data Source adalah fungsi yang membaca data dari Supabase. Buat file", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, " src/datasources/myDataSource.js"), " dengan isi berikut:"), /* @__PURE__ */ React17.createElement("pre", { className: "rdb-wizard-code" }, `// src/datasources/myDataSource.js
|
|
1936
|
+
export function createMySupabaseSource(supabase) {
|
|
1937
|
+
return {
|
|
1938
|
+
async fetchDashboardSnapshot({ fromISO, toISO, statusScope }) {
|
|
1939
|
+
const { data: orders = [] } = await supabase
|
|
1940
|
+
.from("MY_TABLE") // \u2190 ganti nama tabel kamu
|
|
1941
|
+
.select("id, created_at, total_price, status, customer_name")
|
|
1942
|
+
.gte("created_at", fromISO)
|
|
1943
|
+
.lte("created_at", toISO)
|
|
1944
|
+
.order("created_at", { ascending: true });
|
|
1945
|
+
|
|
1946
|
+
const { data: recent = [] } = await supabase
|
|
1947
|
+
.from("MY_TABLE")
|
|
1948
|
+
.select("id, created_at, customer_name, total_price, status")
|
|
1949
|
+
.order("created_at", { ascending: false })
|
|
1950
|
+
.limit(10);
|
|
1951
|
+
|
|
1952
|
+
return {
|
|
1953
|
+
bookings: orders, // WAJIB: nama harus "bookings"
|
|
1954
|
+
recent, // WAJIB: nama harus "recent"
|
|
1955
|
+
packageLocales: [],
|
|
1956
|
+
staticCounts: { packages: 0, sections: 0 },
|
|
1957
|
+
};
|
|
1958
|
+
},
|
|
1959
|
+
|
|
1960
|
+
// Opsional: live update
|
|
1961
|
+
subscribeLiveUpdate(onEvent) {
|
|
1962
|
+
const ch = supabase.channel("my-dashboard-live")
|
|
1963
|
+
.on("postgres_changes",
|
|
1964
|
+
{ event: "*", schema: "public", table: "MY_TABLE" }, onEvent)
|
|
1965
|
+
.subscribe();
|
|
1966
|
+
return () => supabase.removeChannel(ch);
|
|
1967
|
+
},
|
|
1968
|
+
};
|
|
1969
|
+
}`), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-supabase-reader" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { fontWeight: 600, marginBottom: 8 } }, "\u{1F50D} Baca tabel dari Supabase project kamu secara langsung:"), /* @__PURE__ */ React17.createElement(
|
|
1970
|
+
"button",
|
|
1971
|
+
{
|
|
1972
|
+
type: "button",
|
|
1973
|
+
className: "rdb-btn rdb-btn-secondary rdb-btn-sm",
|
|
1974
|
+
onClick: handleReadTables,
|
|
1975
|
+
disabled: loadingTables
|
|
1976
|
+
},
|
|
1977
|
+
loadingTables ? "Membaca..." : "Tampilkan daftar tabel Supabase"
|
|
1978
|
+
), tableError && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-table-error" }, tableError), tables && tables.length > 0 && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-table-list" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { marginBottom: 6 } }, "Tabel ditemukan (", tables.length, "):"), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-table-chips" }, tables.map((t) => /* @__PURE__ */ React17.createElement("span", { key: t, className: "rdb-wizard-chip" }, t))), /* @__PURE__ */ React17.createElement("div", { className: "rdb-caption", style: { marginTop: 8, color: "var(--rdb-text-muted)" } }, "Ganti ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "MY_TABLE"), " di contoh kode di atas dengan nama tabel yang sesuai.")), !supabase && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-table-error" }, "Tambahkan prop ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "supabase"), " ke", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, " <ReusableDashboardView supabase=", "{supabase}", " />"), " untuk mengaktifkan fitur ini.")), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 } }, /* @__PURE__ */ React17.createElement("button", { type: "button", className: "rdb-btn rdb-btn-secondary rdb-btn-sm", onClick: () => setStep(0) }, "\u2190 Kembali"), /* @__PURE__ */ React17.createElement("button", { type: "button", className: "rdb-btn rdb-btn-primary rdb-btn-sm", onClick: () => setStep(2) }, "Lanjut \u2192"))), step === 2 && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-section" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ React17.createElement("span", { className: "rdb-wizard-step-num" }, "2"), /* @__PURE__ */ React17.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Data Adapter")), /* @__PURE__ */ React17.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 12 } }, "Adapter mengubah data mentah dari Data Source ke format standar yang dimengerti komponen dashboard. Buat file ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "src/adapters/myAdapter.js"), ":"), /* @__PURE__ */ React17.createElement("pre", { className: "rdb-wizard-code" }, `// src/adapters/myAdapter.js
|
|
1979
|
+
import { toNumber, buildDayBuckets } from "@rozaqi02/reusable-dashboard";
|
|
1980
|
+
|
|
1981
|
+
export function createEmptyMyData() {
|
|
1982
|
+
return {
|
|
1983
|
+
stats: { bookingsConfirm: 0, revenueConfirm: 0 },
|
|
1984
|
+
charts: {
|
|
1985
|
+
dailyTrends: [],
|
|
1986
|
+
statusDistribution: [],
|
|
1987
|
+
audienceDistribution: [],
|
|
1988
|
+
topPackages: [],
|
|
1989
|
+
},
|
|
1990
|
+
table: { recentBookings: [] },
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
export function adaptMyData({ raw, range, dateLocale, labels }) {
|
|
1995
|
+
if (!raw) return createEmptyMyData();
|
|
1996
|
+
|
|
1997
|
+
const buckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
|
|
1998
|
+
const dayMap = new Map(buckets.map(b => [b.dateKey, b]));
|
|
1999
|
+
const statusMap = new Map();
|
|
2000
|
+
let confirmed = 0, revenue = 0;
|
|
2001
|
+
|
|
2002
|
+
(raw.bookings || []).forEach(row => {
|
|
2003
|
+
const status = String(row.status || "pending").toLowerCase();
|
|
2004
|
+
const amount = toNumber(row.total_price); // \u2190 sesuaikan nama kolom
|
|
2005
|
+
const dayKey = String(row.created_at || "").slice(0, 10);
|
|
2006
|
+
|
|
2007
|
+
statusMap.set(status, (statusMap.get(status) || 0) + 1);
|
|
2008
|
+
const bucket = dayMap.get(dayKey);
|
|
2009
|
+
if (bucket && status === "confirmed") {
|
|
2010
|
+
bucket.count += 1;
|
|
2011
|
+
bucket.revenue += amount;
|
|
2012
|
+
}
|
|
2013
|
+
if (status === "confirmed") { confirmed++; revenue += amount; }
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
return {
|
|
2017
|
+
stats: { bookingsConfirm: confirmed, revenueConfirm: revenue },
|
|
2018
|
+
charts: {
|
|
2019
|
+
dailyTrends: buckets,
|
|
2020
|
+
statusDistribution: Array.from(statusMap.entries())
|
|
2021
|
+
.map(([status, count]) => ({
|
|
2022
|
+
status,
|
|
2023
|
+
label: labels?.formatStatusLabel?.(status) || status,
|
|
2024
|
+
count,
|
|
2025
|
+
})),
|
|
2026
|
+
audienceDistribution: [],
|
|
2027
|
+
topPackages: [],
|
|
2028
|
+
},
|
|
2029
|
+
table: {
|
|
2030
|
+
recentBookings: (raw.recent || []).map(row => ({
|
|
2031
|
+
id: row.id,
|
|
2032
|
+
createdAt: row.created_at,
|
|
2033
|
+
customerName: row.customer_name || "-",
|
|
2034
|
+
packageName: row.service_type || "-", // \u2190 sesuaikan nama kolom
|
|
2035
|
+
audienceLabel: "-",
|
|
2036
|
+
totalIDR: toNumber(row.total_price),
|
|
2037
|
+
status: String(row.status || "pending").toLowerCase(),
|
|
2038
|
+
statusLabel: labels?.formatStatusLabel?.(row.status) || row.status,
|
|
2039
|
+
})),
|
|
2040
|
+
},
|
|
2041
|
+
};
|
|
2042
|
+
}`), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-tip" }, "\u{1F4A1} ", /* @__PURE__ */ React17.createElement("strong", null, "Aturan penting:"), " key di ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "stats"), " (mis. ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "bookingsConfirm"), ") harus cocok dengan ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "valueKey"), " di widget config pada Step 3."), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 } }, /* @__PURE__ */ React17.createElement("button", { type: "button", className: "rdb-btn rdb-btn-secondary rdb-btn-sm", onClick: () => setStep(1) }, "\u2190 Kembali"), /* @__PURE__ */ React17.createElement("button", { type: "button", className: "rdb-btn rdb-btn-primary rdb-btn-sm", onClick: () => setStep(3) }, "Lanjut \u2192"))), step === 3 && /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-section" }, /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-step-title" }, /* @__PURE__ */ React17.createElement("span", { className: "rdb-wizard-step-num" }, "3"), /* @__PURE__ */ React17.createElement("span", { className: "rdb-h3", style: { margin: 0 } }, "Widget Config & Rangkaian Akhir")), /* @__PURE__ */ React17.createElement("div", { className: "rdb-body", style: { color: "var(--rdb-text-muted)", marginBottom: 12 } }, "Widget Config mendefinisikan tampilan dashboard: kartu mana, chart apa, kolom tabel apa. Gunakan ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "createDashboardConfig()"), " untuk mengemas semuanya jadi 1 objek:"), /* @__PURE__ */ React17.createElement("pre", { className: "rdb-wizard-code" }, `// src/pages/MyDashboard.jsx
|
|
2043
|
+
import { supabase } from "../lib/supabaseClient";
|
|
2044
|
+
import {
|
|
2045
|
+
ReusableDashboardView,
|
|
2046
|
+
useReusableDashboard,
|
|
2047
|
+
createDashboardConfig,
|
|
2048
|
+
} from "@rozaqi02/reusable-dashboard";
|
|
2049
|
+
|
|
2050
|
+
import { createMySupabaseSource } from "../datasources/myDataSource";
|
|
2051
|
+
import { adaptMyData, createEmptyMyData } from "../adapters/myAdapter";
|
|
2052
|
+
|
|
2053
|
+
// 1. Widget Config \u2014 deklaratif, tentukan apa yang tampil
|
|
2054
|
+
const myWidgetConfig = {
|
|
2055
|
+
id: "my.dashboard",
|
|
2056
|
+
defaultFilters: { statusScope: "confirmed", daysPreset: 30 },
|
|
2057
|
+
widgets: {
|
|
2058
|
+
stats: [
|
|
2059
|
+
{ id: "orders", label: "confirmedBookings", icon: "TrendingUp",
|
|
2060
|
+
valueKey: "bookingsConfirm", format: "number", accentColor: "blue" },
|
|
2061
|
+
{ id: "revenue", label: "confirmedRevenue", icon: "DollarSign",
|
|
2062
|
+
valueKey: "revenueConfirm", format: "currency", accentColor: "green" },
|
|
2063
|
+
],
|
|
2064
|
+
charts: [
|
|
2065
|
+
{ id: "trend", type: "dailyArea", label: "dailyTrends", icon: "BarChart3" },
|
|
2066
|
+
{ id: "status", type: "statusPie", label: "statusDistribution", icon: "PieChart" },
|
|
2067
|
+
],
|
|
2068
|
+
table: {
|
|
2069
|
+
id: "recent", label: "recentBookings", icon: "Calendar",
|
|
2070
|
+
emptyLabel: "noRecentBookings",
|
|
2071
|
+
columns: [
|
|
2072
|
+
{ id: "date", label: "date", accessor: "createdAt", type: "date" },
|
|
2073
|
+
{ id: "customer", label: "customer", accessor: "customerName" },
|
|
2074
|
+
{ id: "total", label: "total", accessor: "totalIDR", type: "currency" },
|
|
2075
|
+
{ id: "status", label: "status", accessor: "statusLabel",
|
|
2076
|
+
type: "statusBadge", statusAccessor: "status" },
|
|
2077
|
+
],
|
|
2078
|
+
},
|
|
2079
|
+
},
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
// 2. Kemas semua jadi 1 objek (buat di luar komponen, sekali saja)
|
|
2083
|
+
const dashConfig = createDashboardConfig({
|
|
2084
|
+
widgetConfig: myWidgetConfig,
|
|
2085
|
+
dataSource: createMySupabaseSource(supabase),
|
|
2086
|
+
adapter: adaptMyData,
|
|
2087
|
+
createEmptyState: createEmptyMyData,
|
|
2088
|
+
languageCode: "id",
|
|
2089
|
+
dateLocale: "id-ID",
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
// 3. Labels \u2014 teks UI
|
|
2093
|
+
const labels = { title: "Dashboard Saya", refresh: "Refresh",
|
|
2094
|
+
liveUpdate: "Live", loadFailed: "Gagal memuat.", retry: "Coba Lagi",
|
|
2095
|
+
confirmedBookings: "Total Order", confirmedRevenue: "Total Pendapatan",
|
|
2096
|
+
dailyTrends: "Tren Harian", statusDistribution: "Distribusi Status",
|
|
2097
|
+
recentBookings: "Order Terbaru", noRecentBookings: "Belum ada order",
|
|
2098
|
+
date: "Tanggal", customer: "Pelanggan", total: "Total", status: "Status",
|
|
2099
|
+
bookingsMetric: "Order", revenueMetric: "Pendapatan",
|
|
2100
|
+
confirmedBookingMetric: "Order (Confirmed)",
|
|
2101
|
+
confirmedRevenueMetric: "Pendapatan (Confirmed)",
|
|
2102
|
+
dayLabel: n => n + " hari",
|
|
2103
|
+
formatStatusLabel: s =>
|
|
2104
|
+
({ confirmed: "Selesai", pending: "Proses", cancelled: "Batal" })[s] || s,
|
|
2105
|
+
formatAudienceLabel: v => v || "-",
|
|
2106
|
+
};
|
|
2107
|
+
|
|
2108
|
+
// 4. Render \u2014 selesai!
|
|
2109
|
+
export default function MyDashboard() {
|
|
2110
|
+
const state = useReusableDashboard({ ...dashConfig, labels });
|
|
2111
|
+
return (
|
|
2112
|
+
<ReusableDashboardView
|
|
2113
|
+
config={dashConfig.config}
|
|
2114
|
+
labels={labels}
|
|
2115
|
+
loading={state.loading}
|
|
2116
|
+
error={state.error}
|
|
2117
|
+
filters={state.filters}
|
|
2118
|
+
onFilterChange={state.updateFilter}
|
|
2119
|
+
onResetFilters={state.resetFilters}
|
|
2120
|
+
onRefresh={state.refresh}
|
|
2121
|
+
data={state.data}
|
|
2122
|
+
dateLocale={dashConfig.dateLocale}
|
|
2123
|
+
liveUpdateEnabled={state.liveUpdateEnabled}
|
|
2124
|
+
/>
|
|
2125
|
+
);
|
|
2126
|
+
}`), /* @__PURE__ */ React17.createElement("div", { className: "rdb-wizard-tip" }, "\u2705 ", /* @__PURE__ */ React17.createElement("strong", null, "Selesai!"), " Jalankan ", /* @__PURE__ */ React17.createElement("code", { className: "rdb-wizard-code-inline" }, "npm start"), " dan buka halaman dashboard. Wizard ini tidak akan muncul lagi setelah konfigurasi valid."), /* @__PURE__ */ React17.createElement("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 } }, /* @__PURE__ */ React17.createElement("button", { type: "button", className: "rdb-btn rdb-btn-secondary rdb-btn-sm", onClick: () => setStep(2) }, "\u2190 Kembali"), /* @__PURE__ */ React17.createElement("button", { type: "button", className: "rdb-btn rdb-btn-primary rdb-btn-sm", onClick: handleDismiss }, "Tutup & lanjutkan \u2713"))))));
|
|
2127
|
+
}
|
|
2128
|
+
SetupWizard.propTypes = {
|
|
2129
|
+
issues: PropTypes17.arrayOf(PropTypes17.string),
|
|
2130
|
+
onDismiss: PropTypes17.func,
|
|
2131
|
+
supabase: PropTypes17.object
|
|
2132
|
+
};
|
|
2133
|
+
|
|
2134
|
+
// src/presentation/ReusableDashboardView.jsx
|
|
1790
2135
|
function ReusableDashboardView({
|
|
1791
2136
|
config,
|
|
1792
2137
|
labels,
|
|
@@ -1798,9 +2143,48 @@ function ReusableDashboardView({
|
|
|
1798
2143
|
onRefresh,
|
|
1799
2144
|
data,
|
|
1800
2145
|
dateLocale,
|
|
1801
|
-
liveUpdateEnabled
|
|
2146
|
+
liveUpdateEnabled,
|
|
2147
|
+
supabase,
|
|
2148
|
+
dashboardConfig
|
|
1802
2149
|
}) {
|
|
1803
|
-
|
|
2150
|
+
var _a;
|
|
2151
|
+
const [wizardDismissed, setWizardDismissed] = React18.useState(false);
|
|
2152
|
+
const configIssues = React18.useMemo(() => {
|
|
2153
|
+
var _a2, _b, _c, _d, _e, _f, _g;
|
|
2154
|
+
if (wizardDismissed) return [];
|
|
2155
|
+
if (dashboardConfig) {
|
|
2156
|
+
const { issues: issues2 } = validateDashboardConfig(dashboardConfig);
|
|
2157
|
+
return issues2;
|
|
2158
|
+
}
|
|
2159
|
+
const issues = [];
|
|
2160
|
+
if (!config) issues.push("Prop 'config' (widgetConfig) belum diisi.");
|
|
2161
|
+
if (!((_b = (_a2 = config == null ? void 0 : config.widgets) == null ? void 0 : _a2.stats) == null ? void 0 : _b.length)) issues.push("widgetConfig.widgets.stats kosong. Tambahkan minimal 1 stat card.");
|
|
2162
|
+
if (!((_d = (_c = config == null ? void 0 : config.widgets) == null ? void 0 : _c.charts) == null ? void 0 : _d.length)) issues.push("widgetConfig.widgets.charts kosong. Tambahkan minimal 1 chart.");
|
|
2163
|
+
if (!((_g = (_f = (_e = config == null ? void 0 : config.widgets) == null ? void 0 : _e.table) == null ? void 0 : _f.columns) == null ? void 0 : _g.length)) issues.push("widgetConfig.widgets.table.columns kosong.");
|
|
2164
|
+
if (!labels) issues.push("Prop 'labels' belum diisi.");
|
|
2165
|
+
if (!(labels == null ? void 0 : labels.title)) issues.push("labels.title belum diisi.");
|
|
2166
|
+
if (!(labels == null ? void 0 : labels.formatStatusLabel)) issues.push("labels.formatStatusLabel (function) belum diisi.");
|
|
2167
|
+
return issues;
|
|
2168
|
+
}, [config, labels, dashboardConfig, wizardDismissed]);
|
|
2169
|
+
const showWizard = !wizardDismissed && configIssues.length > 0;
|
|
2170
|
+
if (!config || !config.widgets) {
|
|
2171
|
+
return /* @__PURE__ */ React18.createElement("div", { className: "rdb-view" }, /* @__PURE__ */ React18.createElement(
|
|
2172
|
+
SetupWizard,
|
|
2173
|
+
{
|
|
2174
|
+
issues: ["Prop 'config' (widgetConfig) belum diisi atau tidak valid."],
|
|
2175
|
+
onDismiss: () => setWizardDismissed(true),
|
|
2176
|
+
supabase
|
|
2177
|
+
}
|
|
2178
|
+
));
|
|
2179
|
+
}
|
|
2180
|
+
return /* @__PURE__ */ React18.createElement("div", { className: "rdb-view" }, showWizard && /* @__PURE__ */ React18.createElement(
|
|
2181
|
+
SetupWizard,
|
|
2182
|
+
{
|
|
2183
|
+
issues: configIssues,
|
|
2184
|
+
onDismiss: () => setWizardDismissed(true),
|
|
2185
|
+
supabase
|
|
2186
|
+
}
|
|
2187
|
+
), /* @__PURE__ */ React18.createElement("div", { className: "rdb-view-container" }, /* @__PURE__ */ React18.createElement("div", { className: "rdb-header-panel" }, /* @__PURE__ */ React18.createElement("div", { className: "rdb-header-top" }, /* @__PURE__ */ React18.createElement("div", { className: "rdb-header-title-group" }, /* @__PURE__ */ React18.createElement("span", { className: "rdb-h1" }, (labels == null ? void 0 : labels.title) ?? "Dashboard"), liveUpdateEnabled ? /* @__PURE__ */ React18.createElement(Badge, { status: "success" }, (labels == null ? void 0 : labels.liveUpdate) ?? "Live") : null), /* @__PURE__ */ React18.createElement(Button, { variant: "secondary", size: "sm", onClick: () => onRefresh(), title: (labels == null ? void 0 : labels.refresh) ?? "Refresh" }, /* @__PURE__ */ React18.createElement(Icon, { name: "RotateCcw", size: 16 }))), error ? /* @__PURE__ */ React18.createElement("div", { className: "rdb-error-banner" }, /* @__PURE__ */ React18.createElement("span", null, error), /* @__PURE__ */ React18.createElement(Button, { variant: "secondary", size: "sm", onClick: () => onRefresh() }, (labels == null ? void 0 : labels.retry) ?? "Retry")) : null, /* @__PURE__ */ React18.createElement(
|
|
1804
2188
|
FilterPanel,
|
|
1805
2189
|
{
|
|
1806
2190
|
filters,
|
|
@@ -1808,42 +2192,45 @@ function ReusableDashboardView({
|
|
|
1808
2192
|
onFilterChange,
|
|
1809
2193
|
onResetFilters
|
|
1810
2194
|
}
|
|
1811
|
-
)), /* @__PURE__ */
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2195
|
+
)), /* @__PURE__ */ React18.createElement("div", { className: "rdb-stats-grid" }, loading ? Array.from({ length: config.widgets.stats.length || 4 }).map((_, i) => /* @__PURE__ */ React18.createElement(SkeletonLoader, { key: i, style: { height: 112 } })) : config.widgets.stats.map((widget) => {
|
|
2196
|
+
var _a2;
|
|
2197
|
+
return /* @__PURE__ */ React18.createElement(
|
|
2198
|
+
StatCard,
|
|
2199
|
+
{
|
|
2200
|
+
key: widget.id,
|
|
2201
|
+
label: (labels == null ? void 0 : labels[widget.label]) ?? widget.label,
|
|
2202
|
+
value: ((_a2 = data == null ? void 0 : data.stats) == null ? void 0 : _a2[widget.valueKey]) ?? 0,
|
|
2203
|
+
icon: widget.icon,
|
|
2204
|
+
format: widget.format,
|
|
2205
|
+
accentColor: widget.accentColor
|
|
2206
|
+
}
|
|
2207
|
+
);
|
|
2208
|
+
})), /* @__PURE__ */ React18.createElement("div", { className: "rdb-charts-grid" }, config.widgets.charts.map((widget) => /* @__PURE__ */ React18.createElement(
|
|
1822
2209
|
ChartCard,
|
|
1823
2210
|
{
|
|
1824
2211
|
key: widget.id,
|
|
1825
2212
|
widget,
|
|
1826
|
-
labels,
|
|
2213
|
+
labels: labels ?? {},
|
|
1827
2214
|
loading,
|
|
1828
|
-
filters,
|
|
1829
|
-
chartData: data.charts
|
|
2215
|
+
filters: filters ?? {},
|
|
2216
|
+
chartData: (data == null ? void 0 : data.charts) ?? {}
|
|
1830
2217
|
}
|
|
1831
|
-
))), /* @__PURE__ */
|
|
2218
|
+
))), /* @__PURE__ */ React18.createElement("div", { className: "rdb-card", style: { padding: 16 } }, /* @__PURE__ */ React18.createElement(
|
|
1832
2219
|
"div",
|
|
1833
2220
|
{
|
|
1834
2221
|
style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 12 },
|
|
1835
2222
|
className: "rdb-h3"
|
|
1836
2223
|
},
|
|
1837
|
-
/* @__PURE__ */
|
|
1838
|
-
labels[config.widgets.table.label]
|
|
1839
|
-
), loading ? /* @__PURE__ */
|
|
2224
|
+
/* @__PURE__ */ React18.createElement(Icon, { name: config.widgets.table.icon ?? "Calendar", size: 18 }),
|
|
2225
|
+
(labels == null ? void 0 : labels[config.widgets.table.label]) ?? config.widgets.table.label
|
|
2226
|
+
), loading ? /* @__PURE__ */ React18.createElement(SkeletonLoader, { style: { height: 192 } }) : /* @__PURE__ */ React18.createElement(
|
|
1840
2227
|
DataTable,
|
|
1841
2228
|
{
|
|
1842
2229
|
columns: config.widgets.table.columns,
|
|
1843
|
-
data: data.table.recentBookings,
|
|
1844
|
-
labels,
|
|
1845
|
-
dateLocale,
|
|
1846
|
-
emptyLabel: labels[config.widgets.table.emptyLabel]
|
|
2230
|
+
data: ((_a = data == null ? void 0 : data.table) == null ? void 0 : _a.recentBookings) ?? [],
|
|
2231
|
+
labels: labels ?? {},
|
|
2232
|
+
dateLocale: dateLocale ?? "id-ID",
|
|
2233
|
+
emptyLabel: (labels == null ? void 0 : labels[config.widgets.table.emptyLabel]) ?? config.widgets.table.emptyLabel,
|
|
1847
2234
|
searchable: true,
|
|
1848
2235
|
sortable: true,
|
|
1849
2236
|
pageSize: 10
|
|
@@ -1851,17 +2238,21 @@ function ReusableDashboardView({
|
|
|
1851
2238
|
))));
|
|
1852
2239
|
}
|
|
1853
2240
|
ReusableDashboardView.propTypes = {
|
|
1854
|
-
config:
|
|
1855
|
-
labels:
|
|
1856
|
-
loading:
|
|
1857
|
-
error:
|
|
1858
|
-
filters:
|
|
1859
|
-
onFilterChange:
|
|
1860
|
-
onResetFilters:
|
|
1861
|
-
onRefresh:
|
|
1862
|
-
data:
|
|
1863
|
-
dateLocale:
|
|
1864
|
-
liveUpdateEnabled:
|
|
2241
|
+
config: PropTypes18.object.isRequired,
|
|
2242
|
+
labels: PropTypes18.object.isRequired,
|
|
2243
|
+
loading: PropTypes18.bool,
|
|
2244
|
+
error: PropTypes18.string,
|
|
2245
|
+
filters: PropTypes18.object,
|
|
2246
|
+
onFilterChange: PropTypes18.func.isRequired,
|
|
2247
|
+
onResetFilters: PropTypes18.func.isRequired,
|
|
2248
|
+
onRefresh: PropTypes18.func.isRequired,
|
|
2249
|
+
data: PropTypes18.object,
|
|
2250
|
+
dateLocale: PropTypes18.string,
|
|
2251
|
+
liveUpdateEnabled: PropTypes18.bool,
|
|
2252
|
+
/** Supabase client — aktifkan fitur baca tabel di wizard */
|
|
2253
|
+
supabase: PropTypes18.object,
|
|
2254
|
+
/** Hasil createDashboardConfig() — untuk validasi otomatis wizard */
|
|
2255
|
+
dashboardConfig: PropTypes18.object
|
|
1865
2256
|
};
|
|
1866
2257
|
|
|
1867
2258
|
// src/utils/labels.js
|
|
@@ -1996,6 +2387,7 @@ export {
|
|
|
1996
2387
|
Input,
|
|
1997
2388
|
ReusableDashboardView,
|
|
1998
2389
|
SearchBar,
|
|
2390
|
+
SetupWizard,
|
|
1999
2391
|
SidebarNavigation,
|
|
2000
2392
|
SkeletonLoader,
|
|
2001
2393
|
StatCard,
|
|
@@ -2007,6 +2399,7 @@ export {
|
|
|
2007
2399
|
buildDayBuckets,
|
|
2008
2400
|
cidikaWidgetConfig,
|
|
2009
2401
|
createCidikaSupabaseSource,
|
|
2402
|
+
createDashboardConfig,
|
|
2010
2403
|
createDashboardLabels,
|
|
2011
2404
|
createDefaultFilters,
|
|
2012
2405
|
createEmptyDashboardData,
|
|
@@ -2024,6 +2417,7 @@ export {
|
|
|
2024
2417
|
toNumber,
|
|
2025
2418
|
tokoSepatuWidgetConfig,
|
|
2026
2419
|
useRealtimeUpdate,
|
|
2027
|
-
useReusableDashboard
|
|
2420
|
+
useReusableDashboard,
|
|
2421
|
+
validateDashboardConfig
|
|
2028
2422
|
};
|
|
2029
2423
|
//# sourceMappingURL=index.js.map
|