@rozaqi02/reusable-dashboard 1.0.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.cjs ADDED
@@ -0,0 +1,2340 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/index.js
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ Badge: () => Badge,
33
+ Button: () => Button,
34
+ ChartCard: () => ChartCard,
35
+ ChartHeader: () => ChartHeader,
36
+ DashboardLayout: () => DashboardLayout,
37
+ DataTable: () => DataTable,
38
+ DateRangeFilter: () => DateRangeFilter,
39
+ FilterPanel: () => FilterPanel,
40
+ Icon: () => Icon,
41
+ Input: () => Input,
42
+ ReusableDashboardView: () => ReusableDashboardView,
43
+ SearchBar: () => SearchBar,
44
+ SidebarNavigation: () => SidebarNavigation,
45
+ SkeletonLoader: () => SkeletonLoader,
46
+ StatCard: () => StatCard,
47
+ TopbarHeader: () => TopbarHeader,
48
+ Typography: () => Typography,
49
+ adaptCidikaDashboardData: () => adaptCidikaDashboardData,
50
+ adaptDummyUmkmData: () => adaptDummyUmkmData,
51
+ adaptTokoSepatuData: () => adaptTokoSepatuData,
52
+ buildDayBuckets: () => buildDayBuckets,
53
+ cidikaWidgetConfig: () => cidikaWidgetConfig,
54
+ createCidikaSupabaseSource: () => createCidikaSupabaseSource,
55
+ createDashboardLabels: () => createDashboardLabels,
56
+ createDefaultFilters: () => createDefaultFilters,
57
+ createEmptyDashboardData: () => createEmptyDashboardData,
58
+ createEmptyDummyUmkmData: () => createEmptyDummyUmkmData,
59
+ createEmptyTokoSepatuData: () => createEmptyTokoSepatuData,
60
+ createTokoSepatuSupabaseSource: () => createTokoSepatuSupabaseSource,
61
+ dummyUmkmWidgetConfig: () => dummyUmkmWidgetConfig,
62
+ formatDate: () => formatDate,
63
+ formatIDR: () => formatIDR,
64
+ formatYYYYMMDD: () => formatYYYYMMDD,
65
+ resolveDateRange: () => resolveDateRange,
66
+ resolveIcon: () => resolveIcon,
67
+ shortId: () => shortId,
68
+ sortMapEntries: () => sortMapEntries,
69
+ toNumber: () => toNumber,
70
+ tokoSepatuWidgetConfig: () => tokoSepatuWidgetConfig,
71
+ useRealtimeUpdate: () => useRealtimeUpdate,
72
+ useReusableDashboard: () => useReusableDashboard
73
+ });
74
+ module.exports = __toCommonJS(index_exports);
75
+
76
+ // src/config/cidikaWidgetConfig.js
77
+ var cidikaWidgetConfig = {
78
+ id: "cidika.travel.dashboard",
79
+ defaultFilters: {
80
+ statusScope: "confirmed",
81
+ includePendingOverlay: false,
82
+ audience: "",
83
+ daysPreset: 30,
84
+ sortPkgBy: "bookings",
85
+ sortPkgDir: "desc"
86
+ },
87
+ widgets: {
88
+ stats: [
89
+ {
90
+ id: "bookingsConfirm",
91
+ label: "confirmedBookings",
92
+ icon: "TrendingUp",
93
+ valueKey: "bookingsConfirm",
94
+ format: "number"
95
+ },
96
+ {
97
+ id: "revenueConfirm",
98
+ label: "confirmedRevenue",
99
+ icon: "DollarSign",
100
+ valueKey: "revenueConfirm",
101
+ format: "currency"
102
+ },
103
+ {
104
+ id: "avgRevenue",
105
+ label: "avgRevenue",
106
+ icon: "Users",
107
+ valueKey: "avgRevenue",
108
+ format: "currency"
109
+ },
110
+ {
111
+ id: "conversionRate",
112
+ label: "conversionRate",
113
+ icon: "PieChart",
114
+ valueKey: "conversionRate",
115
+ format: "percent"
116
+ }
117
+ ],
118
+ charts: [
119
+ {
120
+ id: "dailyTrends",
121
+ type: "dailyArea",
122
+ label: "dailyTrends",
123
+ icon: "BarChart3"
124
+ },
125
+ {
126
+ id: "statusDistribution",
127
+ type: "statusPie",
128
+ label: "statusDistribution",
129
+ icon: "PieChart"
130
+ },
131
+ {
132
+ id: "audienceDistribution",
133
+ type: "audiencePie",
134
+ label: "audienceDistribution",
135
+ icon: "Users"
136
+ },
137
+ {
138
+ id: "topPackages",
139
+ type: "topPackagesBar",
140
+ label: "topPackages",
141
+ icon: "TrendingUp"
142
+ }
143
+ ],
144
+ table: {
145
+ id: "recentBookings",
146
+ label: "recentBookings",
147
+ icon: "Calendar",
148
+ emptyLabel: "noRecentBookings",
149
+ columns: [
150
+ { id: "date", label: "date", accessor: "createdAt", type: "date" },
151
+ { id: "customer", label: "customer", accessor: "customerName" },
152
+ { id: "package", label: "package", accessor: "packageName" },
153
+ { id: "audience", label: "audience", accessor: "audienceLabel" },
154
+ { id: "total", label: "total", accessor: "totalIDR", type: "currency" },
155
+ {
156
+ id: "status",
157
+ label: "status",
158
+ accessor: "statusLabel",
159
+ type: "statusBadge",
160
+ statusAccessor: "status"
161
+ }
162
+ ]
163
+ }
164
+ }
165
+ };
166
+
167
+ // src/config/dummyUmkmWidgetConfig.js
168
+ var dummyUmkmWidgetConfig = {
169
+ id: "dummy.umkm.dashboard",
170
+ defaultFilters: {
171
+ statusScope: "all",
172
+ includePendingOverlay: false,
173
+ audience: "",
174
+ daysPreset: 30,
175
+ sortPkgBy: "revenue",
176
+ sortPkgDir: "desc"
177
+ },
178
+ widgets: {
179
+ stats: [
180
+ {
181
+ id: "orders",
182
+ label: "confirmedBookings",
183
+ icon: "TrendingUp",
184
+ valueKey: "bookingsConfirm",
185
+ format: "number"
186
+ },
187
+ {
188
+ id: "revenue",
189
+ label: "confirmedRevenue",
190
+ icon: "DollarSign",
191
+ valueKey: "revenueConfirm",
192
+ format: "currency"
193
+ }
194
+ ],
195
+ charts: [
196
+ {
197
+ id: "revenueTrend",
198
+ type: "dailyArea",
199
+ label: "dailyTrends",
200
+ icon: "BarChart3"
201
+ },
202
+ {
203
+ id: "recentOrderStatus",
204
+ type: "statusPie",
205
+ label: "statusDistribution",
206
+ icon: "PieChart"
207
+ }
208
+ ],
209
+ table: {
210
+ id: "recentOrders",
211
+ label: "recentBookings",
212
+ icon: "Calendar",
213
+ emptyLabel: "noRecentBookings",
214
+ columns: [
215
+ { id: "date", label: "date", accessor: "createdAt", type: "date" },
216
+ { id: "customer", label: "customer", accessor: "customerName" },
217
+ { id: "total", label: "total", accessor: "totalIDR", type: "currency" }
218
+ ]
219
+ }
220
+ }
221
+ };
222
+
223
+ // src/config/tokoSepatuWidgetConfig.js
224
+ var tokoSepatuWidgetConfig = {
225
+ id: "toko.sepatu.dashboard",
226
+ defaultFilters: {
227
+ statusScope: "all",
228
+ includePendingOverlay: false,
229
+ audience: "",
230
+ daysPreset: 30,
231
+ sortPkgBy: "revenue",
232
+ sortPkgDir: "desc"
233
+ },
234
+ widgets: {
235
+ stats: [
236
+ {
237
+ id: "totalOrders",
238
+ label: "confirmedBookings",
239
+ icon: "TrendingUp",
240
+ valueKey: "bookingsConfirm",
241
+ format: "number"
242
+ },
243
+ {
244
+ id: "totalRevenue",
245
+ label: "confirmedRevenue",
246
+ icon: "DollarSign",
247
+ valueKey: "revenueConfirm",
248
+ format: "currency"
249
+ },
250
+ {
251
+ id: "avgOrderValue",
252
+ label: "avgRevenue",
253
+ icon: "BarChart3",
254
+ valueKey: "avgRevenue",
255
+ format: "currency"
256
+ },
257
+ {
258
+ id: "totalProducts",
259
+ label: "totalProducts",
260
+ icon: "PieChart",
261
+ valueKey: "packages",
262
+ format: "number"
263
+ }
264
+ ],
265
+ charts: [
266
+ {
267
+ id: "dailyOrderTrends",
268
+ type: "dailyArea",
269
+ label: "dailyTrends",
270
+ icon: "BarChart3"
271
+ },
272
+ {
273
+ id: "orderStatusDistribution",
274
+ type: "statusPie",
275
+ label: "statusDistribution",
276
+ icon: "PieChart"
277
+ },
278
+ {
279
+ id: "topProducts",
280
+ type: "topPackagesBar",
281
+ label: "topPackages",
282
+ icon: "TrendingUp"
283
+ }
284
+ ],
285
+ table: {
286
+ id: "recentOrders",
287
+ label: "recentBookings",
288
+ icon: "Calendar",
289
+ emptyLabel: "noRecentBookings",
290
+ columns: [
291
+ { id: "date", label: "date", accessor: "createdAt", type: "date" },
292
+ { id: "customer", label: "customer", accessor: "customerName" },
293
+ { id: "product", label: "package", accessor: "packageName" },
294
+ { id: "total", label: "total", accessor: "totalIDR", type: "currency" },
295
+ {
296
+ id: "status",
297
+ label: "status",
298
+ accessor: "statusLabel",
299
+ type: "statusBadge",
300
+ statusAccessor: "status"
301
+ }
302
+ ]
303
+ }
304
+ }
305
+ };
306
+
307
+ // src/utils/formatters.js
308
+ function formatIDR(value) {
309
+ try {
310
+ return (Number(value) || 0).toLocaleString("id-ID");
311
+ } catch (_error) {
312
+ return String(value ?? 0);
313
+ }
314
+ }
315
+ function shortId(value, length = 8) {
316
+ if (!value) return "-";
317
+ return String(value).slice(0, length);
318
+ }
319
+ function formatDate(value, locale) {
320
+ if (!value) return "-";
321
+ const date = new Date(value);
322
+ if (Number.isNaN(date.getTime())) return "-";
323
+ return date.toLocaleDateString(locale);
324
+ }
325
+
326
+ // src/utils/adapterHelpers.js
327
+ function toNumber(value) {
328
+ const parsed = Number(value);
329
+ return Number.isFinite(parsed) ? parsed : 0;
330
+ }
331
+ function buildDayBuckets(daysWindow, fromISO, dateLocale) {
332
+ const start = new Date(fromISO);
333
+ return Array.from({ length: daysWindow }).map((_, index) => {
334
+ const day = new Date(start);
335
+ day.setDate(start.getDate() + index);
336
+ return {
337
+ dateKey: day.toISOString().slice(0, 10),
338
+ label: day.toLocaleDateString(dateLocale, {
339
+ day: "2-digit",
340
+ month: "short"
341
+ }),
342
+ count: 0,
343
+ revenue: 0,
344
+ pendingCount: 0
345
+ };
346
+ });
347
+ }
348
+ function sortMapEntries(map, direction) {
349
+ return Array.from(map.entries()).sort(
350
+ (a, b) => direction === "asc" ? a[1] - b[1] : b[1] - a[1]
351
+ );
352
+ }
353
+
354
+ // src/data-adapter/cidikaDashboardAdapter.js
355
+ function createPackageTitleResolver(packageLocales, languageCode) {
356
+ const byPackage = /* @__PURE__ */ new Map();
357
+ packageLocales.forEach((row) => {
358
+ if (!row.package_id) return;
359
+ const current = byPackage.get(row.package_id) || {};
360
+ byPackage.set(row.package_id, {
361
+ ...current,
362
+ [row.lang]: row.title
363
+ });
364
+ });
365
+ return (packageId) => {
366
+ if (!packageId) return "-";
367
+ const labels = byPackage.get(packageId);
368
+ if (!labels) return shortId(packageId, 6);
369
+ return labels[languageCode] || labels.en || labels.id || Object.values(labels).find(Boolean) || shortId(packageId, 6);
370
+ };
371
+ }
372
+ function createEmptyDashboardData() {
373
+ return {
374
+ stats: {
375
+ bookingsConfirm: 0,
376
+ bookingsPending: 0,
377
+ revenueConfirm: 0,
378
+ packages: 0,
379
+ sections: 0,
380
+ avgRevenue: 0,
381
+ conversionRate: 0
382
+ },
383
+ charts: {
384
+ dailyTrends: [],
385
+ statusDistribution: [],
386
+ audienceDistribution: [],
387
+ topPackages: []
388
+ },
389
+ table: {
390
+ recentBookings: []
391
+ }
392
+ };
393
+ }
394
+ function adaptCidikaDashboardData({
395
+ raw,
396
+ filters,
397
+ range,
398
+ dateLocale,
399
+ languageCode,
400
+ labels
401
+ }) {
402
+ var _a, _b;
403
+ if (!raw) return createEmptyDashboardData();
404
+ const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
405
+ const dayLookup = new Map(
406
+ dailyBuckets.map((bucket) => [bucket.dateKey, bucket])
407
+ );
408
+ const statusMap = /* @__PURE__ */ new Map();
409
+ const audienceMap = /* @__PURE__ */ new Map();
410
+ const packageCountMap = /* @__PURE__ */ new Map();
411
+ const packageRevenueMap = /* @__PURE__ */ new Map();
412
+ const getPackageTitle = createPackageTitleResolver(
413
+ raw.packageLocales || [],
414
+ languageCode
415
+ );
416
+ let bookingsConfirm = 0;
417
+ let bookingsPending = 0;
418
+ let revenueConfirm = 0;
419
+ (raw.bookings || []).forEach((row) => {
420
+ const dayKey = String(row.created_at || "").slice(0, 10);
421
+ const status = String(row.status || "pending").toLowerCase();
422
+ const total = toNumber(row.total_idr);
423
+ const audience = row.audience || "unknown";
424
+ statusMap.set(status, (statusMap.get(status) || 0) + 1);
425
+ audienceMap.set(audience, (audienceMap.get(audience) || 0) + 1);
426
+ const bucket = dayLookup.get(dayKey);
427
+ if (bucket) {
428
+ if (status === "confirmed") {
429
+ bucket.count += 1;
430
+ bucket.revenue += total;
431
+ }
432
+ if (status === "pending") {
433
+ bucket.pendingCount += 1;
434
+ }
435
+ }
436
+ if (status === "confirmed") {
437
+ bookingsConfirm += 1;
438
+ revenueConfirm += total;
439
+ packageCountMap.set(
440
+ row.package_id,
441
+ (packageCountMap.get(row.package_id) || 0) + 1
442
+ );
443
+ packageRevenueMap.set(
444
+ row.package_id,
445
+ (packageRevenueMap.get(row.package_id) || 0) + total
446
+ );
447
+ } else if (status === "pending") {
448
+ bookingsPending += 1;
449
+ }
450
+ });
451
+ const statusDistribution = sortMapEntries(statusMap, "desc").map(
452
+ ([status, count]) => ({
453
+ status,
454
+ label: labels.formatStatusLabel(status),
455
+ count
456
+ })
457
+ );
458
+ const audienceDistribution = sortMapEntries(audienceMap, "desc").map(
459
+ ([audience, count]) => ({
460
+ audience,
461
+ label: labels.formatAudienceLabel(audience),
462
+ count
463
+ })
464
+ );
465
+ const metricMap = filters.sortPkgBy === "revenue" ? packageRevenueMap : packageCountMap;
466
+ const topPackages = sortMapEntries(metricMap, filters.sortPkgDir).slice(0, 5).map(([packageId, value]) => ({
467
+ packageId,
468
+ name: getPackageTitle(packageId),
469
+ value: toNumber(value)
470
+ }));
471
+ const recentBookings = (raw.recent || []).map((row) => {
472
+ const status = String(row.status || "pending").toLowerCase();
473
+ return {
474
+ id: row.id,
475
+ createdAt: row.created_at,
476
+ customerName: row.customer_name || "-",
477
+ packageName: getPackageTitle(row.package_id),
478
+ audienceLabel: labels.formatAudienceLabel(row.audience),
479
+ totalIDR: toNumber(row.total_idr),
480
+ status,
481
+ statusLabel: labels.formatStatusLabel(status)
482
+ };
483
+ });
484
+ const avgRevenue = bookingsConfirm > 0 ? Math.round(revenueConfirm / bookingsConfirm) : 0;
485
+ const conversionRate = bookingsConfirm + bookingsPending > 0 ? Math.round(bookingsConfirm / (bookingsConfirm + bookingsPending) * 100) : 0;
486
+ return {
487
+ stats: {
488
+ bookingsConfirm,
489
+ bookingsPending,
490
+ revenueConfirm,
491
+ packages: toNumber((_a = raw.staticCounts) == null ? void 0 : _a.packages),
492
+ sections: toNumber((_b = raw.staticCounts) == null ? void 0 : _b.sections),
493
+ avgRevenue,
494
+ conversionRate
495
+ },
496
+ charts: {
497
+ dailyTrends: dailyBuckets,
498
+ statusDistribution,
499
+ audienceDistribution,
500
+ topPackages
501
+ },
502
+ table: {
503
+ recentBookings
504
+ }
505
+ };
506
+ }
507
+
508
+ // src/data-adapter/dummyUmkmDashboardAdapter.js
509
+ function createEmptyDummyUmkmData() {
510
+ return {
511
+ stats: {
512
+ bookingsConfirm: 0,
513
+ revenueConfirm: 0,
514
+ totalProducts: 0,
515
+ totalCustomers: 0
516
+ },
517
+ charts: {
518
+ dailyTrends: [],
519
+ statusDistribution: [],
520
+ audienceDistribution: [],
521
+ topPackages: []
522
+ },
523
+ table: {
524
+ recentBookings: []
525
+ }
526
+ };
527
+ }
528
+ function adaptDummyUmkmData({ raw, filters, range, dateLocale, labels }) {
529
+ var _a, _b;
530
+ if (!raw) return createEmptyDummyUmkmData();
531
+ const orders = raw.bookings || [];
532
+ const recent = raw.recent || [];
533
+ const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
534
+ const dayLookup = new Map(dailyBuckets.map((b) => [b.dateKey, b]));
535
+ const statusMap = /* @__PURE__ */ new Map();
536
+ let totalOrders = 0;
537
+ let totalRevenue = 0;
538
+ orders.forEach((row) => {
539
+ const dayKey = String(row.created_at || "").slice(0, 10);
540
+ const status = String(row.status || "pending").toLowerCase();
541
+ const amount = toNumber(row.total_amount);
542
+ statusMap.set(status, (statusMap.get(status) || 0) + 1);
543
+ const bucket = dayLookup.get(dayKey);
544
+ if (bucket) {
545
+ if (status === "confirmed") {
546
+ bucket.count += 1;
547
+ bucket.revenue += amount;
548
+ }
549
+ if (status === "pending") {
550
+ bucket.pendingCount += 1;
551
+ }
552
+ }
553
+ if (status === "confirmed") {
554
+ totalOrders += 1;
555
+ totalRevenue += amount;
556
+ }
557
+ });
558
+ const statusDistribution = Array.from(statusMap.entries()).sort((a, b) => b[1] - a[1]).map(([status, count]) => ({
559
+ status,
560
+ label: (labels == null ? void 0 : labels.formatStatusLabel) ? labels.formatStatusLabel(status) : status,
561
+ count
562
+ }));
563
+ const recentBookings = recent.map((row) => {
564
+ const status = String(row.status || "pending").toLowerCase();
565
+ return {
566
+ id: row.id,
567
+ createdAt: row.created_at,
568
+ customerName: row.customer_name || "-",
569
+ packageName: row.product_name || row.product_id || "-",
570
+ totalIDR: toNumber(row.total_amount),
571
+ status,
572
+ statusLabel: (labels == null ? void 0 : labels.formatStatusLabel) ? labels.formatStatusLabel(status) : status
573
+ };
574
+ });
575
+ return {
576
+ stats: {
577
+ bookingsConfirm: totalOrders,
578
+ revenueConfirm: totalRevenue,
579
+ totalProducts: toNumber((_a = raw.staticCounts) == null ? void 0 : _a.packages),
580
+ totalCustomers: toNumber((_b = raw.staticCounts) == null ? void 0 : _b.sections)
581
+ },
582
+ charts: {
583
+ dailyTrends: dailyBuckets,
584
+ statusDistribution,
585
+ audienceDistribution: [],
586
+ topPackages: []
587
+ },
588
+ table: {
589
+ recentBookings
590
+ }
591
+ };
592
+ }
593
+
594
+ // src/data-adapter/tokoSepatuDashboardAdapter.js
595
+ function createEmptyTokoSepatuData() {
596
+ return {
597
+ stats: {
598
+ bookingsConfirm: 0,
599
+ bookingsPending: 0,
600
+ revenueConfirm: 0,
601
+ packages: 0,
602
+ sections: 0,
603
+ avgRevenue: 0,
604
+ conversionRate: 0
605
+ },
606
+ charts: {
607
+ dailyTrends: [],
608
+ statusDistribution: [],
609
+ audienceDistribution: [],
610
+ topPackages: []
611
+ },
612
+ table: {
613
+ recentBookings: []
614
+ }
615
+ };
616
+ }
617
+ function adaptTokoSepatuData({ raw, filters, range, dateLocale, labels }) {
618
+ var _a, _b;
619
+ if (!raw) return createEmptyTokoSepatuData();
620
+ const orders = raw.bookings || [];
621
+ const recent = raw.recent || [];
622
+ const dailyBuckets = buildDayBuckets(range.daysWindow, range.fromISO, dateLocale);
623
+ const dayLookup = new Map(dailyBuckets.map((b) => [b.dateKey, b]));
624
+ const statusMap = /* @__PURE__ */ new Map();
625
+ const productCountMap = /* @__PURE__ */ new Map();
626
+ const productRevenueMap = /* @__PURE__ */ new Map();
627
+ let ordersConfirm = 0;
628
+ let ordersPending = 0;
629
+ let revenueConfirm = 0;
630
+ const productNameMap = /* @__PURE__ */ new Map();
631
+ (raw.products || []).forEach((p) => {
632
+ productNameMap.set(p.id, p.name);
633
+ });
634
+ orders.forEach((row) => {
635
+ const dayKey = String(row.created_at || "").slice(0, 10);
636
+ const status = String(row.status || "pending").toLowerCase();
637
+ const total = toNumber(row.total_amount);
638
+ statusMap.set(status, (statusMap.get(status) || 0) + 1);
639
+ const bucket = dayLookup.get(dayKey);
640
+ if (bucket) {
641
+ if (status === "confirmed") {
642
+ bucket.count += 1;
643
+ bucket.revenue += total;
644
+ }
645
+ if (status === "pending") {
646
+ bucket.pendingCount += 1;
647
+ }
648
+ }
649
+ if (status === "confirmed") {
650
+ ordersConfirm += 1;
651
+ revenueConfirm += total;
652
+ const items = row.order_items || [];
653
+ items.forEach((item) => {
654
+ const pid = item.product_id;
655
+ productCountMap.set(pid, (productCountMap.get(pid) || 0) + toNumber(item.qty));
656
+ productRevenueMap.set(pid, (productRevenueMap.get(pid) || 0) + toNumber(item.subtotal));
657
+ });
658
+ } else if (status === "pending") {
659
+ ordersPending += 1;
660
+ }
661
+ });
662
+ const statusDistribution = sortMapEntries(statusMap, "desc").map(
663
+ ([status, count]) => ({
664
+ status,
665
+ label: (labels == null ? void 0 : labels.formatStatusLabel) ? labels.formatStatusLabel(status) : status,
666
+ count
667
+ })
668
+ );
669
+ const metricMap = filters.sortPkgBy === "revenue" ? productRevenueMap : productCountMap;
670
+ const topPackages = sortMapEntries(metricMap, filters.sortPkgDir || "desc").slice(0, 5).map(([productId, value]) => ({
671
+ packageId: productId,
672
+ name: productNameMap.get(productId) || shortId(productId, 6),
673
+ value: toNumber(value)
674
+ }));
675
+ const recentBookings = recent.map((row) => {
676
+ const status = String(row.status || "pending").toLowerCase();
677
+ const productName = row.product_name || (row.order_items && row.order_items[0] ? productNameMap.get(row.order_items[0].product_id) : null) || "-";
678
+ return {
679
+ id: row.id,
680
+ createdAt: row.created_at,
681
+ customerName: row.customer_name || "-",
682
+ packageName: productName,
683
+ totalIDR: toNumber(row.total_amount),
684
+ status,
685
+ statusLabel: (labels == null ? void 0 : labels.formatStatusLabel) ? labels.formatStatusLabel(status) : status
686
+ };
687
+ });
688
+ const avgRevenue = ordersConfirm > 0 ? Math.round(revenueConfirm / ordersConfirm) : 0;
689
+ const conversionRate = ordersConfirm + ordersPending > 0 ? Math.round(ordersConfirm / (ordersConfirm + ordersPending) * 100) : 0;
690
+ return {
691
+ stats: {
692
+ bookingsConfirm: ordersConfirm,
693
+ bookingsPending: ordersPending,
694
+ revenueConfirm,
695
+ packages: toNumber((_a = raw.staticCounts) == null ? void 0 : _a.products),
696
+ sections: toNumber((_b = raw.staticCounts) == null ? void 0 : _b.customers),
697
+ avgRevenue,
698
+ conversionRate
699
+ },
700
+ charts: {
701
+ dailyTrends: dailyBuckets,
702
+ statusDistribution,
703
+ audienceDistribution: [],
704
+ topPackages
705
+ },
706
+ table: {
707
+ recentBookings
708
+ }
709
+ };
710
+ }
711
+
712
+ // src/data-source/cidikaSupabaseSource.js
713
+ function ensureNoError(response, message) {
714
+ if (response == null ? void 0 : response.error) {
715
+ const err = new Error(message);
716
+ err.cause = response.error;
717
+ throw err;
718
+ }
719
+ }
720
+ function createCidikaSupabaseSource(supabase) {
721
+ return {
722
+ async fetchDashboardSnapshot({
723
+ fromISO,
724
+ toISO,
725
+ audience,
726
+ statusScope,
727
+ languageCode
728
+ }) {
729
+ const bookingsQuery = supabase.from("bookings").select(
730
+ "id, created_at, total_idr, status, package_id, audience, customer_name"
731
+ ).gte("created_at", fromISO).lte("created_at", toISO).order("created_at", { ascending: true });
732
+ if (audience) bookingsQuery.eq("audience", audience);
733
+ const recentQuery = supabase.from("bookings").select(
734
+ "id, created_at, customer_name, total_idr, status, package_id, audience"
735
+ ).gte("created_at", fromISO).lte("created_at", toISO).order("created_at", { ascending: false }).limit(10);
736
+ if (audience) recentQuery.eq("audience", audience);
737
+ if (statusScope && statusScope !== "all") {
738
+ recentQuery.eq("status", statusScope);
739
+ }
740
+ const [bookingsRes, recentRes, packagesRes, sectionsRes] = await Promise.all(
741
+ [
742
+ bookingsQuery,
743
+ recentQuery,
744
+ supabase.from("packages").select("*", { count: "exact", head: true }),
745
+ supabase.from("page_sections").select("*", { count: "exact", head: true })
746
+ ]
747
+ );
748
+ ensureNoError(bookingsRes, "Failed to load booking rows.");
749
+ ensureNoError(recentRes, "Failed to load recent rows.");
750
+ const bookings = bookingsRes.data || [];
751
+ const recent = recentRes.data || [];
752
+ const packageIds = Array.from(
753
+ new Set(
754
+ [...bookings, ...recent].map((row) => row.package_id).filter((value) => Boolean(value))
755
+ )
756
+ );
757
+ let packageLocales = [];
758
+ if (packageIds.length > 0) {
759
+ const localeRes = await supabase.from("package_locales").select("package_id,title,lang").in("package_id", packageIds).in("lang", [languageCode, "en", "id"]);
760
+ if (!localeRes.error) {
761
+ packageLocales = localeRes.data || [];
762
+ }
763
+ }
764
+ return {
765
+ bookings,
766
+ recent,
767
+ packageLocales,
768
+ staticCounts: {
769
+ packages: packagesRes.error ? 0 : packagesRes.count || 0,
770
+ sections: sectionsRes.error ? 0 : sectionsRes.count || 0
771
+ }
772
+ };
773
+ },
774
+ subscribeLiveUpdate(onEvent) {
775
+ const channel = supabase.channel("reusable-dashboard-live").on(
776
+ "postgres_changes",
777
+ { event: "*", schema: "public", table: "bookings" },
778
+ onEvent
779
+ ).on(
780
+ "postgres_changes",
781
+ { event: "*", schema: "public", table: "packages" },
782
+ onEvent
783
+ ).on(
784
+ "postgres_changes",
785
+ { event: "*", schema: "public", table: "page_sections" },
786
+ onEvent
787
+ ).on(
788
+ "postgres_changes",
789
+ { event: "*", schema: "public", table: "package_locales" },
790
+ onEvent
791
+ ).subscribe();
792
+ return () => {
793
+ supabase.removeChannel(channel);
794
+ };
795
+ }
796
+ };
797
+ }
798
+
799
+ // src/data-source/tokoSepatuSupabaseSource.js
800
+ function ensureNoError2(response, message) {
801
+ if (response == null ? void 0 : response.error) {
802
+ const err = new Error(message);
803
+ err.cause = response.error;
804
+ throw err;
805
+ }
806
+ }
807
+ function createTokoSepatuSupabaseSource(supabase) {
808
+ return {
809
+ /**
810
+ * Mengambil snapshot data dashboard dari Supabase.
811
+ */
812
+ async fetchDashboardSnapshot({
813
+ fromISO,
814
+ toISO,
815
+ audience,
816
+ statusScope
817
+ }) {
818
+ const ordersQuery = supabase.from("orders").select(
819
+ "id, created_at, total_amount, status, customer_id, order_items(product_id, qty, price_idr, subtotal)"
820
+ ).gte("created_at", fromISO).lte("created_at", toISO).order("created_at", { ascending: true });
821
+ const recentQuery = supabase.from("orders").select(
822
+ "id, created_at, total_amount, status, customers(name), order_items(product_id, qty, products(name))"
823
+ ).gte("created_at", fromISO).lte("created_at", toISO).order("created_at", { ascending: false }).limit(10);
824
+ if (statusScope && statusScope !== "all") {
825
+ recentQuery.eq("status", statusScope);
826
+ }
827
+ const [ordersRes, recentRes, productsRes, customersRes] = await Promise.all([
828
+ ordersQuery,
829
+ recentQuery,
830
+ supabase.from("products").select("id, name, brand, category, price_idr"),
831
+ supabase.from("customers").select("*", { count: "exact", head: true })
832
+ ]);
833
+ ensureNoError2(ordersRes, "Failed to load orders.");
834
+ ensureNoError2(recentRes, "Failed to load recent orders.");
835
+ const orders = ordersRes.data || [];
836
+ const recentRaw = recentRes.data || [];
837
+ const products = productsRes.data || [];
838
+ const recent = recentRaw.map((row) => {
839
+ var _a, _b, _c, _d;
840
+ return {
841
+ id: row.id,
842
+ created_at: row.created_at,
843
+ total_amount: row.total_amount,
844
+ status: row.status,
845
+ customer_name: ((_a = row.customers) == null ? void 0 : _a.name) || "-",
846
+ product_name: ((_d = (_c = (_b = row.order_items) == null ? void 0 : _b[0]) == null ? void 0 : _c.products) == null ? void 0 : _d.name) || "-",
847
+ order_items: row.order_items
848
+ };
849
+ });
850
+ return {
851
+ bookings: orders,
852
+ recent,
853
+ products,
854
+ packageLocales: [],
855
+ staticCounts: {
856
+ products: products.length,
857
+ customers: customersRes.error ? 0 : customersRes.count || 0
858
+ }
859
+ };
860
+ },
861
+ /**
862
+ * Subscribe ke perubahan realtime pada tabel orders dan products.
863
+ */
864
+ subscribeLiveUpdate(onEvent) {
865
+ const channel = supabase.channel("toko-sepatu-dashboard-live").on(
866
+ "postgres_changes",
867
+ { event: "*", schema: "public", table: "orders" },
868
+ onEvent
869
+ ).on(
870
+ "postgres_changes",
871
+ { event: "*", schema: "public", table: "products" },
872
+ onEvent
873
+ ).on(
874
+ "postgres_changes",
875
+ { event: "*", schema: "public", table: "order_items" },
876
+ onEvent
877
+ ).subscribe();
878
+ return () => {
879
+ supabase.removeChannel(channel);
880
+ };
881
+ }
882
+ };
883
+ }
884
+
885
+ // src/hooks/useReusableDashboard.js
886
+ var import_react2 = require("react");
887
+
888
+ // src/utils/dateRange.js
889
+ function startOfDayISO(date) {
890
+ const value = new Date(date);
891
+ value.setHours(0, 0, 0, 0);
892
+ return value.toISOString();
893
+ }
894
+ function endOfDayISO(date) {
895
+ const value = new Date(date);
896
+ value.setHours(23, 59, 59, 999);
897
+ return value.toISOString();
898
+ }
899
+ function formatYYYYMMDD(date) {
900
+ const value = new Date(date);
901
+ const year = value.getFullYear();
902
+ const month = String(value.getMonth() + 1).padStart(2, "0");
903
+ const day = String(value.getDate()).padStart(2, "0");
904
+ return `${year}-${month}-${day}`;
905
+ }
906
+ function createDefaultFilters(base = {}) {
907
+ const fromDate = /* @__PURE__ */ new Date();
908
+ fromDate.setDate(fromDate.getDate() - 29);
909
+ return {
910
+ statusScope: "confirmed",
911
+ includePendingOverlay: false,
912
+ audience: "",
913
+ daysPreset: 30,
914
+ dateFrom: formatYYYYMMDD(fromDate),
915
+ dateTo: formatYYYYMMDD(/* @__PURE__ */ new Date()),
916
+ sortPkgBy: "bookings",
917
+ sortPkgDir: "desc",
918
+ ...base
919
+ };
920
+ }
921
+ var MIN_YEAR = 2e3;
922
+ var MAX_DAYS_WINDOW = 365 * 5;
923
+ function isReasonableDate(date) {
924
+ if (!date || Number.isNaN(date.getTime())) return false;
925
+ const year = date.getFullYear();
926
+ return year >= MIN_YEAR && year <= (/* @__PURE__ */ new Date()).getFullYear() + 1;
927
+ }
928
+ function resolveDateRange({ daysPreset, dateFrom, dateTo }) {
929
+ if (Number(daysPreset) > 0) {
930
+ const fromDate = /* @__PURE__ */ new Date();
931
+ fromDate.setDate(fromDate.getDate() - (Number(daysPreset) - 1));
932
+ const toDate = /* @__PURE__ */ new Date();
933
+ return {
934
+ fromISO: startOfDayISO(fromDate),
935
+ toISO: endOfDayISO(toDate),
936
+ daysWindow: Number(daysPreset)
937
+ };
938
+ }
939
+ const fallbackFrom = new Date(Date.now() - 29 * 24 * 60 * 60 * 1e3);
940
+ const fallbackTo = /* @__PURE__ */ new Date();
941
+ let from = dateFrom ? new Date(dateFrom) : fallbackFrom;
942
+ let to = dateTo ? new Date(dateTo) : fallbackTo;
943
+ if (!isReasonableDate(from)) from = fallbackFrom;
944
+ if (!isReasonableDate(to)) to = fallbackTo;
945
+ if (from.getTime() > to.getTime()) {
946
+ const swap = from;
947
+ from = to;
948
+ to = swap;
949
+ }
950
+ const fromISO = startOfDayISO(from);
951
+ const toISO = endOfDayISO(to);
952
+ const diffMs = new Date(toISO).getTime() - new Date(fromISO).getTime();
953
+ const daysWindow = Math.min(
954
+ MAX_DAYS_WINDOW,
955
+ Math.max(1, Math.round(diffMs / 864e5) + 1)
956
+ );
957
+ return { fromISO, toISO, daysWindow };
958
+ }
959
+
960
+ // src/hooks/useRealtimeUpdate.js
961
+ var import_react = require("react");
962
+ function useRealtimeUpdate({ dataSource, onUpdate, debounceMs = 350 }) {
963
+ const [liveUpdateEnabled, setLiveUpdateEnabled] = (0, import_react.useState)(false);
964
+ (0, import_react.useEffect)(() => {
965
+ if (!(dataSource == null ? void 0 : dataSource.subscribeLiveUpdate)) return void 0;
966
+ let debounceId;
967
+ setLiveUpdateEnabled(true);
968
+ const unsubscribe = dataSource.subscribeLiveUpdate(() => {
969
+ clearTimeout(debounceId);
970
+ debounceId = setTimeout(() => {
971
+ onUpdate == null ? void 0 : onUpdate();
972
+ }, debounceMs);
973
+ });
974
+ return () => {
975
+ clearTimeout(debounceId);
976
+ setLiveUpdateEnabled(false);
977
+ unsubscribe == null ? void 0 : unsubscribe();
978
+ };
979
+ }, [dataSource, debounceMs]);
980
+ return { liveUpdateEnabled };
981
+ }
982
+
983
+ // src/hooks/useReusableDashboard.js
984
+ function useReusableDashboard({
985
+ config,
986
+ dataSource,
987
+ adapter,
988
+ createEmptyState,
989
+ languageCode,
990
+ dateLocale,
991
+ labels
992
+ }) {
993
+ const [filters, setFilters] = (0, import_react2.useState)(
994
+ () => createDefaultFilters(config == null ? void 0 : config.defaultFilters)
995
+ );
996
+ const [loading, setLoading] = (0, import_react2.useState)(true);
997
+ const [error, setError] = (0, import_react2.useState)("");
998
+ const [lastUpdatedAt, setLastUpdatedAt] = (0, import_react2.useState)(null);
999
+ const [data, setData] = (0, import_react2.useState)(() => createEmptyState());
1000
+ const range = (0, import_react2.useMemo)(
1001
+ () => resolveDateRange({
1002
+ daysPreset: filters.daysPreset,
1003
+ dateFrom: filters.dateFrom,
1004
+ dateTo: filters.dateTo
1005
+ }),
1006
+ [filters.daysPreset, filters.dateFrom, filters.dateTo]
1007
+ );
1008
+ const refresh = (0, import_react2.useCallback)(
1009
+ async ({ silent = false } = {}) => {
1010
+ if (!(dataSource == null ? void 0 : dataSource.fetchDashboardSnapshot)) return;
1011
+ if (!silent) {
1012
+ setLoading(true);
1013
+ setError("");
1014
+ }
1015
+ try {
1016
+ const raw = await dataSource.fetchDashboardSnapshot({
1017
+ fromISO: range.fromISO,
1018
+ toISO: range.toISO,
1019
+ audience: filters.audience,
1020
+ statusScope: filters.statusScope,
1021
+ languageCode
1022
+ });
1023
+ const adapted = adapter({
1024
+ raw,
1025
+ filters,
1026
+ range,
1027
+ dateLocale,
1028
+ languageCode,
1029
+ labels
1030
+ });
1031
+ setData(adapted);
1032
+ setLastUpdatedAt(/* @__PURE__ */ new Date());
1033
+ if (!silent) setError("");
1034
+ } catch (loadError) {
1035
+ console.error(loadError);
1036
+ if (!silent) {
1037
+ setError(labels.loadFailed);
1038
+ setData(createEmptyState());
1039
+ }
1040
+ } finally {
1041
+ if (!silent) setLoading(false);
1042
+ }
1043
+ },
1044
+ [
1045
+ dataSource,
1046
+ range,
1047
+ filters,
1048
+ languageCode,
1049
+ adapter,
1050
+ dateLocale,
1051
+ labels,
1052
+ createEmptyState
1053
+ ]
1054
+ );
1055
+ (0, import_react2.useEffect)(() => {
1056
+ refresh();
1057
+ }, [refresh]);
1058
+ const { liveUpdateEnabled } = useRealtimeUpdate({
1059
+ dataSource,
1060
+ onUpdate: () => refresh({ silent: true })
1061
+ });
1062
+ const updateFilter = (0, import_react2.useCallback)((field, value) => {
1063
+ setFilters((current) => ({
1064
+ ...current,
1065
+ [field]: value
1066
+ }));
1067
+ }, []);
1068
+ const resetFilters = (0, import_react2.useCallback)(() => {
1069
+ setFilters(createDefaultFilters(config == null ? void 0 : config.defaultFilters));
1070
+ }, [config == null ? void 0 : config.defaultFilters]);
1071
+ return {
1072
+ filters,
1073
+ updateFilter,
1074
+ resetFilters,
1075
+ loading,
1076
+ error,
1077
+ data,
1078
+ refresh,
1079
+ range,
1080
+ liveUpdateEnabled,
1081
+ lastUpdatedAt
1082
+ };
1083
+ }
1084
+
1085
+ // src/presentation/atoms/Button.jsx
1086
+ var import_react3 = __toESM(require("react"), 1);
1087
+ var import_prop_types = __toESM(require("prop-types"), 1);
1088
+ function Button({
1089
+ variant = "primary",
1090
+ size = "md",
1091
+ disabled = false,
1092
+ className = "",
1093
+ onClick,
1094
+ children,
1095
+ ...rest
1096
+ }) {
1097
+ const base = "inline-flex items-center justify-center font-medium rounded-xl transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
1098
+ const variants = {
1099
+ primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
1100
+ secondary: "border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700 focus:ring-slate-400",
1101
+ ghost: "text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 focus:ring-slate-400"
1102
+ };
1103
+ const sizes = {
1104
+ sm: "px-2.5 py-1 text-xs gap-1",
1105
+ md: "px-3 py-1.5 text-sm gap-1.5",
1106
+ lg: "px-4 py-2 text-base gap-2"
1107
+ };
1108
+ const classes = [base, variants[variant] || variants.primary, sizes[size] || sizes.md, className].filter(Boolean).join(" ");
1109
+ return /* @__PURE__ */ import_react3.default.createElement(
1110
+ "button",
1111
+ {
1112
+ type: "button",
1113
+ className: classes,
1114
+ disabled,
1115
+ onClick,
1116
+ ...rest
1117
+ },
1118
+ children
1119
+ );
1120
+ }
1121
+ Button.propTypes = {
1122
+ /** Varian visual tombol. */
1123
+ variant: import_prop_types.default.oneOf(["primary", "secondary", "ghost"]),
1124
+ /** Ukuran tombol. */
1125
+ size: import_prop_types.default.oneOf(["sm", "md", "lg"]),
1126
+ /** Apakah tombol non-aktif. */
1127
+ disabled: import_prop_types.default.bool,
1128
+ /** ClassName tambahan dari consumer. */
1129
+ className: import_prop_types.default.string,
1130
+ /** Callback saat tombol diklik. */
1131
+ onClick: import_prop_types.default.func,
1132
+ /** Konten tombol. */
1133
+ children: import_prop_types.default.node.isRequired
1134
+ };
1135
+
1136
+ // src/presentation/atoms/Input.jsx
1137
+ var import_react4 = __toESM(require("react"), 1);
1138
+ var import_prop_types2 = __toESM(require("prop-types"), 1);
1139
+ function Input({
1140
+ type = "text",
1141
+ value,
1142
+ onChange,
1143
+ placeholder,
1144
+ label,
1145
+ disabled = false,
1146
+ className = "",
1147
+ ...rest
1148
+ }) {
1149
+ const inputClasses = [
1150
+ "px-3 py-2 rounded-2xl border border-slate-300 dark:border-slate-600",
1151
+ "bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100",
1152
+ "text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500",
1153
+ "disabled:opacity-50 disabled:cursor-not-allowed",
1154
+ className
1155
+ ].filter(Boolean).join(" ");
1156
+ return /* @__PURE__ */ import_react4.default.createElement("div", { className: "flex flex-col gap-1" }, label ? /* @__PURE__ */ import_react4.default.createElement("label", { className: "text-xs font-medium text-slate-500 dark:text-slate-400" }, label) : null, /* @__PURE__ */ import_react4.default.createElement(
1157
+ "input",
1158
+ {
1159
+ type,
1160
+ value,
1161
+ onChange,
1162
+ placeholder,
1163
+ disabled,
1164
+ className: inputClasses,
1165
+ ...rest
1166
+ }
1167
+ ));
1168
+ }
1169
+ Input.propTypes = {
1170
+ /** Tipe input. */
1171
+ type: import_prop_types2.default.string,
1172
+ /** Nilai input. */
1173
+ value: import_prop_types2.default.oneOfType([import_prop_types2.default.string, import_prop_types2.default.number]),
1174
+ /** Callback saat nilai berubah. */
1175
+ onChange: import_prop_types2.default.func,
1176
+ /** Placeholder teks. */
1177
+ placeholder: import_prop_types2.default.string,
1178
+ /** Label di atas input. */
1179
+ label: import_prop_types2.default.string,
1180
+ /** Apakah input non-aktif. */
1181
+ disabled: import_prop_types2.default.bool,
1182
+ /** ClassName tambahan. */
1183
+ className: import_prop_types2.default.string
1184
+ };
1185
+
1186
+ // src/presentation/atoms/Icon.jsx
1187
+ var import_react5 = __toESM(require("react"), 1);
1188
+ var import_prop_types3 = __toESM(require("prop-types"), 1);
1189
+ var import_lucide_react = require("lucide-react");
1190
+ var ICON_REGISTRY = {
1191
+ BarChart3: import_lucide_react.BarChart3,
1192
+ Calendar: import_lucide_react.Calendar,
1193
+ DollarSign: import_lucide_react.DollarSign,
1194
+ PieChart: import_lucide_react.PieChart,
1195
+ RotateCcw: import_lucide_react.RotateCcw,
1196
+ TrendingUp: import_lucide_react.TrendingUp,
1197
+ TrendingDown: import_lucide_react.TrendingDown,
1198
+ Users: import_lucide_react.Users,
1199
+ Search: import_lucide_react.Search,
1200
+ ChevronLeft: import_lucide_react.ChevronLeft,
1201
+ ChevronRight: import_lucide_react.ChevronRight,
1202
+ ArrowUp: import_lucide_react.ArrowUp,
1203
+ ArrowDown: import_lucide_react.ArrowDown,
1204
+ AlertCircle: import_lucide_react.AlertCircle
1205
+ };
1206
+ function Icon({ name, size = 20, className = "", ...rest }) {
1207
+ const IconComponent = ICON_REGISTRY[name] || import_lucide_react.BarChart3;
1208
+ return /* @__PURE__ */ import_react5.default.createElement(IconComponent, { size, className, ...rest });
1209
+ }
1210
+ function resolveIcon(name) {
1211
+ return ICON_REGISTRY[name] || import_lucide_react.BarChart3;
1212
+ }
1213
+ Icon.propTypes = {
1214
+ /** Nama ikon dari registry Lucide React. */
1215
+ name: import_prop_types3.default.string.isRequired,
1216
+ /** Ukuran ikon dalam piksel. */
1217
+ size: import_prop_types3.default.number,
1218
+ /** ClassName tambahan. */
1219
+ className: import_prop_types3.default.string
1220
+ };
1221
+
1222
+ // src/presentation/atoms/Typography.jsx
1223
+ var import_react6 = __toESM(require("react"), 1);
1224
+ var import_prop_types4 = __toESM(require("prop-types"), 1);
1225
+ var VARIANT_MAP = {
1226
+ h1: { tag: "h1", className: "text-2xl sm:text-3xl font-bold" },
1227
+ h2: { tag: "h2", className: "text-xl sm:text-2xl font-bold" },
1228
+ h3: { tag: "h3", className: "text-lg font-semibold" },
1229
+ subheading: { tag: "p", className: "text-sm font-medium text-slate-500 dark:text-slate-400" },
1230
+ body: { tag: "p", className: "text-sm text-slate-700 dark:text-slate-300" },
1231
+ caption: { tag: "span", className: "text-xs text-slate-500 dark:text-slate-400" },
1232
+ metric: { tag: "span", className: "text-3xl font-bold text-slate-900 dark:text-slate-100" }
1233
+ };
1234
+ function Typography({
1235
+ variant = "body",
1236
+ className = "",
1237
+ children,
1238
+ ...rest
1239
+ }) {
1240
+ const config = VARIANT_MAP[variant] || VARIANT_MAP.body;
1241
+ const Tag = config.tag;
1242
+ const classes = [config.className, className].filter(Boolean).join(" ");
1243
+ return /* @__PURE__ */ import_react6.default.createElement(Tag, { className: classes, ...rest }, children);
1244
+ }
1245
+ Typography.propTypes = {
1246
+ /** Varian tipografi. */
1247
+ variant: import_prop_types4.default.oneOf(["h1", "h2", "h3", "subheading", "body", "caption", "metric"]),
1248
+ /** ClassName tambahan. */
1249
+ className: import_prop_types4.default.string,
1250
+ /** Konten teks. */
1251
+ children: import_prop_types4.default.node.isRequired
1252
+ };
1253
+
1254
+ // src/presentation/atoms/Badge.jsx
1255
+ var import_react7 = __toESM(require("react"), 1);
1256
+ var import_prop_types5 = __toESM(require("prop-types"), 1);
1257
+ var STATUS_CLASSES = {
1258
+ confirmed: "bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-200",
1259
+ pending: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-200",
1260
+ cancelled: "bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-200",
1261
+ default: "bg-slate-100 text-slate-800 dark:bg-slate-700 dark:text-slate-200",
1262
+ success: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300",
1263
+ info: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300"
1264
+ };
1265
+ function Badge({ status = "default", className = "", children, ...rest }) {
1266
+ const colorClass = STATUS_CLASSES[status] || STATUS_CLASSES.default;
1267
+ const classes = ["px-2 py-1 rounded-full text-xs font-medium", colorClass, className].filter(Boolean).join(" ");
1268
+ return /* @__PURE__ */ import_react7.default.createElement("span", { className: classes, ...rest }, children);
1269
+ }
1270
+ Badge.propTypes = {
1271
+ /** Status yang menentukan warna badge. */
1272
+ status: import_prop_types5.default.string,
1273
+ /** ClassName tambahan. */
1274
+ className: import_prop_types5.default.string,
1275
+ /** Label teks badge. */
1276
+ children: import_prop_types5.default.node.isRequired
1277
+ };
1278
+
1279
+ // src/presentation/atoms/SkeletonLoader.jsx
1280
+ var import_react8 = __toESM(require("react"), 1);
1281
+ var import_prop_types6 = __toESM(require("prop-types"), 1);
1282
+ function SkeletonLoader({ className = "" }) {
1283
+ return /* @__PURE__ */ import_react8.default.createElement(
1284
+ "div",
1285
+ {
1286
+ className: `animate-pulse bg-slate-200 dark:bg-slate-700 rounded-xl ${className}`,
1287
+ role: "status",
1288
+ "aria-label": "Loading..."
1289
+ }
1290
+ );
1291
+ }
1292
+ SkeletonLoader.propTypes = {
1293
+ /** ClassName tambahan untuk mengatur ukuran skeleton (misal: "h-28", "h-64"). */
1294
+ className: import_prop_types6.default.string
1295
+ };
1296
+
1297
+ // src/presentation/molecules/StatCard.jsx
1298
+ var import_react9 = __toESM(require("react"), 1);
1299
+ var import_prop_types7 = __toESM(require("prop-types"), 1);
1300
+ function StatCard({
1301
+ label,
1302
+ value,
1303
+ icon = "TrendingUp",
1304
+ format = "number",
1305
+ trend = null,
1306
+ className = ""
1307
+ }) {
1308
+ const formattedValue = renderValue(format, value);
1309
+ return /* @__PURE__ */ import_react9.default.createElement("div", { className: `card p-4 flex flex-col justify-between gap-2 ${className}` }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ import_react9.default.createElement("div", { className: "flex items-center gap-2 text-slate-500 dark:text-slate-400" }, /* @__PURE__ */ import_react9.default.createElement(Icon, { name: icon, size: 20 }), /* @__PURE__ */ import_react9.default.createElement(Typography, { variant: "subheading" }, label)), trend ? /* @__PURE__ */ import_react9.default.createElement(
1310
+ Icon,
1311
+ {
1312
+ name: trend === "up" ? "TrendingUp" : "TrendingDown",
1313
+ size: 16,
1314
+ className: trend === "up" ? "text-emerald-500" : "text-red-500"
1315
+ }
1316
+ ) : null), /* @__PURE__ */ import_react9.default.createElement(Typography, { variant: "metric" }, formattedValue));
1317
+ }
1318
+ function renderValue(format, value) {
1319
+ if (format === "currency") return `Rp ${formatIDR(value)}`;
1320
+ if (format === "percent") return `${Number(value) || 0}%`;
1321
+ return String(Number(value) || 0);
1322
+ }
1323
+ StatCard.propTypes = {
1324
+ /** Label deskriptif metrik. */
1325
+ label: import_prop_types7.default.string.isRequired,
1326
+ /** Nilai metrik. */
1327
+ value: import_prop_types7.default.oneOfType([import_prop_types7.default.number, import_prop_types7.default.string]),
1328
+ /** Nama ikon dari registry Lucide React. */
1329
+ icon: import_prop_types7.default.string,
1330
+ /** Format tampilan nilai. */
1331
+ format: import_prop_types7.default.oneOf(["number", "currency", "percent"]),
1332
+ /** Arah tren opsional. */
1333
+ trend: import_prop_types7.default.oneOf(["up", "down", null]),
1334
+ /** ClassName tambahan. */
1335
+ className: import_prop_types7.default.string
1336
+ };
1337
+
1338
+ // src/presentation/molecules/SearchBar.jsx
1339
+ var import_react10 = __toESM(require("react"), 1);
1340
+ var import_prop_types8 = __toESM(require("prop-types"), 1);
1341
+ function SearchBar({
1342
+ value: controlledValue,
1343
+ onChange,
1344
+ onSearch,
1345
+ placeholder = "Search...",
1346
+ className = ""
1347
+ }) {
1348
+ const [internalValue, setInternalValue] = (0, import_react10.useState)("");
1349
+ const isControlled = controlledValue !== void 0;
1350
+ const currentValue = isControlled ? controlledValue : internalValue;
1351
+ const handleChange = (0, import_react10.useCallback)(
1352
+ (event) => {
1353
+ const newValue = event.target.value;
1354
+ if (!isControlled) setInternalValue(newValue);
1355
+ if (onChange) onChange(newValue);
1356
+ if (onSearch) onSearch(newValue);
1357
+ },
1358
+ [isControlled, onChange, onSearch]
1359
+ );
1360
+ const handleKeyDown = (0, import_react10.useCallback)(
1361
+ (event) => {
1362
+ if (event.key === "Enter" && onSearch) {
1363
+ onSearch(currentValue);
1364
+ }
1365
+ },
1366
+ [currentValue, onSearch]
1367
+ );
1368
+ return /* @__PURE__ */ import_react10.default.createElement("div", { className: `relative ${className}` }, /* @__PURE__ */ import_react10.default.createElement("div", { className: "absolute inset-y-0 left-3 flex items-center pointer-events-none" }, /* @__PURE__ */ import_react10.default.createElement(Icon, { name: "Search", size: 16, className: "text-slate-400" })), /* @__PURE__ */ import_react10.default.createElement(
1369
+ Input,
1370
+ {
1371
+ type: "search",
1372
+ value: currentValue,
1373
+ onChange: handleChange,
1374
+ onKeyDown: handleKeyDown,
1375
+ placeholder,
1376
+ className: "pl-9 w-full"
1377
+ }
1378
+ ));
1379
+ }
1380
+ SearchBar.propTypes = {
1381
+ /** Nilai pencarian saat ini (controlled). */
1382
+ value: import_prop_types8.default.string,
1383
+ /** Callback saat nilai input berubah. */
1384
+ onChange: import_prop_types8.default.func,
1385
+ /** Callback saat pencarian disubmit. */
1386
+ onSearch: import_prop_types8.default.func,
1387
+ /** Placeholder teks. */
1388
+ placeholder: import_prop_types8.default.string,
1389
+ /** ClassName tambahan. */
1390
+ className: import_prop_types8.default.string
1391
+ };
1392
+
1393
+ // src/presentation/molecules/DateRangeFilter.jsx
1394
+ var import_react11 = __toESM(require("react"), 1);
1395
+ var import_prop_types9 = __toESM(require("prop-types"), 1);
1396
+ var CURRENT_YEAR = (/* @__PURE__ */ new Date()).getFullYear();
1397
+ var MIN_YEAR2 = 2020;
1398
+ var MAX_YEAR = CURRENT_YEAR + 1;
1399
+ var MONTHS = [
1400
+ "Jan",
1401
+ "Feb",
1402
+ "Mar",
1403
+ "Apr",
1404
+ "May",
1405
+ "Jun",
1406
+ "Jul",
1407
+ "Aug",
1408
+ "Sep",
1409
+ "Oct",
1410
+ "Nov",
1411
+ "Dec"
1412
+ ];
1413
+ function getYears() {
1414
+ const years = [];
1415
+ for (let y = MAX_YEAR; y >= MIN_YEAR2; y--) {
1416
+ years.push(y);
1417
+ }
1418
+ return years;
1419
+ }
1420
+ function getDaysInMonth(year, month) {
1421
+ return new Date(year, month, 0).getDate();
1422
+ }
1423
+ function parseDate(value) {
1424
+ if (value && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
1425
+ const [y, m, d] = value.split("-").map(Number);
1426
+ if (y >= MIN_YEAR2 && y <= MAX_YEAR && m >= 1 && m <= 12) {
1427
+ return { year: y, month: m, day: d };
1428
+ }
1429
+ }
1430
+ const now = /* @__PURE__ */ new Date();
1431
+ return { year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() };
1432
+ }
1433
+ function formatDate2({ year, month, day }) {
1434
+ return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
1435
+ }
1436
+ function DatePicker({ value, onChange, disabled = false, label = "" }) {
1437
+ const parsed = parseDate(value);
1438
+ const years = getYears();
1439
+ const maxDay = getDaysInMonth(parsed.year, parsed.month);
1440
+ const handleChange = (0, import_react11.useCallback)(
1441
+ (field, newVal) => {
1442
+ const updated = { ...parsed, [field]: Number(newVal) };
1443
+ const maxD = getDaysInMonth(updated.year, updated.month);
1444
+ if (updated.day > maxD) updated.day = maxD;
1445
+ onChange(formatDate2(updated));
1446
+ },
1447
+ [parsed, onChange]
1448
+ );
1449
+ const selectClass = "px-2 py-1.5 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed";
1450
+ return /* @__PURE__ */ import_react11.default.createElement("div", { className: "flex flex-col gap-1" }, label ? /* @__PURE__ */ import_react11.default.createElement(Typography, { variant: "caption", className: "font-medium" }, label) : null, /* @__PURE__ */ import_react11.default.createElement("div", { className: "flex items-center gap-1" }, /* @__PURE__ */ import_react11.default.createElement(
1451
+ "select",
1452
+ {
1453
+ className: selectClass,
1454
+ value: parsed.day,
1455
+ onChange: (e) => handleChange("day", e.target.value),
1456
+ disabled,
1457
+ "aria-label": `${label} day`
1458
+ },
1459
+ Array.from({ length: maxDay }, (_, i) => i + 1).map((d) => /* @__PURE__ */ import_react11.default.createElement("option", { key: d, value: d }, String(d).padStart(2, "0")))
1460
+ ), /* @__PURE__ */ import_react11.default.createElement(
1461
+ "select",
1462
+ {
1463
+ className: selectClass,
1464
+ value: parsed.month,
1465
+ onChange: (e) => handleChange("month", e.target.value),
1466
+ disabled,
1467
+ "aria-label": `${label} month`
1468
+ },
1469
+ MONTHS.map((name, idx) => /* @__PURE__ */ import_react11.default.createElement("option", { key: idx + 1, value: idx + 1 }, name))
1470
+ ), /* @__PURE__ */ import_react11.default.createElement(
1471
+ "select",
1472
+ {
1473
+ className: selectClass,
1474
+ value: parsed.year,
1475
+ onChange: (e) => handleChange("year", e.target.value),
1476
+ disabled,
1477
+ "aria-label": `${label} year`
1478
+ },
1479
+ years.map((y) => /* @__PURE__ */ import_react11.default.createElement("option", { key: y, value: y }, y))
1480
+ )));
1481
+ }
1482
+ DatePicker.propTypes = {
1483
+ value: import_prop_types9.default.string.isRequired,
1484
+ onChange: import_prop_types9.default.func.isRequired,
1485
+ disabled: import_prop_types9.default.bool,
1486
+ label: import_prop_types9.default.string
1487
+ };
1488
+ function DateRangeFilter({
1489
+ dateFrom,
1490
+ dateTo,
1491
+ onRangeChange,
1492
+ disabled = false,
1493
+ labelFrom = "From",
1494
+ labelTo = "To",
1495
+ className = ""
1496
+ }) {
1497
+ const handleFromChange = (0, import_react11.useCallback)(
1498
+ (newDate) => onRangeChange == null ? void 0 : onRangeChange({ dateFrom: newDate, dateTo }),
1499
+ [dateTo, onRangeChange]
1500
+ );
1501
+ const handleToChange = (0, import_react11.useCallback)(
1502
+ (newDate) => onRangeChange == null ? void 0 : onRangeChange({ dateFrom, dateTo: newDate }),
1503
+ [dateFrom, onRangeChange]
1504
+ );
1505
+ return /* @__PURE__ */ import_react11.default.createElement("div", { className: `flex flex-wrap items-end gap-3 ${className}` }, /* @__PURE__ */ import_react11.default.createElement(
1506
+ DatePicker,
1507
+ {
1508
+ value: dateFrom,
1509
+ onChange: handleFromChange,
1510
+ disabled,
1511
+ label: labelFrom
1512
+ }
1513
+ ), /* @__PURE__ */ import_react11.default.createElement(
1514
+ DatePicker,
1515
+ {
1516
+ value: dateTo,
1517
+ onChange: handleToChange,
1518
+ disabled,
1519
+ label: labelTo
1520
+ }
1521
+ ));
1522
+ }
1523
+ DateRangeFilter.propTypes = {
1524
+ dateFrom: import_prop_types9.default.string.isRequired,
1525
+ dateTo: import_prop_types9.default.string.isRequired,
1526
+ onRangeChange: import_prop_types9.default.func.isRequired,
1527
+ disabled: import_prop_types9.default.bool,
1528
+ labelFrom: import_prop_types9.default.string,
1529
+ labelTo: import_prop_types9.default.string,
1530
+ className: import_prop_types9.default.string
1531
+ };
1532
+
1533
+ // src/presentation/molecules/ChartHeader.jsx
1534
+ var import_react12 = __toESM(require("react"), 1);
1535
+ var import_prop_types10 = __toESM(require("prop-types"), 1);
1536
+ function ChartHeader({
1537
+ title,
1538
+ icon = "BarChart3",
1539
+ actions = null,
1540
+ className = ""
1541
+ }) {
1542
+ return /* @__PURE__ */ import_react12.default.createElement("div", { className: `flex items-center justify-between mb-2 ${className}` }, /* @__PURE__ */ import_react12.default.createElement(Typography, { variant: "h3", className: "flex items-center gap-2" }, /* @__PURE__ */ import_react12.default.createElement(Icon, { name: icon, size: 18 }), title), actions);
1543
+ }
1544
+ ChartHeader.propTypes = {
1545
+ /** Judul grafik. */
1546
+ title: import_prop_types10.default.string.isRequired,
1547
+ /** Nama ikon header. */
1548
+ icon: import_prop_types10.default.string,
1549
+ /** Elemen aksi tambahan di sisi kanan. */
1550
+ actions: import_prop_types10.default.node,
1551
+ /** ClassName tambahan. */
1552
+ className: import_prop_types10.default.string
1553
+ };
1554
+
1555
+ // src/presentation/organisms/DataTable.jsx
1556
+ var import_react13 = __toESM(require("react"), 1);
1557
+ var import_prop_types11 = __toESM(require("prop-types"), 1);
1558
+ function DataTable({
1559
+ columns,
1560
+ data = [],
1561
+ labels = {},
1562
+ dateLocale = "id-ID",
1563
+ searchable = true,
1564
+ sortable = true,
1565
+ pageSize = 10,
1566
+ emptyLabel = "No data",
1567
+ searchPlaceholder = "Search...",
1568
+ className = ""
1569
+ }) {
1570
+ const [searchQuery, setSearchQuery] = (0, import_react13.useState)("");
1571
+ const [sortField, setSortField] = (0, import_react13.useState)(null);
1572
+ const [sortDirection, setSortDirection] = (0, import_react13.useState)("asc");
1573
+ const [currentPage, setCurrentPage] = (0, import_react13.useState)(1);
1574
+ const filteredData = (0, import_react13.useMemo)(() => {
1575
+ if (!searchQuery.trim()) return data;
1576
+ const query = searchQuery.toLowerCase();
1577
+ return data.filter(
1578
+ (row) => columns.some((col) => {
1579
+ const value = row[col.accessor];
1580
+ return value != null && String(value).toLowerCase().includes(query);
1581
+ })
1582
+ );
1583
+ }, [data, searchQuery, columns]);
1584
+ const sortedData = (0, import_react13.useMemo)(() => {
1585
+ if (!sortField) return filteredData;
1586
+ const sorted = [...filteredData].sort((a, b) => {
1587
+ const aVal = a[sortField] ?? "";
1588
+ const bVal = b[sortField] ?? "";
1589
+ if (typeof aVal === "number" && typeof bVal === "number") {
1590
+ return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
1591
+ }
1592
+ const comparison = String(aVal).localeCompare(String(bVal), void 0, {
1593
+ numeric: true
1594
+ });
1595
+ return sortDirection === "asc" ? comparison : -comparison;
1596
+ });
1597
+ return sorted;
1598
+ }, [filteredData, sortField, sortDirection]);
1599
+ const totalPages = Math.max(1, Math.ceil(sortedData.length / pageSize));
1600
+ const safePage = Math.min(currentPage, totalPages);
1601
+ const paginatedData = (0, import_react13.useMemo)(() => {
1602
+ const start = (safePage - 1) * pageSize;
1603
+ return sortedData.slice(start, start + pageSize);
1604
+ }, [sortedData, safePage, pageSize]);
1605
+ const handleSearch = (0, import_react13.useCallback)((value) => {
1606
+ setSearchQuery(value);
1607
+ setCurrentPage(1);
1608
+ }, []);
1609
+ const handleSort = (0, import_react13.useCallback)(
1610
+ (accessor) => {
1611
+ if (!sortable) return;
1612
+ if (sortField === accessor) {
1613
+ setSortDirection((prev) => prev === "asc" ? "desc" : "asc");
1614
+ } else {
1615
+ setSortField(accessor);
1616
+ setSortDirection("asc");
1617
+ }
1618
+ setCurrentPage(1);
1619
+ },
1620
+ [sortable, sortField]
1621
+ );
1622
+ const handlePrevPage = (0, import_react13.useCallback)(() => {
1623
+ setCurrentPage((prev) => Math.max(1, prev - 1));
1624
+ }, []);
1625
+ const handleNextPage = (0, import_react13.useCallback)(() => {
1626
+ setCurrentPage((prev) => Math.min(totalPages, prev + 1));
1627
+ }, [totalPages]);
1628
+ return /* @__PURE__ */ import_react13.default.createElement("div", { className: `space-y-3 ${className}` }, searchable ? /* @__PURE__ */ import_react13.default.createElement(
1629
+ SearchBar,
1630
+ {
1631
+ value: searchQuery,
1632
+ onSearch: handleSearch,
1633
+ placeholder: searchPlaceholder,
1634
+ className: "max-w-sm"
1635
+ }
1636
+ ) : null, /* @__PURE__ */ import_react13.default.createElement("div", { className: "overflow-x-auto" }, /* @__PURE__ */ import_react13.default.createElement("table", { className: "min-w-full text-sm border-collapse" }, /* @__PURE__ */ import_react13.default.createElement("thead", null, /* @__PURE__ */ import_react13.default.createElement("tr", { className: "bg-slate-100 dark:bg-slate-800 text-left" }, columns.map((column) => /* @__PURE__ */ import_react13.default.createElement(
1637
+ "th",
1638
+ {
1639
+ key: column.id,
1640
+ className: [
1641
+ "p-3",
1642
+ sortable ? "cursor-pointer select-none hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" : ""
1643
+ ].join(" "),
1644
+ onClick: () => handleSort(column.accessor)
1645
+ },
1646
+ /* @__PURE__ */ import_react13.default.createElement("div", { className: "flex items-center gap-1" }, /* @__PURE__ */ import_react13.default.createElement("span", null, labels[column.label] || column.label), sortable && sortField === column.accessor ? /* @__PURE__ */ import_react13.default.createElement(
1647
+ Icon,
1648
+ {
1649
+ name: sortDirection === "asc" ? "ArrowUp" : "ArrowDown",
1650
+ size: 14,
1651
+ className: "text-blue-500"
1652
+ }
1653
+ ) : null)
1654
+ )))), /* @__PURE__ */ import_react13.default.createElement("tbody", null, paginatedData.map((row) => /* @__PURE__ */ import_react13.default.createElement(
1655
+ "tr",
1656
+ {
1657
+ key: row.id,
1658
+ className: "border-b border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800/50"
1659
+ },
1660
+ columns.map((column) => /* @__PURE__ */ import_react13.default.createElement("td", { key: `${row.id}-${column.id}`, className: "p-3" }, renderCell(column, row, dateLocale)))
1661
+ )), paginatedData.length === 0 ? /* @__PURE__ */ import_react13.default.createElement("tr", null, /* @__PURE__ */ import_react13.default.createElement(
1662
+ "td",
1663
+ {
1664
+ className: "p-6 text-center text-slate-500",
1665
+ colSpan: columns.length
1666
+ },
1667
+ emptyLabel
1668
+ )) : null))), sortedData.length > pageSize ? /* @__PURE__ */ import_react13.default.createElement("div", { className: "flex items-center justify-between px-1" }, /* @__PURE__ */ import_react13.default.createElement("span", { className: "text-xs text-slate-500" }, (safePage - 1) * pageSize + 1, "\u2013", Math.min(safePage * pageSize, sortedData.length), " of", " ", sortedData.length), /* @__PURE__ */ import_react13.default.createElement("div", { className: "flex items-center gap-1" }, /* @__PURE__ */ import_react13.default.createElement(
1669
+ "button",
1670
+ {
1671
+ type: "button",
1672
+ className: "p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed transition-colors",
1673
+ onClick: handlePrevPage,
1674
+ disabled: safePage <= 1,
1675
+ "aria-label": "Previous page"
1676
+ },
1677
+ /* @__PURE__ */ import_react13.default.createElement(Icon, { name: "ChevronLeft", size: 16 })
1678
+ ), /* @__PURE__ */ import_react13.default.createElement("span", { className: "text-xs text-slate-600 dark:text-slate-300 min-w-[60px] text-center" }, safePage, " / ", totalPages), /* @__PURE__ */ import_react13.default.createElement(
1679
+ "button",
1680
+ {
1681
+ type: "button",
1682
+ className: "p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed transition-colors",
1683
+ onClick: handleNextPage,
1684
+ disabled: safePage >= totalPages,
1685
+ "aria-label": "Next page"
1686
+ },
1687
+ /* @__PURE__ */ import_react13.default.createElement(Icon, { name: "ChevronRight", size: 16 })
1688
+ ))) : null);
1689
+ }
1690
+ function renderCell(column, row, dateLocale) {
1691
+ const value = row[column.accessor];
1692
+ if (column.type === "date") return formatDate(value, dateLocale);
1693
+ if (column.type === "currency") return `Rp ${formatIDR(value)}`;
1694
+ if (column.type === "statusBadge") {
1695
+ const status = row[column.statusAccessor] || "pending";
1696
+ return /* @__PURE__ */ import_react13.default.createElement(Badge, { status }, value);
1697
+ }
1698
+ return value || "-";
1699
+ }
1700
+ DataTable.propTypes = {
1701
+ /** Definisi kolom tabel. */
1702
+ columns: import_prop_types11.default.arrayOf(
1703
+ import_prop_types11.default.shape({
1704
+ id: import_prop_types11.default.string.isRequired,
1705
+ label: import_prop_types11.default.string.isRequired,
1706
+ accessor: import_prop_types11.default.string.isRequired,
1707
+ type: import_prop_types11.default.oneOf(["date", "currency", "statusBadge"]),
1708
+ statusAccessor: import_prop_types11.default.string
1709
+ })
1710
+ ).isRequired,
1711
+ /** Array data baris. */
1712
+ data: import_prop_types11.default.arrayOf(import_prop_types11.default.object),
1713
+ /** Objek label i18n. */
1714
+ labels: import_prop_types11.default.object,
1715
+ /** Locale untuk format tanggal. */
1716
+ dateLocale: import_prop_types11.default.string,
1717
+ /** Apakah tabel mendukung pencarian. */
1718
+ searchable: import_prop_types11.default.bool,
1719
+ /** Apakah tabel mendukung pengurutan. */
1720
+ sortable: import_prop_types11.default.bool,
1721
+ /** Jumlah baris per halaman. */
1722
+ pageSize: import_prop_types11.default.number,
1723
+ /** Label saat data kosong. */
1724
+ emptyLabel: import_prop_types11.default.string,
1725
+ /** Placeholder pencarian. */
1726
+ searchPlaceholder: import_prop_types11.default.string,
1727
+ /** ClassName tambahan. */
1728
+ className: import_prop_types11.default.string
1729
+ };
1730
+
1731
+ // src/presentation/organisms/ChartCard.jsx
1732
+ var import_react14 = __toESM(require("react"), 1);
1733
+ var import_prop_types12 = __toESM(require("prop-types"), 1);
1734
+ var import_recharts = require("recharts");
1735
+ var COLORS = [
1736
+ "#0ea5e9",
1737
+ "#a78bfa",
1738
+ "#f59e0b",
1739
+ "#10b981",
1740
+ "#ef4444",
1741
+ "#22d3ee"
1742
+ ];
1743
+ function ChartCard({
1744
+ widget,
1745
+ labels,
1746
+ loading,
1747
+ filters,
1748
+ chartData,
1749
+ className = ""
1750
+ }) {
1751
+ const content = (() => {
1752
+ if (loading) return /* @__PURE__ */ import_react14.default.createElement(SkeletonLoader, { className: "h-64" });
1753
+ if (widget.type === "dailyArea") {
1754
+ return renderDailyArea(labels, filters, chartData.dailyTrends);
1755
+ }
1756
+ if (widget.type === "statusPie") {
1757
+ return renderPie(chartData.statusDistribution);
1758
+ }
1759
+ if (widget.type === "audiencePie") {
1760
+ return renderPie(chartData.audienceDistribution);
1761
+ }
1762
+ if (widget.type === "topPackagesBar") {
1763
+ return renderTopPackages(chartData.topPackages, labels, filters.sortPkgBy);
1764
+ }
1765
+ return /* @__PURE__ */ import_react14.default.createElement("div", { className: "text-sm text-slate-500" }, "Unsupported chart type.");
1766
+ })();
1767
+ return /* @__PURE__ */ import_react14.default.createElement("div", { className: `card p-4 ${className}` }, /* @__PURE__ */ import_react14.default.createElement(
1768
+ ChartHeader,
1769
+ {
1770
+ title: labels[widget.label] || widget.label,
1771
+ icon: widget.icon
1772
+ }
1773
+ ), content);
1774
+ }
1775
+ function renderDailyArea(labels, filters, dailyData = []) {
1776
+ return /* @__PURE__ */ import_react14.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: 256 }, /* @__PURE__ */ import_react14.default.createElement(import_recharts.AreaChart, { data: dailyData }, /* @__PURE__ */ import_react14.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react14.default.createElement(import_recharts.XAxis, { dataKey: "label" }), /* @__PURE__ */ import_react14.default.createElement(import_recharts.YAxis, { yAxisId: "left" }), /* @__PURE__ */ import_react14.default.createElement(import_recharts.YAxis, { yAxisId: "right", orientation: "right" }), /* @__PURE__ */ import_react14.default.createElement(
1777
+ import_recharts.Tooltip,
1778
+ {
1779
+ formatter: (value, name) => name === "revenue" ? [`Rp ${formatIDR(value)}`, labels.revenueMetric] : [value, labels.bookingsMetric]
1780
+ }
1781
+ ), /* @__PURE__ */ import_react14.default.createElement(import_recharts.Legend, null), /* @__PURE__ */ import_react14.default.createElement(
1782
+ import_recharts.Area,
1783
+ {
1784
+ yAxisId: "left",
1785
+ type: "monotone",
1786
+ dataKey: "count",
1787
+ name: labels.confirmedBookingMetric,
1788
+ stroke: "#0ea5e9",
1789
+ fill: "#0ea5e9",
1790
+ fillOpacity: 0.3
1791
+ }
1792
+ ), /* @__PURE__ */ import_react14.default.createElement(
1793
+ import_recharts.Area,
1794
+ {
1795
+ yAxisId: "right",
1796
+ type: "monotone",
1797
+ dataKey: "revenue",
1798
+ name: labels.confirmedRevenueMetric,
1799
+ stroke: "#a78bfa",
1800
+ fill: "#a78bfa",
1801
+ fillOpacity: 0.3
1802
+ }
1803
+ ), filters.includePendingOverlay ? /* @__PURE__ */ import_react14.default.createElement(
1804
+ import_recharts.Area,
1805
+ {
1806
+ yAxisId: "left",
1807
+ type: "monotone",
1808
+ dataKey: "pendingCount",
1809
+ name: labels.pendingMetric,
1810
+ stroke: "#ef4444",
1811
+ fill: "#ef4444",
1812
+ fillOpacity: 0.1,
1813
+ strokeDasharray: "4 4"
1814
+ }
1815
+ ) : null));
1816
+ }
1817
+ function renderPie(data = []) {
1818
+ return /* @__PURE__ */ import_react14.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: 256 }, /* @__PURE__ */ import_react14.default.createElement(import_recharts.PieChart, null, /* @__PURE__ */ import_react14.default.createElement(
1819
+ import_recharts.Pie,
1820
+ {
1821
+ data,
1822
+ dataKey: "count",
1823
+ nameKey: "label",
1824
+ innerRadius: 60,
1825
+ outerRadius: 100,
1826
+ paddingAngle: 2
1827
+ },
1828
+ data.map((_, index) => /* @__PURE__ */ import_react14.default.createElement(
1829
+ import_recharts.Cell,
1830
+ {
1831
+ key: `${index}-${data[index].label}`,
1832
+ fill: COLORS[index % COLORS.length]
1833
+ }
1834
+ ))
1835
+ ), /* @__PURE__ */ import_react14.default.createElement(import_recharts.Tooltip, null), /* @__PURE__ */ import_react14.default.createElement(import_recharts.Legend, null)));
1836
+ }
1837
+ function renderTopPackages(data = [], labels, sortPkgBy) {
1838
+ return /* @__PURE__ */ import_react14.default.createElement(import_recharts.ResponsiveContainer, { width: "100%", height: 256 }, /* @__PURE__ */ import_react14.default.createElement(import_recharts.BarChart, { data, layout: "vertical" }, /* @__PURE__ */ import_react14.default.createElement(import_recharts.CartesianGrid, { strokeDasharray: "3 3" }), /* @__PURE__ */ import_react14.default.createElement(import_recharts.XAxis, { type: "number", allowDecimals: false }), /* @__PURE__ */ import_react14.default.createElement(import_recharts.YAxis, { type: "category", dataKey: "name", width: 120 }), /* @__PURE__ */ import_react14.default.createElement(
1839
+ import_recharts.Tooltip,
1840
+ {
1841
+ formatter: (value) => sortPkgBy === "revenue" ? [`Rp ${formatIDR(value)}`, labels.revenueMetric] : [value, labels.bookingsMetric]
1842
+ }
1843
+ ), /* @__PURE__ */ import_react14.default.createElement(
1844
+ import_recharts.Bar,
1845
+ {
1846
+ dataKey: "value",
1847
+ name: sortPkgBy === "revenue" ? labels.revenueMetric : labels.bookingsMetric,
1848
+ fill: "#10b981",
1849
+ radius: [4, 4, 4, 4]
1850
+ }
1851
+ )));
1852
+ }
1853
+ ChartCard.propTypes = {
1854
+ /** Konfigurasi widget chart. */
1855
+ widget: import_prop_types12.default.shape({
1856
+ id: import_prop_types12.default.string.isRequired,
1857
+ type: import_prop_types12.default.string.isRequired,
1858
+ label: import_prop_types12.default.string.isRequired,
1859
+ icon: import_prop_types12.default.string
1860
+ }).isRequired,
1861
+ /** Objek label i18n. */
1862
+ labels: import_prop_types12.default.object.isRequired,
1863
+ /** Apakah sedang loading. */
1864
+ loading: import_prop_types12.default.bool,
1865
+ /** State filter dashboard. */
1866
+ filters: import_prop_types12.default.object,
1867
+ /** Data untuk semua jenis chart. */
1868
+ chartData: import_prop_types12.default.object,
1869
+ /** ClassName tambahan. */
1870
+ className: import_prop_types12.default.string
1871
+ };
1872
+
1873
+ // src/presentation/organisms/FilterPanel.jsx
1874
+ var import_react15 = __toESM(require("react"), 1);
1875
+ var import_prop_types13 = __toESM(require("prop-types"), 1);
1876
+ var STATUS_OPTIONS = ["confirmed", "pending", "all"];
1877
+ var AUDIENCE_OPTIONS = ["", "domestic", "foreign"];
1878
+ var DAY_PRESETS = [7, 30, 90, 0];
1879
+ var SORT_BY_OPTIONS = ["bookings", "revenue"];
1880
+ var SORT_DIR_OPTIONS = ["desc", "asc"];
1881
+ function FilterPanel({
1882
+ filters,
1883
+ labels,
1884
+ onFilterChange,
1885
+ onResetFilters,
1886
+ className = ""
1887
+ }) {
1888
+ return /* @__PURE__ */ import_react15.default.createElement("div", { className: `flex flex-col gap-2 ${className}` }, /* @__PURE__ */ import_react15.default.createElement("div", { className: "grid grid-cols-1 lg:grid-cols-6 gap-2" }, /* @__PURE__ */ import_react15.default.createElement(
1889
+ "select",
1890
+ {
1891
+ className: "px-3 py-2 rounded-2xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-sm",
1892
+ value: filters.statusScope,
1893
+ onChange: (event) => onFilterChange("statusScope", event.target.value)
1894
+ },
1895
+ STATUS_OPTIONS.map((status) => /* @__PURE__ */ import_react15.default.createElement("option", { key: status, value: status }, status === "confirmed" ? labels.confirmedOnly : status === "pending" ? labels.pendingOnly : labels.allStatus))
1896
+ ), /* @__PURE__ */ import_react15.default.createElement("label", { className: "inline-flex items-center gap-2 px-3 py-2 rounded-2xl border border-slate-300 dark:border-slate-600" }, /* @__PURE__ */ import_react15.default.createElement(
1897
+ Input,
1898
+ {
1899
+ type: "checkbox",
1900
+ checked: filters.includePendingOverlay,
1901
+ onChange: (event) => onFilterChange("includePendingOverlay", event.target.checked)
1902
+ }
1903
+ ), /* @__PURE__ */ import_react15.default.createElement(Typography, { variant: "body" }, labels.showPendingOverlay)), /* @__PURE__ */ import_react15.default.createElement(
1904
+ "select",
1905
+ {
1906
+ className: "px-3 py-2 rounded-2xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-sm",
1907
+ value: filters.audience,
1908
+ onChange: (event) => onFilterChange("audience", event.target.value)
1909
+ },
1910
+ AUDIENCE_OPTIONS.map((audience) => /* @__PURE__ */ import_react15.default.createElement("option", { key: audience || "all", value: audience }, audience === "domestic" ? labels.audienceDomestic : audience === "foreign" ? labels.audienceForeign : labels.allAudience))
1911
+ ), /* @__PURE__ */ import_react15.default.createElement(
1912
+ "select",
1913
+ {
1914
+ className: "px-3 py-2 rounded-2xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-sm",
1915
+ value: String(filters.daysPreset),
1916
+ onChange: (event) => onFilterChange("daysPreset", Number(event.target.value))
1917
+ },
1918
+ DAY_PRESETS.map((days) => /* @__PURE__ */ import_react15.default.createElement("option", { key: String(days), value: String(days) }, days === 0 ? labels.customDate : labels.dayLabel(days)))
1919
+ ), /* @__PURE__ */ import_react15.default.createElement(
1920
+ DateRangeFilter,
1921
+ {
1922
+ dateFrom: filters.dateFrom || "",
1923
+ dateTo: filters.dateTo || "",
1924
+ onRangeChange: ({ dateFrom, dateTo }) => {
1925
+ if (dateFrom !== filters.dateFrom) onFilterChange("dateFrom", dateFrom);
1926
+ if (dateTo !== filters.dateTo) onFilterChange("dateTo", dateTo);
1927
+ },
1928
+ disabled: filters.daysPreset !== 0,
1929
+ className: "lg:col-span-2"
1930
+ }
1931
+ )), /* @__PURE__ */ import_react15.default.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ import_react15.default.createElement(Button, { variant: "secondary", size: "sm", onClick: onResetFilters }, labels.reset), /* @__PURE__ */ import_react15.default.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ import_react15.default.createElement(Typography, { variant: "caption" }, labels.topSort, ":"), /* @__PURE__ */ import_react15.default.createElement(
1932
+ "select",
1933
+ {
1934
+ className: "px-2 py-1.5 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-sm",
1935
+ value: filters.sortPkgBy,
1936
+ onChange: (event) => onFilterChange("sortPkgBy", event.target.value)
1937
+ },
1938
+ SORT_BY_OPTIONS.map((sortBy) => /* @__PURE__ */ import_react15.default.createElement("option", { key: sortBy, value: sortBy }, sortBy === "bookings" ? labels.sortBookings : labels.sortRevenue))
1939
+ ), /* @__PURE__ */ import_react15.default.createElement(
1940
+ "select",
1941
+ {
1942
+ className: "px-2 py-1.5 rounded-xl border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-sm",
1943
+ value: filters.sortPkgDir,
1944
+ onChange: (event) => onFilterChange("sortPkgDir", event.target.value)
1945
+ },
1946
+ SORT_DIR_OPTIONS.map((direction) => /* @__PURE__ */ import_react15.default.createElement("option", { key: direction, value: direction }, direction === "desc" ? labels.sortDesc : labels.sortAsc))
1947
+ ))));
1948
+ }
1949
+ FilterPanel.propTypes = {
1950
+ /** State filter saat ini. */
1951
+ filters: import_prop_types13.default.shape({
1952
+ statusScope: import_prop_types13.default.string,
1953
+ includePendingOverlay: import_prop_types13.default.bool,
1954
+ audience: import_prop_types13.default.string,
1955
+ daysPreset: import_prop_types13.default.number,
1956
+ dateFrom: import_prop_types13.default.string,
1957
+ dateTo: import_prop_types13.default.string,
1958
+ sortPkgBy: import_prop_types13.default.string,
1959
+ sortPkgDir: import_prop_types13.default.string
1960
+ }).isRequired,
1961
+ /** Objek label i18n. */
1962
+ labels: import_prop_types13.default.object.isRequired,
1963
+ /** Callback saat filter berubah (field, value). */
1964
+ onFilterChange: import_prop_types13.default.func.isRequired,
1965
+ /** Callback saat filter di-reset. */
1966
+ onResetFilters: import_prop_types13.default.func.isRequired,
1967
+ /** ClassName tambahan. */
1968
+ className: import_prop_types13.default.string
1969
+ };
1970
+
1971
+ // src/presentation/organisms/SidebarNavigation.jsx
1972
+ var import_react16 = __toESM(require("react"), 1);
1973
+ var import_prop_types14 = __toESM(require("prop-types"), 1);
1974
+ function SidebarNavigation({
1975
+ items = [],
1976
+ activeItem = "",
1977
+ onItemClick,
1978
+ logo = null,
1979
+ className = ""
1980
+ }) {
1981
+ return /* @__PURE__ */ import_react16.default.createElement(
1982
+ "aside",
1983
+ {
1984
+ className: `flex flex-col w-60 min-h-screen bg-white dark:bg-slate-900 border-r border-slate-200 dark:border-slate-800 ${className}`
1985
+ },
1986
+ logo ? /* @__PURE__ */ import_react16.default.createElement("div", { className: "px-4 py-4 border-b border-slate-100 dark:border-slate-800" }, logo) : null,
1987
+ /* @__PURE__ */ import_react16.default.createElement("nav", { className: "flex-1 px-2 py-3 space-y-0.5", "aria-label": "Sidebar navigation" }, items.map((item) => {
1988
+ const isActive = item.id === activeItem;
1989
+ return /* @__PURE__ */ import_react16.default.createElement(
1990
+ "button",
1991
+ {
1992
+ key: item.id,
1993
+ type: "button",
1994
+ id: `sidebar-nav-${item.id}`,
1995
+ onClick: () => onItemClick == null ? void 0 : onItemClick(item.id),
1996
+ "aria-current": isActive ? "page" : void 0,
1997
+ className: [
1998
+ "w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors text-left",
1999
+ isActive ? "bg-blue-50 dark:bg-blue-950/60 text-blue-700 dark:text-blue-300" : "text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-slate-100"
2000
+ ].join(" ")
2001
+ },
2002
+ item.icon ? /* @__PURE__ */ import_react16.default.createElement(
2003
+ Icon,
2004
+ {
2005
+ name: item.icon,
2006
+ size: 18,
2007
+ className: isActive ? "text-blue-600 dark:text-blue-400" : "text-slate-400 dark:text-slate-500"
2008
+ }
2009
+ ) : null,
2010
+ /* @__PURE__ */ import_react16.default.createElement("span", null, item.label),
2011
+ isActive ? /* @__PURE__ */ import_react16.default.createElement("span", { className: "ml-auto w-1.5 h-1.5 rounded-full bg-blue-500" }) : null
2012
+ );
2013
+ }))
2014
+ );
2015
+ }
2016
+ SidebarNavigation.propTypes = {
2017
+ /** Daftar item menu navigasi. */
2018
+ items: import_prop_types14.default.arrayOf(
2019
+ import_prop_types14.default.shape({
2020
+ id: import_prop_types14.default.string.isRequired,
2021
+ label: import_prop_types14.default.string.isRequired,
2022
+ icon: import_prop_types14.default.string
2023
+ })
2024
+ ),
2025
+ /** ID item yang sedang aktif. */
2026
+ activeItem: import_prop_types14.default.string,
2027
+ /** Callback saat item diklik, menerima id item sebagai argumen. */
2028
+ onItemClick: import_prop_types14.default.func,
2029
+ /** Konten logo/brand di atas sidebar. */
2030
+ logo: import_prop_types14.default.node,
2031
+ /** ClassName tambahan. */
2032
+ className: import_prop_types14.default.string
2033
+ };
2034
+
2035
+ // src/presentation/organisms/TopbarHeader.jsx
2036
+ var import_react17 = __toESM(require("react"), 1);
2037
+ var import_prop_types15 = __toESM(require("prop-types"), 1);
2038
+ function TopbarHeader({
2039
+ title = "",
2040
+ actions = null,
2041
+ leading = null,
2042
+ className = ""
2043
+ }) {
2044
+ return /* @__PURE__ */ import_react17.default.createElement(
2045
+ "header",
2046
+ {
2047
+ className: `sticky top-0 z-30 flex items-center justify-between h-14 px-4 bg-white/80 dark:bg-slate-900/80 backdrop-blur-md border-b border-slate-200 dark:border-slate-800 ${className}`
2048
+ },
2049
+ /* @__PURE__ */ import_react17.default.createElement("div", { className: "flex items-center gap-3" }, leading, title ? /* @__PURE__ */ import_react17.default.createElement(Typography, { variant: "h2", className: "truncate" }, title) : null),
2050
+ actions ? /* @__PURE__ */ import_react17.default.createElement("div", { className: "flex items-center gap-2" }, actions) : null
2051
+ );
2052
+ }
2053
+ TopbarHeader.propTypes = {
2054
+ /** Judul halaman yang ditampilkan. */
2055
+ title: import_prop_types15.default.string,
2056
+ /** Elemen aksi di sisi kanan (tombol, avatar, dsb.). */
2057
+ actions: import_prop_types15.default.node,
2058
+ /** Elemen di sisi kiri sebelum judul (misal: hamburger menu). */
2059
+ leading: import_prop_types15.default.node,
2060
+ /** ClassName tambahan. */
2061
+ className: import_prop_types15.default.string
2062
+ };
2063
+
2064
+ // src/presentation/templates/DashboardLayout.jsx
2065
+ var import_react18 = __toESM(require("react"), 1);
2066
+ var import_prop_types16 = __toESM(require("prop-types"), 1);
2067
+ function DashboardLayout({
2068
+ header = null,
2069
+ sidebar = null,
2070
+ children,
2071
+ className = ""
2072
+ }) {
2073
+ return /* @__PURE__ */ import_react18.default.createElement("div", { className: `min-h-screen bg-slate-50 dark:bg-slate-900 ${className}` }, header, /* @__PURE__ */ import_react18.default.createElement("div", { className: "flex" }, sidebar, /* @__PURE__ */ import_react18.default.createElement("main", { className: "flex-1 container mx-auto px-4 py-3 space-y-4" }, children)));
2074
+ }
2075
+ DashboardLayout.propTypes = {
2076
+ /** Konten header atas. */
2077
+ header: import_prop_types16.default.node,
2078
+ /** Konten sidebar. */
2079
+ sidebar: import_prop_types16.default.node,
2080
+ /** Konten utama dashboard. */
2081
+ children: import_prop_types16.default.node.isRequired,
2082
+ /** ClassName tambahan. */
2083
+ className: import_prop_types16.default.string
2084
+ };
2085
+
2086
+ // src/presentation/ReusableDashboardView.jsx
2087
+ var import_react19 = __toESM(require("react"), 1);
2088
+ var import_prop_types17 = __toESM(require("prop-types"), 1);
2089
+ function ReusableDashboardView({
2090
+ config,
2091
+ labels,
2092
+ loading,
2093
+ error,
2094
+ filters,
2095
+ onFilterChange,
2096
+ onResetFilters,
2097
+ onRefresh,
2098
+ data,
2099
+ dateLocale,
2100
+ liveUpdateEnabled
2101
+ }) {
2102
+ return /* @__PURE__ */ import_react19.default.createElement("div", { className: "container mt-3 space-y-4" }, /* @__PURE__ */ import_react19.default.createElement("div", null, /* @__PURE__ */ import_react19.default.createElement("div", { className: "rounded-2xl border border-slate-200/60 dark:border-slate-800/60 backdrop-blur-md px-3 sm:px-4 py-2 glass shadow-smooth" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "flex flex-col gap-2" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "flex flex-wrap items-center justify-between gap-2" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ import_react19.default.createElement(Typography, { variant: "h1" }, labels.title), liveUpdateEnabled ? /* @__PURE__ */ import_react19.default.createElement(Badge, { status: "success" }, labels.liveUpdate) : null), /* @__PURE__ */ import_react19.default.createElement(
2103
+ Button,
2104
+ {
2105
+ variant: "secondary",
2106
+ size: "sm",
2107
+ onClick: () => onRefresh(),
2108
+ title: labels.refresh
2109
+ },
2110
+ /* @__PURE__ */ import_react19.default.createElement(Icon, { name: "RotateCcw", size: 16 })
2111
+ )), error ? /* @__PURE__ */ import_react19.default.createElement("div", { className: "rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-300" }, /* @__PURE__ */ import_react19.default.createElement("div", { className: "flex items-center justify-between gap-3" }, /* @__PURE__ */ import_react19.default.createElement("span", null, error), /* @__PURE__ */ import_react19.default.createElement(Button, { variant: "secondary", size: "sm", onClick: () => onRefresh() }, labels.retry))) : null, /* @__PURE__ */ import_react19.default.createElement(
2112
+ FilterPanel,
2113
+ {
2114
+ filters,
2115
+ labels,
2116
+ onFilterChange,
2117
+ onResetFilters
2118
+ }
2119
+ )))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" }, loading ? Array.from({ length: 4 }).map((_, index) => /* @__PURE__ */ import_react19.default.createElement(SkeletonLoader, { key: `stat-skeleton-${index}`, className: "h-28" })) : config.widgets.stats.map((widget) => /* @__PURE__ */ import_react19.default.createElement(
2120
+ StatCard,
2121
+ {
2122
+ key: widget.id,
2123
+ label: labels[widget.label] || widget.label,
2124
+ value: data.stats[widget.valueKey],
2125
+ icon: widget.icon,
2126
+ format: widget.format
2127
+ }
2128
+ ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "grid grid-cols-1 lg:grid-cols-2 gap-4" }, config.widgets.charts.map((widget) => /* @__PURE__ */ import_react19.default.createElement(
2129
+ ChartCard,
2130
+ {
2131
+ key: widget.id,
2132
+ widget,
2133
+ labels,
2134
+ loading,
2135
+ filters,
2136
+ chartData: data.charts
2137
+ }
2138
+ ))), /* @__PURE__ */ import_react19.default.createElement("div", { className: "card p-4" }, /* @__PURE__ */ import_react19.default.createElement(Typography, { variant: "h3", className: "flex items-center gap-2 mb-3" }, /* @__PURE__ */ import_react19.default.createElement(Icon, { name: config.widgets.table.icon, size: 18 }), labels[config.widgets.table.label] || config.widgets.table.label), loading ? /* @__PURE__ */ import_react19.default.createElement(SkeletonLoader, { className: "h-48" }) : /* @__PURE__ */ import_react19.default.createElement(
2139
+ DataTable,
2140
+ {
2141
+ columns: config.widgets.table.columns,
2142
+ data: data.table.recentBookings,
2143
+ labels,
2144
+ dateLocale,
2145
+ emptyLabel: labels[config.widgets.table.emptyLabel] || config.widgets.table.emptyLabel,
2146
+ searchable: true,
2147
+ sortable: true,
2148
+ pageSize: 10
2149
+ }
2150
+ )));
2151
+ }
2152
+ ReusableDashboardView.propTypes = {
2153
+ /** Konfigurasi widget dashboard. */
2154
+ config: import_prop_types17.default.object.isRequired,
2155
+ /** Objek label i18n. */
2156
+ labels: import_prop_types17.default.object.isRequired,
2157
+ /** Status loading data. */
2158
+ loading: import_prop_types17.default.bool,
2159
+ /** Pesan error jika ada. */
2160
+ error: import_prop_types17.default.string,
2161
+ /** State filter dashboard. */
2162
+ filters: import_prop_types17.default.object,
2163
+ /** Callback perubahan filter. */
2164
+ onFilterChange: import_prop_types17.default.func.isRequired,
2165
+ /** Callback reset filter. */
2166
+ onResetFilters: import_prop_types17.default.func.isRequired,
2167
+ /** Callback refresh data. */
2168
+ onRefresh: import_prop_types17.default.func.isRequired,
2169
+ /** Data dashboard. */
2170
+ data: import_prop_types17.default.object,
2171
+ /** Locale untuk format tanggal. */
2172
+ dateLocale: import_prop_types17.default.string,
2173
+ /** Apakah live update aktif. */
2174
+ liveUpdateEnabled: import_prop_types17.default.bool
2175
+ };
2176
+
2177
+ // src/utils/labels.js
2178
+ function createDashboardLabels(t) {
2179
+ const labels = {
2180
+ title: t("admin.dashboard.title", { defaultValue: "Dashboard" }),
2181
+ refresh: t("admin.dashboard.refresh", { defaultValue: "Refresh" }),
2182
+ liveUpdate: t("admin.dashboard.liveUpdate", { defaultValue: "Live update" }),
2183
+ loadFailed: t("admin.dashboard.loadFailed", {
2184
+ defaultValue: "Failed to load dashboard data."
2185
+ }),
2186
+ retry: t("admin.dashboard.retry", { defaultValue: "Retry" }),
2187
+ confirmedOnly: t("admin.dashboard.filters.confirmedOnly", {
2188
+ defaultValue: "Confirmed only"
2189
+ }),
2190
+ pendingOnly: t("admin.dashboard.filters.pendingOnly", {
2191
+ defaultValue: "Pending only"
2192
+ }),
2193
+ allStatus: t("admin.dashboard.filters.allStatus", {
2194
+ defaultValue: "All status"
2195
+ }),
2196
+ showPendingOverlay: t("admin.dashboard.filters.showPendingOverlay", {
2197
+ defaultValue: "Show pending overlay"
2198
+ }),
2199
+ allAudience: t("admin.dashboard.filters.allAudience", {
2200
+ defaultValue: "All audience"
2201
+ }),
2202
+ audienceDomestic: t("explore.domestic", { defaultValue: "Domestic" }),
2203
+ audienceForeign: t("explore.foreign", { defaultValue: "Foreign" }),
2204
+ customDate: t("admin.dashboard.filters.customDate", {
2205
+ defaultValue: "Custom"
2206
+ }),
2207
+ reset: t("admin.dashboard.filters.reset", { defaultValue: "Reset" }),
2208
+ topSort: t("admin.dashboard.filters.topSort", {
2209
+ defaultValue: "Top packages sort"
2210
+ }),
2211
+ sortBookings: t("admin.dashboard.filters.sortBookings", {
2212
+ defaultValue: "Bookings"
2213
+ }),
2214
+ sortRevenue: t("admin.dashboard.filters.sortRevenue", {
2215
+ defaultValue: "Revenue"
2216
+ }),
2217
+ sortDesc: t("admin.dashboard.filters.sortDesc", { defaultValue: "Desc" }),
2218
+ sortAsc: t("admin.dashboard.filters.sortAsc", { defaultValue: "Asc" }),
2219
+ confirmedBookings: t("admin.dashboard.cards.confirmedBookings", {
2220
+ defaultValue: "Confirmed Bookings"
2221
+ }),
2222
+ confirmedRevenue: t("admin.dashboard.cards.confirmedRevenue", {
2223
+ defaultValue: "Revenue (Confirmed)"
2224
+ }),
2225
+ avgRevenue: t("admin.dashboard.cards.avgRevenue", {
2226
+ defaultValue: "Avg Revenue / Booking"
2227
+ }),
2228
+ conversionRate: t("admin.dashboard.cards.conversionRate", {
2229
+ defaultValue: "Conversion Rate"
2230
+ }),
2231
+ totalProducts: t("admin.dashboard.cards.totalProducts", {
2232
+ defaultValue: "Total Products"
2233
+ }),
2234
+ totalCustomers: t("admin.dashboard.cards.totalCustomers", {
2235
+ defaultValue: "Total Customers"
2236
+ }),
2237
+ dailyTrends: t("admin.dashboard.sections.dailyTrends", {
2238
+ defaultValue: "Daily Trends"
2239
+ }),
2240
+ statusDistribution: t("admin.dashboard.sections.statusDistribution", {
2241
+ defaultValue: "Status Distribution"
2242
+ }),
2243
+ audienceDistribution: t("admin.dashboard.sections.audienceDistribution", {
2244
+ defaultValue: "Audience Distribution"
2245
+ }),
2246
+ topPackages: t("admin.dashboard.sections.topPackages", {
2247
+ defaultValue: "Top Packages"
2248
+ }),
2249
+ recentBookings: t("admin.dashboard.sections.recentBookings", {
2250
+ defaultValue: "Recent Bookings"
2251
+ }),
2252
+ date: t("admin.dashboard.table.date", { defaultValue: "Date" }),
2253
+ customer: t("admin.dashboard.table.customer", { defaultValue: "Customer" }),
2254
+ package: t("admin.dashboard.table.package", { defaultValue: "Package" }),
2255
+ audience: t("admin.dashboard.table.audience", { defaultValue: "Audience" }),
2256
+ total: t("admin.dashboard.table.total", { defaultValue: "Total" }),
2257
+ status: t("admin.dashboard.table.status", { defaultValue: "Status" }),
2258
+ noRecentBookings: t("admin.dashboard.emptyRecent", {
2259
+ defaultValue: "No recent bookings"
2260
+ }),
2261
+ bookingsMetric: t("admin.dashboard.metrics.bookings", {
2262
+ defaultValue: "Bookings"
2263
+ }),
2264
+ revenueMetric: t("admin.dashboard.metrics.revenue", {
2265
+ defaultValue: "Revenue"
2266
+ }),
2267
+ pendingMetric: t("admin.dashboard.metrics.pendingBookings", {
2268
+ defaultValue: "Bookings (Pending)"
2269
+ }),
2270
+ confirmedBookingMetric: t("admin.dashboard.metrics.confirmedBookings", {
2271
+ defaultValue: "Bookings (Confirmed)"
2272
+ }),
2273
+ confirmedRevenueMetric: t("admin.dashboard.metrics.confirmedRevenue", {
2274
+ defaultValue: "Revenue (Confirmed)"
2275
+ }),
2276
+ unknownAudience: t("admin.dashboard.filters.unknownAudience", {
2277
+ defaultValue: "Unknown"
2278
+ })
2279
+ };
2280
+ labels.dayLabel = (count) => t("admin.dashboard.filters.dayWindow", {
2281
+ count,
2282
+ defaultValue: "{{count}} days"
2283
+ });
2284
+ labels.formatStatusLabel = (status) => {
2285
+ const normalized = String(status || "pending").toLowerCase();
2286
+ const defaultValue = normalized.charAt(0).toUpperCase() + normalized.slice(1);
2287
+ return t(`admin.dashboard.status.${normalized}`, { defaultValue });
2288
+ };
2289
+ labels.formatAudienceLabel = (value) => {
2290
+ if (value === "domestic") return labels.audienceDomestic;
2291
+ if (value === "foreign") return labels.audienceForeign;
2292
+ return labels.unknownAudience;
2293
+ };
2294
+ return labels;
2295
+ }
2296
+ // Annotate the CommonJS export names for ESM import in node:
2297
+ 0 && (module.exports = {
2298
+ Badge,
2299
+ Button,
2300
+ ChartCard,
2301
+ ChartHeader,
2302
+ DashboardLayout,
2303
+ DataTable,
2304
+ DateRangeFilter,
2305
+ FilterPanel,
2306
+ Icon,
2307
+ Input,
2308
+ ReusableDashboardView,
2309
+ SearchBar,
2310
+ SidebarNavigation,
2311
+ SkeletonLoader,
2312
+ StatCard,
2313
+ TopbarHeader,
2314
+ Typography,
2315
+ adaptCidikaDashboardData,
2316
+ adaptDummyUmkmData,
2317
+ adaptTokoSepatuData,
2318
+ buildDayBuckets,
2319
+ cidikaWidgetConfig,
2320
+ createCidikaSupabaseSource,
2321
+ createDashboardLabels,
2322
+ createDefaultFilters,
2323
+ createEmptyDashboardData,
2324
+ createEmptyDummyUmkmData,
2325
+ createEmptyTokoSepatuData,
2326
+ createTokoSepatuSupabaseSource,
2327
+ dummyUmkmWidgetConfig,
2328
+ formatDate,
2329
+ formatIDR,
2330
+ formatYYYYMMDD,
2331
+ resolveDateRange,
2332
+ resolveIcon,
2333
+ shortId,
2334
+ sortMapEntries,
2335
+ toNumber,
2336
+ tokoSepatuWidgetConfig,
2337
+ useRealtimeUpdate,
2338
+ useReusableDashboard
2339
+ });
2340
+ //# sourceMappingURL=index.cjs.map