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