@open-mercato/core 0.4.2-canary-10c7a8bf2a → 0.4.2-canary-e6bf6a353e

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.
Files changed (136) hide show
  1. package/dist/modules/auth/lib/setup-app.js +2 -0
  2. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  3. package/dist/modules/catalog/analytics.js +27 -0
  4. package/dist/modules/catalog/analytics.js.map +7 -0
  5. package/dist/modules/customers/analytics.js +50 -0
  6. package/dist/modules/customers/analytics.js.map +7 -0
  7. package/dist/modules/dashboards/acl.js +2 -1
  8. package/dist/modules/dashboards/acl.js.map +2 -2
  9. package/dist/modules/dashboards/api/widgets/data/route.js +187 -0
  10. package/dist/modules/dashboards/api/widgets/data/route.js.map +7 -0
  11. package/dist/modules/dashboards/cli.js +142 -1
  12. package/dist/modules/dashboards/cli.js.map +2 -2
  13. package/dist/modules/dashboards/di.js +11 -0
  14. package/dist/modules/dashboards/di.js.map +7 -0
  15. package/dist/modules/dashboards/lib/aggregations.js +162 -0
  16. package/dist/modules/dashboards/lib/aggregations.js.map +7 -0
  17. package/dist/modules/dashboards/lib/formatters.js +34 -0
  18. package/dist/modules/dashboards/lib/formatters.js.map +7 -0
  19. package/dist/modules/dashboards/seed/analytics.js +383 -0
  20. package/dist/modules/dashboards/seed/analytics.js.map +7 -0
  21. package/dist/modules/dashboards/services/analyticsRegistry.js +52 -0
  22. package/dist/modules/dashboards/services/analyticsRegistry.js.map +7 -0
  23. package/dist/modules/dashboards/services/widgetDataService.js +207 -0
  24. package/dist/modules/dashboards/services/widgetDataService.js.map +7 -0
  25. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js +18 -0
  26. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js.map +7 -0
  27. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +128 -0
  28. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +7 -0
  29. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js +25 -0
  30. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js.map +7 -0
  31. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js +18 -0
  32. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js.map +7 -0
  33. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +126 -0
  34. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +7 -0
  35. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js +25 -0
  36. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js.map +7 -0
  37. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js +18 -0
  38. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js.map +7 -0
  39. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +151 -0
  40. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +7 -0
  41. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js +25 -0
  42. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js.map +7 -0
  43. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js +18 -0
  44. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js.map +7 -0
  45. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +126 -0
  46. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +7 -0
  47. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js +25 -0
  48. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js.map +7 -0
  49. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +16 -0
  50. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +7 -0
  51. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +123 -0
  52. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +7 -0
  53. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +25 -0
  54. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +7 -0
  55. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js +18 -0
  56. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js.map +7 -0
  57. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +128 -0
  58. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +7 -0
  59. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js +25 -0
  60. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js.map +7 -0
  61. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js +21 -0
  62. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js.map +7 -0
  63. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +211 -0
  64. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +7 -0
  65. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js +25 -0
  66. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js.map +7 -0
  67. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js +19 -0
  68. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js.map +7 -0
  69. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +131 -0
  70. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +7 -0
  71. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js +25 -0
  72. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js.map +7 -0
  73. package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js +19 -0
  74. package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js.map +7 -0
  75. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +153 -0
  76. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +7 -0
  77. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js +25 -0
  78. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js.map +7 -0
  79. package/dist/modules/dashboards/widgets/dashboard/top-products/config.js +22 -0
  80. package/dist/modules/dashboards/widgets/dashboard/top-products/config.js.map +7 -0
  81. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +180 -0
  82. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +7 -0
  83. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js +25 -0
  84. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js.map +7 -0
  85. package/dist/modules/sales/analytics.js +67 -0
  86. package/dist/modules/sales/analytics.js.map +7 -0
  87. package/package.json +2 -2
  88. package/src/modules/auth/lib/setup-app.ts +2 -0
  89. package/src/modules/catalog/analytics.ts +24 -0
  90. package/src/modules/customers/analytics.ts +47 -0
  91. package/src/modules/dashboards/acl.ts +1 -0
  92. package/src/modules/dashboards/api/widgets/data/route.ts +221 -0
  93. package/src/modules/dashboards/cli.ts +164 -1
  94. package/src/modules/dashboards/di.ts +9 -0
  95. package/src/modules/dashboards/i18n/de.json +115 -1
  96. package/src/modules/dashboards/i18n/en.json +115 -1
  97. package/src/modules/dashboards/i18n/es.json +115 -1
  98. package/src/modules/dashboards/i18n/pl.json +115 -1
  99. package/src/modules/dashboards/lib/__tests__/aggregations.test.ts +327 -0
  100. package/src/modules/dashboards/lib/__tests__/formatters.test.ts +128 -0
  101. package/src/modules/dashboards/lib/aggregations.ts +225 -0
  102. package/src/modules/dashboards/lib/formatters.ts +36 -0
  103. package/src/modules/dashboards/seed/analytics.ts +405 -0
  104. package/src/modules/dashboards/services/analyticsRegistry.ts +79 -0
  105. package/src/modules/dashboards/services/widgetDataService.ts +329 -0
  106. package/src/modules/dashboards/widgets/dashboard/aov-kpi/config.ts +20 -0
  107. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +135 -0
  108. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.ts +24 -0
  109. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/config.ts +20 -0
  110. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +133 -0
  111. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.ts +24 -0
  112. package/src/modules/dashboards/widgets/dashboard/orders-by-status/config.ts +20 -0
  113. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +154 -0
  114. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.ts +24 -0
  115. package/src/modules/dashboards/widgets/dashboard/orders-kpi/config.ts +20 -0
  116. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +133 -0
  117. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.ts +24 -0
  118. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +17 -0
  119. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +137 -0
  120. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +24 -0
  121. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/config.ts +20 -0
  122. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +135 -0
  123. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.ts +24 -0
  124. package/src/modules/dashboards/widgets/dashboard/revenue-trend/config.ts +24 -0
  125. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +220 -0
  126. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.ts +24 -0
  127. package/src/modules/dashboards/widgets/dashboard/sales-by-region/config.ts +21 -0
  128. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +131 -0
  129. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.ts +24 -0
  130. package/src/modules/dashboards/widgets/dashboard/top-customers/config.ts +21 -0
  131. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +161 -0
  132. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.ts +24 -0
  133. package/src/modules/dashboards/widgets/dashboard/top-products/config.ts +27 -0
  134. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +181 -0
  135. package/src/modules/dashboards/widgets/dashboard/top-products/widget.ts +24 -0
  136. package/src/modules/sales/analytics.ts +64 -0
@@ -0,0 +1,131 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { BarChart } from "@open-mercato/ui/backend/charts";
7
+ import { DateRangeSelect } from "@open-mercato/ui/backend/date-range";
8
+ import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
9
+ import { formatCurrencyCompact } from "../../../lib/formatters.js";
10
+ async function fetchSalesByRegionData(settings) {
11
+ const body = {
12
+ entityType: "sales:orders",
13
+ metric: {
14
+ field: "grandTotalGrossAmount",
15
+ aggregate: "sum"
16
+ },
17
+ groupBy: {
18
+ field: "shippingAddressSnapshot.region",
19
+ limit: settings.limit
20
+ },
21
+ dateRange: {
22
+ field: "placedAt",
23
+ preset: settings.dateRange
24
+ }
25
+ };
26
+ const call = await apiCall("/api/dashboards/widgets/data", {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/json" },
29
+ body: JSON.stringify(body)
30
+ });
31
+ if (!call.ok) {
32
+ const errorMsg = call.result?.error;
33
+ throw new Error(typeof errorMsg === "string" ? errorMsg : "Failed to fetch sales by region data");
34
+ }
35
+ return call.result;
36
+ }
37
+ const SalesByRegionWidget = ({
38
+ mode,
39
+ settings = DEFAULT_SETTINGS,
40
+ onSettingsChange,
41
+ refreshToken,
42
+ onRefreshStateChange
43
+ }) => {
44
+ const t = useT();
45
+ const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
46
+ const [data, setData] = React.useState([]);
47
+ const [loading, setLoading] = React.useState(true);
48
+ const [error, setError] = React.useState(null);
49
+ const refresh = React.useCallback(async () => {
50
+ onRefreshStateChange?.(true);
51
+ setLoading(true);
52
+ setError(null);
53
+ try {
54
+ const result = await fetchSalesByRegionData(hydrated);
55
+ const chartData = result.data.map((item) => ({
56
+ region: String(item.groupKey || t("dashboards.analytics.labels.unknown", "Unknown")),
57
+ Revenue: item.value ?? 0
58
+ }));
59
+ setData(chartData);
60
+ } catch (err) {
61
+ console.error("Failed to load sales by region data", err);
62
+ setError(t("dashboards.analytics.widgets.salesByRegion.error", "Failed to load data"));
63
+ } finally {
64
+ setLoading(false);
65
+ onRefreshStateChange?.(false);
66
+ }
67
+ }, [hydrated, onRefreshStateChange, t]);
68
+ React.useEffect(() => {
69
+ refresh().catch(() => {
70
+ });
71
+ }, [refresh, refreshToken]);
72
+ if (mode === "settings") {
73
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-sm", children: [
74
+ /* @__PURE__ */ jsx(
75
+ DateRangeSelect,
76
+ {
77
+ id: "sales-by-region-date-range",
78
+ label: t("dashboards.analytics.settings.dateRange", "Date Range"),
79
+ value: hydrated.dateRange,
80
+ onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
81
+ }
82
+ ),
83
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
84
+ /* @__PURE__ */ jsx(
85
+ "label",
86
+ {
87
+ htmlFor: "sales-by-region-limit",
88
+ className: "text-xs font-semibold uppercase text-muted-foreground",
89
+ children: t("dashboards.analytics.settings.limit", "Number of items")
90
+ }
91
+ ),
92
+ /* @__PURE__ */ jsx(
93
+ "input",
94
+ {
95
+ id: "sales-by-region-limit",
96
+ type: "number",
97
+ min: 1,
98
+ max: 20,
99
+ className: "w-24 rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary",
100
+ value: hydrated.limit,
101
+ onChange: (e) => {
102
+ const next = Number(e.target.value);
103
+ onSettingsChange({ ...hydrated, limit: Number.isFinite(next) ? next : hydrated.limit });
104
+ }
105
+ }
106
+ )
107
+ ] })
108
+ ] });
109
+ }
110
+ return /* @__PURE__ */ jsx(
111
+ BarChart,
112
+ {
113
+ data,
114
+ index: "region",
115
+ categories: ["Revenue"],
116
+ categoryLabels: { Revenue: t("dashboards.analytics.widgets.topCustomers.column.revenue", "Revenue") },
117
+ loading,
118
+ error,
119
+ layout: "horizontal",
120
+ valueFormatter: formatCurrencyCompact,
121
+ colors: ["cyan"],
122
+ showLegend: false,
123
+ emptyMessage: t("dashboards.analytics.widgets.salesByRegion.empty", "No regional sales data for this period")
124
+ }
125
+ );
126
+ };
127
+ var widget_client_default = SalesByRegionWidget;
128
+ export {
129
+ widget_client_default as default
130
+ };
131
+ //# sourceMappingURL=widget.client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport { DateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'\nimport { DEFAULT_SETTINGS, hydrateSettings, type SalesByRegionSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyCompact } from '../../../lib/formatters'\n\nasync function fetchSalesByRegionData(settings: SalesByRegionSettings): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:orders',\n metric: {\n field: 'grandTotalGrossAmount',\n aggregate: 'sum',\n },\n groupBy: {\n field: 'shippingAddressSnapshot.region',\n limit: settings.limit,\n },\n dateRange: {\n field: 'placedAt',\n preset: settings.dateRange,\n },\n }\n\n const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n\n if (!call.ok) {\n const errorMsg = (call.result as Record<string, unknown>)?.error\n throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch sales by region data')\n }\n\n return call.result as WidgetDataResponse\n}\n\nconst SalesByRegionWidget: React.FC<DashboardWidgetComponentProps<SalesByRegionSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchSalesByRegionData(hydrated)\n const chartData = result.data.map((item) => ({\n region: String(item.groupKey || t('dashboards.analytics.labels.unknown', 'Unknown')),\n Revenue: item.value ?? 0,\n }))\n setData(chartData)\n } catch (err) {\n console.error('Failed to load sales by region data', err)\n setError(t('dashboards.analytics.widgets.salesByRegion.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"sales-by-region-date-range\"\n label={t('dashboards.analytics.settings.dateRange', 'Date Range')}\n value={hydrated.dateRange}\n onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}\n />\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"sales-by-region-limit\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.limit', 'Number of items')}\n </label>\n <input\n id=\"sales-by-region-limit\"\n type=\"number\"\n min={1}\n max={20}\n className=\"w-24 rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={hydrated.limit}\n onChange={(e) => {\n const next = Number(e.target.value)\n onSettingsChange({ ...hydrated, limit: Number.isFinite(next) ? next : hydrated.limit })\n }}\n />\n </div>\n </div>\n )\n }\n\n return (\n <BarChart\n data={data}\n index=\"region\"\n categories={['Revenue']}\n categoryLabels={{ Revenue: t('dashboards.analytics.widgets.topCustomers.column.revenue', 'Revenue') }}\n loading={loading}\n error={error}\n layout=\"horizontal\"\n valueFormatter={formatCurrencyCompact}\n colors={['cyan']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.salesByRegion.empty', 'No regional sales data for this period')}\n />\n )\n}\n\nexport default SalesByRegionWidget\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,25 @@
1
+ import SalesByRegionWidget from "./widget.client.js";
2
+ import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
3
+ const widget = {
4
+ metadata: {
5
+ id: "dashboards.analytics.salesByRegion",
6
+ title: "Sales by Region",
7
+ description: "Revenue distribution by shipping region",
8
+ features: ["analytics.view", "sales.orders.view"],
9
+ defaultSize: "md",
10
+ defaultEnabled: false,
11
+ defaultSettings: DEFAULT_SETTINGS,
12
+ tags: ["analytics", "sales", "geography", "chart"],
13
+ category: "analytics",
14
+ icon: "map-pin",
15
+ supportsRefresh: true
16
+ },
17
+ Widget: SalesByRegionWidget,
18
+ hydrateSettings,
19
+ dehydrateSettings: (s) => ({ dateRange: s.dateRange, limit: s.limit })
20
+ };
21
+ var widget_default = widget;
22
+ export {
23
+ widget_default as default
24
+ };
25
+ //# sourceMappingURL=widget.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/sales-by-region/widget.ts"],
4
+ "sourcesContent": ["import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport SalesByRegionWidget from './widget.client'\nimport { DEFAULT_SETTINGS, hydrateSettings, type SalesByRegionSettings } from './config'\n\nconst widget: DashboardWidgetModule<SalesByRegionSettings> = {\n metadata: {\n id: 'dashboards.analytics.salesByRegion',\n title: 'Sales by Region',\n description: 'Revenue distribution by shipping region',\n features: ['analytics.view', 'sales.orders.view'],\n defaultSize: 'md',\n defaultEnabled: false,\n defaultSettings: DEFAULT_SETTINGS,\n tags: ['analytics', 'sales', 'geography', 'chart'],\n category: 'analytics',\n icon: 'map-pin',\n supportsRefresh: true,\n },\n Widget: SalesByRegionWidget,\n hydrateSettings,\n dehydrateSettings: (s) => ({ dateRange: s.dateRange, limit: s.limit }),\n}\n\nexport default widget\n"],
5
+ "mappings": "AACA,OAAO,yBAAyB;AAChC,SAAS,kBAAkB,uBAAmD;AAE9E,MAAM,SAAuD;AAAA,EAC3D,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,kBAAkB,mBAAmB;AAAA,IAChD,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,MAAM,CAAC,aAAa,SAAS,aAAa,OAAO;AAAA,IACjD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,mBAAmB,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,OAAO,EAAE,MAAM;AACtE;AAEA,IAAO,iBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,19 @@
1
+ import { isValidDateRangePreset } from "@open-mercato/ui/backend/date-range";
2
+ const DEFAULT_SETTINGS = {
3
+ dateRange: "this_month",
4
+ limit: 10
5
+ };
6
+ function hydrateSettings(raw) {
7
+ if (!raw || typeof raw !== "object") return { ...DEFAULT_SETTINGS };
8
+ const obj = raw;
9
+ const parsedLimit = Number(obj.limit);
10
+ return {
11
+ dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
12
+ limit: Number.isFinite(parsedLimit) && parsedLimit >= 1 && parsedLimit <= 20 ? Math.floor(parsedLimit) : DEFAULT_SETTINGS.limit
13
+ };
14
+ }
15
+ export {
16
+ DEFAULT_SETTINGS,
17
+ hydrateSettings
18
+ };
19
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/top-customers/config.ts"],
4
+ "sourcesContent": ["import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'\n\nexport type TopCustomersSettings = {\n dateRange: DateRangePreset\n limit: number\n}\n\nexport const DEFAULT_SETTINGS: TopCustomersSettings = {\n dateRange: 'this_month',\n limit: 10,\n}\n\nexport function hydrateSettings(raw: unknown): TopCustomersSettings {\n if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }\n const obj = raw as Record<string, unknown>\n const parsedLimit = Number(obj.limit)\n return {\n dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,\n limit: Number.isFinite(parsedLimit) && parsedLimit >= 1 && parsedLimit <= 20 ? Math.floor(parsedLimit) : DEFAULT_SETTINGS.limit,\n }\n}\n"],
5
+ "mappings": "AAAA,SAA+B,8BAA8B;AAOtD,MAAM,mBAAyC;AAAA,EACpD,WAAW;AAAA,EACX,OAAO;AACT;AAEO,SAAS,gBAAgB,KAAoC;AAClE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,EAAE,GAAG,iBAAiB;AAClE,QAAM,MAAM;AACZ,QAAM,cAAc,OAAO,IAAI,KAAK;AACpC,SAAO;AAAA,IACL,WAAW,uBAAuB,IAAI,SAAS,IAAI,IAAI,YAAY,iBAAiB;AAAA,IACpF,OAAO,OAAO,SAAS,WAAW,KAAK,eAAe,KAAK,eAAe,KAAK,KAAK,MAAM,WAAW,IAAI,iBAAiB;AAAA,EAC5H;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,153 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { TopNTable } from "@open-mercato/ui/backend/charts";
7
+ import { DateRangeSelect } from "@open-mercato/ui/backend/date-range";
8
+ import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
9
+ import { formatCurrencySafe } from "../../../lib/formatters.js";
10
+ async function fetchTopCustomersData(settings) {
11
+ const body = {
12
+ entityType: "sales:orders",
13
+ metric: {
14
+ field: "grandTotalGrossAmount",
15
+ aggregate: "sum"
16
+ },
17
+ groupBy: {
18
+ field: "customerEntityId",
19
+ limit: settings.limit,
20
+ resolveLabels: true
21
+ },
22
+ dateRange: {
23
+ field: "placedAt",
24
+ preset: settings.dateRange
25
+ }
26
+ };
27
+ const call = await apiCall("/api/dashboards/widgets/data", {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify(body)
31
+ });
32
+ if (!call.ok) {
33
+ const errorMsg = call.result?.error;
34
+ throw new Error(typeof errorMsg === "string" ? errorMsg : "Failed to fetch top customers data");
35
+ }
36
+ return call.result;
37
+ }
38
+ function formatCustomerName(name, unknownLabel) {
39
+ if (!name) return unknownLabel;
40
+ return name;
41
+ }
42
+ const TopCustomersWidget = ({
43
+ mode,
44
+ settings = DEFAULT_SETTINGS,
45
+ onSettingsChange,
46
+ refreshToken,
47
+ onRefreshStateChange
48
+ }) => {
49
+ const t = useT();
50
+ const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
51
+ const [data, setData] = React.useState([]);
52
+ const [loading, setLoading] = React.useState(true);
53
+ const [error, setError] = React.useState(null);
54
+ const unknownLabel = t("dashboards.analytics.labels.unknown", "Unknown");
55
+ const columns = React.useMemo(
56
+ () => [
57
+ {
58
+ key: "rank",
59
+ header: "#",
60
+ width: "40px"
61
+ },
62
+ {
63
+ key: "customerId",
64
+ header: t("dashboards.analytics.widgets.topCustomers.column.customer", "Customer"),
65
+ formatter: (value) => formatCustomerName(String(value || ""), unknownLabel)
66
+ },
67
+ {
68
+ key: "revenue",
69
+ header: t("dashboards.analytics.widgets.topCustomers.column.revenue", "Revenue"),
70
+ align: "right",
71
+ formatter: (value) => formatCurrencySafe(value)
72
+ }
73
+ ],
74
+ [t, unknownLabel]
75
+ );
76
+ const refresh = React.useCallback(async () => {
77
+ onRefreshStateChange?.(true);
78
+ setLoading(true);
79
+ setError(null);
80
+ try {
81
+ const result = await fetchTopCustomersData(hydrated);
82
+ const tableData = result.data.map((item, index) => ({
83
+ rank: index + 1,
84
+ customerId: item.groupLabel || String(item.groupKey || t("dashboards.analytics.labels.unknown", "Unknown")),
85
+ revenue: item.value ?? 0
86
+ }));
87
+ setData(tableData);
88
+ } catch (err) {
89
+ console.error("Failed to load top customers data", err);
90
+ setError(t("dashboards.analytics.widgets.topCustomers.error", "Failed to load data"));
91
+ } finally {
92
+ setLoading(false);
93
+ onRefreshStateChange?.(false);
94
+ }
95
+ }, [hydrated, onRefreshStateChange, t]);
96
+ React.useEffect(() => {
97
+ refresh().catch(() => {
98
+ });
99
+ }, [refresh, refreshToken]);
100
+ if (mode === "settings") {
101
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-sm", children: [
102
+ /* @__PURE__ */ jsx(
103
+ DateRangeSelect,
104
+ {
105
+ id: "top-customers-date-range",
106
+ label: t("dashboards.analytics.settings.dateRange", "Date Range"),
107
+ value: hydrated.dateRange,
108
+ onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
109
+ }
110
+ ),
111
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
112
+ /* @__PURE__ */ jsx(
113
+ "label",
114
+ {
115
+ htmlFor: "top-customers-limit",
116
+ className: "text-xs font-semibold uppercase text-muted-foreground",
117
+ children: t("dashboards.analytics.settings.limit", "Number of items")
118
+ }
119
+ ),
120
+ /* @__PURE__ */ jsx(
121
+ "input",
122
+ {
123
+ id: "top-customers-limit",
124
+ type: "number",
125
+ min: 1,
126
+ max: 20,
127
+ className: "w-24 rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary",
128
+ value: hydrated.limit,
129
+ onChange: (e) => {
130
+ const next = Number(e.target.value);
131
+ onSettingsChange({ ...hydrated, limit: Number.isFinite(next) ? next : hydrated.limit });
132
+ }
133
+ }
134
+ )
135
+ ] })
136
+ ] });
137
+ }
138
+ return /* @__PURE__ */ jsx(
139
+ TopNTable,
140
+ {
141
+ data,
142
+ columns,
143
+ loading,
144
+ error,
145
+ emptyMessage: t("dashboards.analytics.widgets.topCustomers.empty", "No customer data for this period")
146
+ }
147
+ );
148
+ };
149
+ var widget_client_default = TopCustomersWidget;
150
+ export {
151
+ widget_client_default as default
152
+ };
153
+ //# sourceMappingURL=widget.client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { TopNTable, type TopNTableColumn } from '@open-mercato/ui/backend/charts'\nimport { DateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopCustomersSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencySafe } from '../../../lib/formatters'\n\ntype CustomerRow = {\n rank: number\n customerId: string\n revenue: number\n}\n\nasync function fetchTopCustomersData(settings: TopCustomersSettings): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:orders',\n metric: {\n field: 'grandTotalGrossAmount',\n aggregate: 'sum',\n },\n groupBy: {\n field: 'customerEntityId',\n limit: settings.limit,\n resolveLabels: true,\n },\n dateRange: {\n field: 'placedAt',\n preset: settings.dateRange,\n },\n }\n\n const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n\n if (!call.ok) {\n const errorMsg = (call.result as Record<string, unknown>)?.error\n throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch top customers data')\n }\n\n return call.result as WidgetDataResponse\n}\n\nfunction formatCustomerName(name: string | null, unknownLabel: string): string {\n if (!name) return unknownLabel\n return name\n}\n\nconst TopCustomersWidget: React.FC<DashboardWidgetComponentProps<TopCustomersSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<CustomerRow[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const unknownLabel = t('dashboards.analytics.labels.unknown', 'Unknown')\n const columns: TopNTableColumn<CustomerRow>[] = React.useMemo(\n () => [\n {\n key: 'rank',\n header: '#',\n width: '40px',\n },\n {\n key: 'customerId',\n header: t('dashboards.analytics.widgets.topCustomers.column.customer', 'Customer'),\n formatter: (value) => formatCustomerName(String(value || ''), unknownLabel),\n },\n {\n key: 'revenue',\n header: t('dashboards.analytics.widgets.topCustomers.column.revenue', 'Revenue'),\n align: 'right',\n formatter: (value: unknown) => formatCurrencySafe(value),\n },\n ],\n [t, unknownLabel],\n )\n\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchTopCustomersData(hydrated)\n const tableData: CustomerRow[] = result.data.map((item, index) => ({\n rank: index + 1,\n customerId: item.groupLabel || String(item.groupKey || t('dashboards.analytics.labels.unknown', 'Unknown')),\n revenue: item.value ?? 0,\n }))\n setData(tableData)\n } catch (err) {\n console.error('Failed to load top customers data', err)\n setError(t('dashboards.analytics.widgets.topCustomers.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"top-customers-date-range\"\n label={t('dashboards.analytics.settings.dateRange', 'Date Range')}\n value={hydrated.dateRange}\n onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}\n />\n <div className=\"space-y-1.5\">\n <label\n htmlFor=\"top-customers-limit\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.limit', 'Number of items')}\n </label>\n <input\n id=\"top-customers-limit\"\n type=\"number\"\n min={1}\n max={20}\n className=\"w-24 rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n value={hydrated.limit}\n onChange={(e) => {\n const next = Number(e.target.value)\n onSettingsChange({ ...hydrated, limit: Number.isFinite(next) ? next : hydrated.limit })\n }}\n />\n </div>\n </div>\n )\n }\n\n return (\n <TopNTable\n data={data}\n columns={columns}\n loading={loading}\n error={error}\n emptyMessage={t('dashboards.analytics.widgets.topCustomers.empty', 'No customer data for this period')}\n />\n )\n}\n\nexport default TopCustomersWidget\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,25 @@
1
+ import TopCustomersWidget from "./widget.client.js";
2
+ import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
3
+ const widget = {
4
+ metadata: {
5
+ id: "dashboards.analytics.topCustomers",
6
+ title: "Top Customers",
7
+ description: "Top customers by revenue",
8
+ features: ["analytics.view", "sales.orders.view", "customers.people.view"],
9
+ defaultSize: "md",
10
+ defaultEnabled: false,
11
+ defaultSettings: DEFAULT_SETTINGS,
12
+ tags: ["analytics", "sales", "customers", "table"],
13
+ category: "analytics",
14
+ icon: "users",
15
+ supportsRefresh: true
16
+ },
17
+ Widget: TopCustomersWidget,
18
+ hydrateSettings,
19
+ dehydrateSettings: (s) => ({ dateRange: s.dateRange, limit: s.limit })
20
+ };
21
+ var widget_default = widget;
22
+ export {
23
+ widget_default as default
24
+ };
25
+ //# sourceMappingURL=widget.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/top-customers/widget.ts"],
4
+ "sourcesContent": ["import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport TopCustomersWidget from './widget.client'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopCustomersSettings } from './config'\n\nconst widget: DashboardWidgetModule<TopCustomersSettings> = {\n metadata: {\n id: 'dashboards.analytics.topCustomers',\n title: 'Top Customers',\n description: 'Top customers by revenue',\n features: ['analytics.view', 'sales.orders.view', 'customers.people.view'],\n defaultSize: 'md',\n defaultEnabled: false,\n defaultSettings: DEFAULT_SETTINGS,\n tags: ['analytics', 'sales', 'customers', 'table'],\n category: 'analytics',\n icon: 'users',\n supportsRefresh: true,\n },\n Widget: TopCustomersWidget,\n hydrateSettings,\n dehydrateSettings: (s) => ({ dateRange: s.dateRange, limit: s.limit }),\n}\n\nexport default widget\n"],
5
+ "mappings": "AACA,OAAO,wBAAwB;AAC/B,SAAS,kBAAkB,uBAAkD;AAE7E,MAAM,SAAsD;AAAA,EAC1D,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,kBAAkB,qBAAqB,uBAAuB;AAAA,IACzE,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,MAAM,CAAC,aAAa,SAAS,aAAa,OAAO;AAAA,IACjD,UAAU;AAAA,IACV,MAAM;AAAA,IACN,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,mBAAmB,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,OAAO,EAAE,MAAM;AACtE;AAEA,IAAO,iBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,22 @@
1
+ import { isValidDateRangePreset } from "@open-mercato/ui/backend/date-range";
2
+ const DEFAULT_SETTINGS = {
3
+ dateRange: "this_month",
4
+ limit: 10,
5
+ layout: "horizontal"
6
+ };
7
+ function hydrateSettings(raw) {
8
+ if (!raw || typeof raw !== "object") return { ...DEFAULT_SETTINGS };
9
+ const obj = raw;
10
+ const parsedLimit = Number(obj.limit);
11
+ const layout = obj.layout === "vertical" ? "vertical" : "horizontal";
12
+ return {
13
+ dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
14
+ limit: Number.isFinite(parsedLimit) && parsedLimit >= 1 && parsedLimit <= 20 ? Math.floor(parsedLimit) : DEFAULT_SETTINGS.limit,
15
+ layout
16
+ };
17
+ }
18
+ export {
19
+ DEFAULT_SETTINGS,
20
+ hydrateSettings
21
+ };
22
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/top-products/config.ts"],
4
+ "sourcesContent": ["import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'\n\nexport type ChartLayout = 'horizontal' | 'vertical'\n\nexport type TopProductsSettings = {\n dateRange: DateRangePreset\n limit: number\n layout: ChartLayout\n}\n\nexport const DEFAULT_SETTINGS: TopProductsSettings = {\n dateRange: 'this_month',\n limit: 10,\n layout: 'horizontal',\n}\n\nexport function hydrateSettings(raw: unknown): TopProductsSettings {\n if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }\n const obj = raw as Record<string, unknown>\n const parsedLimit = Number(obj.limit)\n const layout = obj.layout === 'vertical' ? 'vertical' : 'horizontal'\n return {\n dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,\n limit: Number.isFinite(parsedLimit) && parsedLimit >= 1 && parsedLimit <= 20 ? Math.floor(parsedLimit) : DEFAULT_SETTINGS.limit,\n layout,\n }\n}\n"],
5
+ "mappings": "AAAA,SAA+B,8BAA8B;AAUtD,MAAM,mBAAwC;AAAA,EACnD,WAAW;AAAA,EACX,OAAO;AAAA,EACP,QAAQ;AACV;AAEO,SAAS,gBAAgB,KAAmC;AACjE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,EAAE,GAAG,iBAAiB;AAClE,QAAM,MAAM;AACZ,QAAM,cAAc,OAAO,IAAI,KAAK;AACpC,QAAM,SAAS,IAAI,WAAW,aAAa,aAAa;AACxD,SAAO;AAAA,IACL,WAAW,uBAAuB,IAAI,SAAS,IAAI,IAAI,YAAY,iBAAiB;AAAA,IACpF,OAAO,OAAO,SAAS,WAAW,KAAK,eAAe,KAAK,eAAe,KAAK,KAAK,MAAM,WAAW,IAAI,iBAAiB;AAAA,IAC1H;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,180 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { BarChart } from "@open-mercato/ui/backend/charts";
7
+ import { DateRangeSelect, InlineDateRangeSelect } from "@open-mercato/ui/backend/date-range";
8
+ import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
9
+ import { formatCurrencyCompact } from "../../../lib/formatters.js";
10
+ async function fetchTopProductsData(settings) {
11
+ const body = {
12
+ entityType: "sales:order_lines",
13
+ metric: {
14
+ field: "totalGrossAmount",
15
+ aggregate: "sum"
16
+ },
17
+ groupBy: {
18
+ field: "productId",
19
+ limit: settings.limit,
20
+ resolveLabels: true
21
+ },
22
+ dateRange: {
23
+ field: "createdAt",
24
+ preset: settings.dateRange
25
+ }
26
+ };
27
+ const call = await apiCall("/api/dashboards/widgets/data", {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify(body)
31
+ });
32
+ if (!call.ok) {
33
+ const errorMsg = call.result?.error;
34
+ throw new Error(typeof errorMsg === "string" ? errorMsg : "Failed to fetch top products data");
35
+ }
36
+ return call.result;
37
+ }
38
+ function truncateLabel(label, t, maxLength = 20) {
39
+ if (label == null || label === "") return t("dashboards.analytics.labels.unknownProduct", "Unknown Product");
40
+ const labelStr = String(label);
41
+ if (labelStr === "0" || labelStr === "null" || labelStr === "undefined") {
42
+ return t("dashboards.analytics.labels.unknownProduct", "Unknown Product");
43
+ }
44
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(labelStr)) {
45
+ return t("dashboards.analytics.labels.unnamedProduct", "Unnamed Product");
46
+ }
47
+ if (labelStr.length <= maxLength) return labelStr;
48
+ return labelStr.slice(0, maxLength - 3) + "...";
49
+ }
50
+ const TopProductsWidget = ({
51
+ mode,
52
+ settings = DEFAULT_SETTINGS,
53
+ onSettingsChange,
54
+ refreshToken,
55
+ onRefreshStateChange
56
+ }) => {
57
+ const t = useT();
58
+ const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
59
+ const [data, setData] = React.useState([]);
60
+ const [loading, setLoading] = React.useState(true);
61
+ const [error, setError] = React.useState(null);
62
+ const fetchingRef = React.useRef(false);
63
+ const refresh = React.useCallback(async () => {
64
+ if (fetchingRef.current) return;
65
+ fetchingRef.current = true;
66
+ onRefreshStateChange?.(true);
67
+ setLoading(true);
68
+ setError(null);
69
+ try {
70
+ const result = await fetchTopProductsData(hydrated);
71
+ const chartData = result.data.map((item, index) => ({
72
+ name: truncateLabel(item.groupLabel ?? item.groupKey ?? `Product ${index + 1}`, t),
73
+ Revenue: item.value ?? 0
74
+ }));
75
+ setData(chartData);
76
+ } catch (err) {
77
+ console.error("Failed to load top products data", err);
78
+ setError(t("dashboards.analytics.widgets.topProducts.error", "Failed to load data"));
79
+ } finally {
80
+ setLoading(false);
81
+ onRefreshStateChange?.(false);
82
+ fetchingRef.current = false;
83
+ }
84
+ }, [hydrated, onRefreshStateChange, t]);
85
+ React.useEffect(() => {
86
+ refresh().catch(() => {
87
+ });
88
+ }, [refresh, refreshToken]);
89
+ if (mode === "settings") {
90
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-sm", children: [
91
+ /* @__PURE__ */ jsx(
92
+ DateRangeSelect,
93
+ {
94
+ id: "top-products-date-range",
95
+ label: t("dashboards.analytics.settings.dateRange", "Date Range"),
96
+ value: hydrated.dateRange,
97
+ onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
98
+ }
99
+ ),
100
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
101
+ /* @__PURE__ */ jsx(
102
+ "label",
103
+ {
104
+ htmlFor: "top-products-limit",
105
+ className: "text-xs font-semibold uppercase text-muted-foreground",
106
+ children: t("dashboards.analytics.settings.limit", "Number of items")
107
+ }
108
+ ),
109
+ /* @__PURE__ */ jsx(
110
+ "input",
111
+ {
112
+ id: "top-products-limit",
113
+ type: "number",
114
+ min: 1,
115
+ max: 20,
116
+ className: "w-24 rounded-md border px-2 py-1 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary",
117
+ value: hydrated.limit,
118
+ onChange: (e) => {
119
+ const next = Number(e.target.value);
120
+ onSettingsChange({ ...hydrated, limit: Number.isFinite(next) ? next : hydrated.limit });
121
+ }
122
+ }
123
+ )
124
+ ] }),
125
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
126
+ /* @__PURE__ */ jsx(
127
+ "label",
128
+ {
129
+ htmlFor: "top-products-layout",
130
+ className: "text-xs font-semibold uppercase text-muted-foreground",
131
+ children: t("dashboards.analytics.settings.chartLayout", "Chart Layout")
132
+ }
133
+ ),
134
+ /* @__PURE__ */ jsxs(
135
+ "select",
136
+ {
137
+ id: "top-products-layout",
138
+ className: "w-full rounded-md border bg-background px-2 py-1 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary",
139
+ value: hydrated.layout,
140
+ onChange: (e) => onSettingsChange({ ...hydrated, layout: e.target.value }),
141
+ children: [
142
+ /* @__PURE__ */ jsx("option", { value: "horizontal", children: t("dashboards.analytics.settings.horizontal", "Horizontal") }),
143
+ /* @__PURE__ */ jsx("option", { value: "vertical", children: t("dashboards.analytics.settings.vertical", "Vertical") })
144
+ ]
145
+ }
146
+ )
147
+ ] })
148
+ ] });
149
+ }
150
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
151
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end mb-2", children: /* @__PURE__ */ jsx(
152
+ InlineDateRangeSelect,
153
+ {
154
+ value: hydrated.dateRange,
155
+ onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
156
+ }
157
+ ) }),
158
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(
159
+ BarChart,
160
+ {
161
+ data,
162
+ index: "name",
163
+ categories: ["Revenue"],
164
+ categoryLabels: { Revenue: t("dashboards.analytics.widgets.topCustomers.column.revenue", "Revenue") },
165
+ loading,
166
+ error,
167
+ layout: hydrated.layout,
168
+ valueFormatter: formatCurrencyCompact,
169
+ colors: ["emerald"],
170
+ showLegend: false,
171
+ emptyMessage: t("dashboards.analytics.widgets.topProducts.empty", "No product sales data for this period")
172
+ }
173
+ ) })
174
+ ] });
175
+ };
176
+ var widget_client_default = TopProductsWidget;
177
+ export {
178
+ widget_client_default as default
179
+ };
180
+ //# sourceMappingURL=widget.client.js.map