@open-mercato/core 0.6.7-develop.6751.1.ac823a3d26 → 0.6.7-develop.6758.1.697eade236
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/attachments/api/route.js +16 -2
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/lib/upload-limits.js +33 -0
- package/dist/modules/attachments/lib/upload-limits.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +52 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
- package/dist/modules/currencies/di.js +4 -0
- package/dist/modules/currencies/di.js.map +2 -2
- package/dist/modules/currencies/services/baseCurrencyService.js +42 -0
- package/dist/modules/currencies/services/baseCurrencyService.js.map +7 -0
- package/dist/modules/customers/analytics.js +3 -1
- package/dist/modules/customers/analytics.js.map +2 -2
- package/dist/modules/customers/api/deals/aggregate/route.js +5 -4
- package/dist/modules/customers/api/deals/aggregate/route.js.map +2 -2
- package/dist/modules/customers/api/deals/summary/route.js +5 -4
- package/dist/modules/customers/api/deals/summary/route.js.map +2 -2
- package/dist/modules/customers/lib/optionalBaseCurrency.js +19 -0
- package/dist/modules/customers/lib/optionalBaseCurrency.js.map +7 -0
- package/dist/modules/dashboards/api/widgets/data/batch/route.js +8 -1
- package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +8 -1
- package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/schema.js +2 -1
- package/dist/modules/dashboards/api/widgets/data/schema.js.map +2 -2
- package/dist/modules/dashboards/components/UnlabelledAmountNotice.js +33 -0
- package/dist/modules/dashboards/components/UnlabelledAmountNotice.js.map +7 -0
- package/dist/modules/dashboards/lib/aggregations.js +17 -0
- package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
- package/dist/modules/dashboards/lib/formatters.js +84 -11
- package/dist/modules/dashboards/lib/formatters.js.map +2 -2
- package/dist/modules/dashboards/lib/optionalBaseCurrency.js +11 -0
- package/dist/modules/dashboards/lib/optionalBaseCurrency.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +86 -6
- package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
- package/dist/modules/dashboards/subscribers/invalidateWidgetDataCache.js +26 -0
- package/dist/modules/dashboards/subscribers/invalidateWidgetDataCache.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +9 -3
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +10 -4
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +9 -3
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +8 -3
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +26 -18
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +22 -14
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +10 -4
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
- package/dist/modules/notifications/api/route.js +62 -3
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/sales/analytics.js +7 -3
- package/dist/modules/sales/analytics.js.map +2 -2
- package/dist/modules/sales/api/document-history/route.js +2 -1
- package/dist/modules/sales/api/document-history/route.js.map +2 -2
- package/dist/modules/sales/lib/historyHelpers.js +69 -1
- package/dist/modules/sales/lib/historyHelpers.js.map +2 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js +51 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/api/route.ts +16 -2
- package/src/modules/attachments/lib/upload-limits.ts +35 -0
- package/src/modules/communication_channels/api/post/test-seed/route.ts +69 -2
- package/src/modules/currencies/di.ts +4 -1
- package/src/modules/currencies/services/baseCurrencyService.ts +59 -0
- package/src/modules/customers/analytics.ts +2 -0
- package/src/modules/customers/api/deals/aggregate/route.ts +5 -4
- package/src/modules/customers/api/deals/summary/route.ts +5 -4
- package/src/modules/customers/lib/optionalBaseCurrency.ts +34 -0
- package/src/modules/dashboards/api/widgets/data/batch/route.ts +8 -1
- package/src/modules/dashboards/api/widgets/data/route.ts +8 -1
- package/src/modules/dashboards/api/widgets/data/schema.ts +1 -0
- package/src/modules/dashboards/components/UnlabelledAmountNotice.tsx +43 -0
- package/src/modules/dashboards/i18n/de.json +2 -0
- package/src/modules/dashboards/i18n/en.json +2 -0
- package/src/modules/dashboards/i18n/es.json +2 -0
- package/src/modules/dashboards/i18n/pl.json +2 -0
- package/src/modules/dashboards/lib/aggregations.ts +35 -0
- package/src/modules/dashboards/lib/formatters.ts +137 -12
- package/src/modules/dashboards/lib/optionalBaseCurrency.ts +22 -0
- package/src/modules/dashboards/services/widgetDataService.ts +110 -4
- package/src/modules/dashboards/subscribers/invalidateWidgetDataCache.ts +32 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +9 -3
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +9 -3
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +9 -3
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +7 -2
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +25 -15
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +21 -11
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +9 -3
- package/src/modules/notifications/api/route.ts +88 -3
- package/src/modules/sales/analytics.ts +4 -0
- package/src/modules/sales/api/document-history/route.ts +1 -0
- package/src/modules/sales/i18n/de.json +19 -0
- package/src/modules/sales/i18n/en.json +19 -0
- package/src/modules/sales/i18n/es.json +19 -0
- package/src/modules/sales/i18n/pl.json +19 -0
- package/src/modules/sales/lib/historyHelpers.ts +88 -1
- package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +64 -3
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
SelectValue
|
|
17
17
|
} from "@open-mercato/ui/primitives/select";
|
|
18
18
|
import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
|
|
19
|
-
import {
|
|
19
|
+
import { createCurrencyFormatters } from "../../../lib/formatters.js";
|
|
20
|
+
import { UnlabelledAmountNotice } from "../../../components/UnlabelledAmountNotice.js";
|
|
20
21
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
21
22
|
const logger = createLogger("dashboards").child({ component: "revenue-trend" });
|
|
22
23
|
async function fetchRevenueTrendData(settings, fetchWidgetData) {
|
|
@@ -103,8 +104,10 @@ const RevenueTrendWidget = ({
|
|
|
103
104
|
const locale = useLocale();
|
|
104
105
|
const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
|
|
105
106
|
const [data, setData] = React.useState([]);
|
|
107
|
+
const [currency, setCurrency] = React.useState(null);
|
|
106
108
|
const [loading, setLoading] = React.useState(true);
|
|
107
109
|
const [error, setError] = React.useState(null);
|
|
110
|
+
const money = React.useMemo(() => createCurrencyFormatters(currency, "--", locale), [currency, locale]);
|
|
108
111
|
const fetchWidgetData = useWidgetData();
|
|
109
112
|
const refresh = React.useCallback(async () => {
|
|
110
113
|
onRefreshStateChange?.(true);
|
|
@@ -122,6 +125,7 @@ const RevenueTrendWidget = ({
|
|
|
122
125
|
Revenue: item.value ?? 0
|
|
123
126
|
}));
|
|
124
127
|
setData(chartData);
|
|
128
|
+
setCurrency(result.metadata?.currency ?? null);
|
|
125
129
|
} catch (err) {
|
|
126
130
|
logger.error("Failed to load revenue trend data", { err });
|
|
127
131
|
setError(t("dashboards.analytics.widgets.revenueTrend.error", "Failed to load data"));
|
|
@@ -199,11 +203,12 @@ const RevenueTrendWidget = ({
|
|
|
199
203
|
loading,
|
|
200
204
|
error,
|
|
201
205
|
showArea: hydrated.showArea,
|
|
202
|
-
valueFormatter:
|
|
206
|
+
valueFormatter: money.formatCompact,
|
|
203
207
|
colors: ["blue"],
|
|
204
208
|
emptyMessage: t("dashboards.analytics.widgets.revenueTrend.empty", "No revenue data for this period")
|
|
205
209
|
}
|
|
206
|
-
)
|
|
210
|
+
),
|
|
211
|
+
/* @__PURE__ */ jsx(UnlabelledAmountNotice, { currency, loading, error })
|
|
207
212
|
] });
|
|
208
213
|
};
|
|
209
214
|
var widget_client_default = RevenueTrendWidget;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { LineChart, type LineChartDataItem } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport type { DateGranularity } from '@open-mercato/shared/modules/analytics'\nimport { DEFAULT_SETTINGS, hydrateSettings, type RevenueTrendSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'\nimport { LineChart, type LineChartDataItem } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport type { DateGranularity } from '@open-mercato/shared/modules/analytics'\nimport { DEFAULT_SETTINGS, hydrateSettings, type RevenueTrendSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { createCurrencyFormatters } from '../../../lib/formatters'\nimport { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'revenue-trend' })\n\nasync function fetchRevenueTrendData(settings: RevenueTrendSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:orders',\n metric: {\n field: 'grandTotalGrossAmount',\n aggregate: 'sum',\n },\n groupBy: {\n field: 'placedAt',\n granularity: settings.granularity,\n },\n dateRange: {\n field: 'placedAt',\n preset: settings.dateRange,\n },\n }\n\n return fetchWidgetData<WidgetDataResponse>(body)\n}\n\nfunction formatDate(dateStr: string | null, granularity: DateGranularity, locale?: string): string {\n if (!dateStr) return '--'\n try {\n const date = new Date(dateStr)\n const localeStr = locale ?? undefined\n switch (granularity) {\n case 'day':\n case 'week':\n return date.toLocaleDateString(localeStr, { month: 'short', day: 'numeric' })\n case 'month':\n return date.toLocaleDateString(localeStr, { month: 'short', year: 'numeric' })\n case 'quarter': {\n const quarter = Math.floor(date.getMonth() / 3) + 1\n return `Q${quarter} ${date.getFullYear()}`\n }\n case 'year':\n return date.toLocaleDateString(localeStr, { year: 'numeric' })\n default:\n return date.toLocaleDateString(localeStr, { month: 'short', day: 'numeric' })\n }\n } catch {\n return String(dateStr)\n }\n}\n\nconst GRANULARITY_OPTIONS: { value: DateGranularity; labelKey: string }[] = [\n { value: 'day', labelKey: 'dashboards.analytics.granularity.day' },\n { value: 'week', labelKey: 'dashboards.analytics.granularity.week' },\n { value: 'month', labelKey: 'dashboards.analytics.granularity.month' },\n { value: 'quarter', labelKey: 'dashboards.analytics.granularity.quarter' },\n { value: 'year', labelKey: 'dashboards.analytics.granularity.year' },\n]\n\nfunction getAutoGranularity(dateRange: DateRangePreset): DateGranularity {\n switch (dateRange) {\n case 'today':\n case 'yesterday':\n case 'last_7_days':\n return 'day'\n case 'this_week':\n case 'last_week':\n case 'last_30_days':\n return 'day'\n case 'this_month':\n case 'last_month':\n case 'last_90_days':\n return 'week'\n case 'this_quarter':\n case 'last_quarter':\n return 'week'\n case 'this_year':\n case 'last_year':\n return 'month'\n default:\n return 'day'\n }\n}\n\nconst RevenueTrendWidget: React.FC<DashboardWidgetComponentProps<RevenueTrendSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const locale = useLocale()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<LineChartDataItem[]>([])\n const [currency, setCurrency] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchRevenueTrendData(hydrated, fetchWidgetData)\n const sortedData = [...result.data].sort((a, b) => {\n const aTime = new Date(a.groupKey as string || 0).getTime()\n const bTime = new Date(b.groupKey as string || 0).getTime()\n return aTime - bTime\n })\n const chartData = sortedData.map((item) => ({\n date: formatDate(item.groupKey as string | null, hydrated.granularity, locale),\n Revenue: item.value ?? 0,\n }))\n setData(chartData)\n setCurrency(result.metadata?.currency ?? null)\n } catch (err) {\n logger.error('Failed to load revenue trend data', { err })\n setError(t('dashboards.analytics.widgets.revenueTrend.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, fetchWidgetData, locale, 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=\"revenue-trend-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=\"revenue-trend-granularity\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.granularity', 'Granularity')}\n </label>\n <Select\n value={hydrated.granularity}\n onValueChange={(value) => onSettingsChange({ ...hydrated, granularity: value as DateGranularity })}\n >\n <SelectTrigger id=\"revenue-trend-granularity\" size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {GRANULARITY_OPTIONS.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {t(opt.labelKey, opt.value)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-1.5\">\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={hydrated.showArea}\n onChange={(e) => onSettingsChange({ ...hydrated, showArea: e.target.checked })}\n className=\"h-4 w-4 rounded border focus-visible:ring-ring\"\n />\n {t('dashboards.analytics.settings.showArea', 'Show area fill')}\n </label>\n </div>\n </div>\n )\n }\n\n const effectiveGranularity = hydrated.granularity === 'day' ? getAutoGranularity(hydrated.dateRange) : hydrated.granularity\n\n return (\n <div>\n <div className=\"mb-2 flex justify-end\">\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange, granularity: getAutoGranularity(dateRange) })}\n />\n </div>\n <LineChart\n data={data}\n index=\"date\"\n categories={['Revenue']}\n categoryLabels={{ Revenue: t('dashboards.analytics.widgets.topCustomers.column.revenue', 'Revenue') }}\n loading={loading}\n error={error}\n showArea={hydrated.showArea}\n valueFormatter={money.formatCompact}\n colors={['blue']}\n emptyMessage={t('dashboards.analytics.widgets.revenueTrend.empty', 'No revenue data for this period')}\n />\n <UnlabelledAmountNotice currency={currency} loading={loading} error={error} />\n </div>\n )\n}\n\nexport default RevenueTrendWidget\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useWidgetData } from "@open-mercato/ui/backend/dashboard/widgetData";
|
|
5
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
6
|
import { BarChart } from "@open-mercato/ui/backend/charts";
|
|
7
7
|
import { DateRangeSelect } from "@open-mercato/ui/backend/date-range";
|
|
8
8
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
9
9
|
import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
|
|
10
|
-
import {
|
|
10
|
+
import { createCurrencyFormatters } from "../../../lib/formatters.js";
|
|
11
|
+
import { UnlabelledAmountNotice } from "../../../components/UnlabelledAmountNotice.js";
|
|
11
12
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
12
13
|
const logger = createLogger("dashboards").child({ component: "sales-by-region" });
|
|
13
14
|
async function fetchSalesByRegionData(settings, fetchWidgetData) {
|
|
@@ -36,10 +37,13 @@ const SalesByRegionWidget = ({
|
|
|
36
37
|
onRefreshStateChange
|
|
37
38
|
}) => {
|
|
38
39
|
const t = useT();
|
|
40
|
+
const locale = useLocale();
|
|
39
41
|
const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
|
|
40
42
|
const [data, setData] = React.useState([]);
|
|
43
|
+
const [currency, setCurrency] = React.useState(null);
|
|
41
44
|
const [loading, setLoading] = React.useState(true);
|
|
42
45
|
const [error, setError] = React.useState(null);
|
|
46
|
+
const money = React.useMemo(() => createCurrencyFormatters(currency, "--", locale), [currency, locale]);
|
|
43
47
|
const fetchWidgetData = useWidgetData();
|
|
44
48
|
const refresh = React.useCallback(async () => {
|
|
45
49
|
onRefreshStateChange?.(true);
|
|
@@ -52,6 +56,7 @@ const SalesByRegionWidget = ({
|
|
|
52
56
|
Revenue: item.value ?? 0
|
|
53
57
|
}));
|
|
54
58
|
setData(chartData);
|
|
59
|
+
setCurrency(result.metadata?.currency ?? null);
|
|
55
60
|
} catch (err) {
|
|
56
61
|
logger.error("Failed to load sales by region data", { err });
|
|
57
62
|
setError(t("dashboards.analytics.widgets.salesByRegion.error", "Failed to load data"));
|
|
@@ -102,22 +107,25 @@ const SalesByRegionWidget = ({
|
|
|
102
107
|
] })
|
|
103
108
|
] });
|
|
104
109
|
}
|
|
105
|
-
return /* @__PURE__ */
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
|
|
111
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(
|
|
112
|
+
BarChart,
|
|
113
|
+
{
|
|
114
|
+
data,
|
|
115
|
+
index: "region",
|
|
116
|
+
categories: ["Revenue"],
|
|
117
|
+
categoryLabels: { Revenue: t("dashboards.analytics.widgets.topCustomers.column.revenue", "Revenue") },
|
|
118
|
+
loading,
|
|
119
|
+
error,
|
|
120
|
+
layout: "horizontal",
|
|
121
|
+
valueFormatter: money.formatCompact,
|
|
122
|
+
colors: ["cyan"],
|
|
123
|
+
showLegend: false,
|
|
124
|
+
emptyMessage: t("dashboards.analytics.widgets.salesByRegion.empty", "No regional sales data for this period")
|
|
125
|
+
}
|
|
126
|
+
) }),
|
|
127
|
+
/* @__PURE__ */ jsx(UnlabelledAmountNotice, { currency, loading, error })
|
|
128
|
+
] });
|
|
121
129
|
};
|
|
122
130
|
var widget_client_default = SalesByRegionWidget;
|
|
123
131
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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 { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\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 { Input } from '@open-mercato/ui/primitives/input'\nimport { DEFAULT_SETTINGS, hydrateSettings, type SalesByRegionSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useLocale, 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 { Input } from '@open-mercato/ui/primitives/input'\nimport { DEFAULT_SETTINGS, hydrateSettings, type SalesByRegionSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { createCurrencyFormatters } from '../../../lib/formatters'\nimport { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'sales-by-region' })\n\nasync function fetchSalesByRegionData(settings: SalesByRegionSettings, fetchWidgetData: WidgetDataFetcher): 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 return fetchWidgetData<WidgetDataResponse>(body)\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 locale = useLocale()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [currency, setCurrency] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchSalesByRegionData(hydrated, fetchWidgetData)\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 setCurrency(result.metadata?.currency ?? null)\n } catch (err) {\n logger.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, fetchWidgetData, 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\"\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 <div className=\"flex flex-col h-full\">\n <div className=\"flex-1 min-h-0\">\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={money.formatCompact}\n colors={['cyan']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.salesByRegion.empty', 'No regional sales data for this period')}\n />\n </div>\n <UnlabelledAmountNotice currency={currency} loading={loading} error={error} />\n </div>\n )\n}\n\nexport default SalesByRegionWidget\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useWidgetData } from "@open-mercato/ui/backend/dashboard/widgetData";
|
|
5
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
6
|
import { TopNTable } from "@open-mercato/ui/backend/charts";
|
|
7
7
|
import { DateRangeSelect } from "@open-mercato/ui/backend/date-range";
|
|
8
8
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
9
9
|
import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
|
|
10
|
-
import {
|
|
10
|
+
import { createCurrencyFormatters } from "../../../lib/formatters.js";
|
|
11
|
+
import { UnlabelledAmountNotice } from "../../../components/UnlabelledAmountNotice.js";
|
|
11
12
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
12
13
|
const logger = createLogger("dashboards").child({ component: "top-customers" });
|
|
13
14
|
async function fetchTopCustomersData(settings, fetchWidgetData) {
|
|
@@ -41,10 +42,13 @@ const TopCustomersWidget = ({
|
|
|
41
42
|
onRefreshStateChange
|
|
42
43
|
}) => {
|
|
43
44
|
const t = useT();
|
|
45
|
+
const locale = useLocale();
|
|
44
46
|
const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
|
|
45
47
|
const [data, setData] = React.useState([]);
|
|
48
|
+
const [currency, setCurrency] = React.useState(null);
|
|
46
49
|
const [loading, setLoading] = React.useState(true);
|
|
47
50
|
const [error, setError] = React.useState(null);
|
|
51
|
+
const money = React.useMemo(() => createCurrencyFormatters(currency, "--", locale), [currency, locale]);
|
|
48
52
|
const unknownLabel = t("dashboards.analytics.labels.unknown", "Unknown");
|
|
49
53
|
const columns = React.useMemo(
|
|
50
54
|
() => [
|
|
@@ -62,10 +66,10 @@ const TopCustomersWidget = ({
|
|
|
62
66
|
key: "revenue",
|
|
63
67
|
header: t("dashboards.analytics.widgets.topCustomers.column.revenue", "Revenue"),
|
|
64
68
|
align: "right",
|
|
65
|
-
formatter: (value) =>
|
|
69
|
+
formatter: (value) => money.formatSafe(value)
|
|
66
70
|
}
|
|
67
71
|
],
|
|
68
|
-
[t, unknownLabel]
|
|
72
|
+
[t, unknownLabel, money]
|
|
69
73
|
);
|
|
70
74
|
const fetchWidgetData = useWidgetData();
|
|
71
75
|
const refresh = React.useCallback(async () => {
|
|
@@ -80,6 +84,7 @@ const TopCustomersWidget = ({
|
|
|
80
84
|
revenue: item.value ?? 0
|
|
81
85
|
}));
|
|
82
86
|
setData(tableData);
|
|
87
|
+
setCurrency(result.metadata?.currency ?? null);
|
|
83
88
|
} catch (err) {
|
|
84
89
|
logger.error("Failed to load top customers data", { err });
|
|
85
90
|
setError(t("dashboards.analytics.widgets.topCustomers.error", "Failed to load data"));
|
|
@@ -130,16 +135,19 @@ const TopCustomersWidget = ({
|
|
|
130
135
|
] })
|
|
131
136
|
] });
|
|
132
137
|
}
|
|
133
|
-
return /* @__PURE__ */
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
|
|
139
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(
|
|
140
|
+
TopNTable,
|
|
141
|
+
{
|
|
142
|
+
data,
|
|
143
|
+
columns,
|
|
144
|
+
loading,
|
|
145
|
+
error,
|
|
146
|
+
emptyMessage: t("dashboards.analytics.widgets.topCustomers.empty", "No customer data for this period")
|
|
147
|
+
}
|
|
148
|
+
) }),
|
|
149
|
+
/* @__PURE__ */ jsx(UnlabelledAmountNotice, { currency, loading, error })
|
|
150
|
+
] });
|
|
143
151
|
};
|
|
144
152
|
var widget_client_default = TopCustomersWidget;
|
|
145
153
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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 { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\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 { Input } from '@open-mercato/ui/primitives/input'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopCustomersSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useLocale, 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 { Input } from '@open-mercato/ui/primitives/input'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopCustomersSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { createCurrencyFormatters } from '../../../lib/formatters'\nimport { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'top-customers' })\n\ntype CustomerRow = {\n rank: number\n customerId: string\n revenue: number\n}\n\nasync function fetchTopCustomersData(settings: TopCustomersSettings, fetchWidgetData: WidgetDataFetcher): 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 return fetchWidgetData<WidgetDataResponse>(body)\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 locale = useLocale()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<CustomerRow[]>([])\n const [currency, setCurrency] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])\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) => money.formatSafe(value),\n },\n ],\n [t, unknownLabel, money],\n )\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchTopCustomersData(hydrated, fetchWidgetData)\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 setCurrency(result.metadata?.currency ?? null)\n } catch (err) {\n logger.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, fetchWidgetData, 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\"\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 <div className=\"flex flex-col h-full\">\n <div className=\"flex-1 min-h-0\">\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 </div>\n <UnlabelledAmountNotice currency={currency} loading={loading} error={error} />\n </div>\n )\n}\n\nexport default TopCustomersWidget\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useWidgetData } from "@open-mercato/ui/backend/dashboard/widgetData";
|
|
5
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
|
|
6
6
|
import { BarChart } from "@open-mercato/ui/backend/charts";
|
|
7
7
|
import { DateRangeSelect, InlineDateRangeSelect } from "@open-mercato/ui/backend/date-range";
|
|
8
8
|
import { Input } from "@open-mercato/ui/primitives/input";
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
SelectValue
|
|
15
15
|
} from "@open-mercato/ui/primitives/select";
|
|
16
16
|
import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
|
|
17
|
-
import {
|
|
17
|
+
import { createCurrencyFormatters } from "../../../lib/formatters.js";
|
|
18
|
+
import { UnlabelledAmountNotice } from "../../../components/UnlabelledAmountNotice.js";
|
|
18
19
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
19
20
|
const logger = createLogger("dashboards").child({ component: "top-products" });
|
|
20
21
|
async function fetchTopProductsData(settings, fetchWidgetData) {
|
|
@@ -56,11 +57,14 @@ const TopProductsWidget = ({
|
|
|
56
57
|
onRefreshStateChange
|
|
57
58
|
}) => {
|
|
58
59
|
const t = useT();
|
|
60
|
+
const locale = useLocale();
|
|
59
61
|
const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
|
|
60
62
|
const [data, setData] = React.useState([]);
|
|
63
|
+
const [currency, setCurrency] = React.useState(null);
|
|
61
64
|
const [loading, setLoading] = React.useState(true);
|
|
62
65
|
const [error, setError] = React.useState(null);
|
|
63
66
|
const fetchingRef = React.useRef(false);
|
|
67
|
+
const money = React.useMemo(() => createCurrencyFormatters(currency, "--", locale), [currency, locale]);
|
|
64
68
|
const fetchWidgetData = useWidgetData();
|
|
65
69
|
const refresh = React.useCallback(async () => {
|
|
66
70
|
if (fetchingRef.current) return;
|
|
@@ -75,6 +79,7 @@ const TopProductsWidget = ({
|
|
|
75
79
|
Revenue: item.value ?? 0
|
|
76
80
|
}));
|
|
77
81
|
setData(chartData);
|
|
82
|
+
setCurrency(result.metadata?.currency ?? null);
|
|
78
83
|
} catch (err) {
|
|
79
84
|
logger.error("Failed to load top products data", { err });
|
|
80
85
|
setError(t("dashboards.analytics.widgets.topProducts.error", "Failed to load data"));
|
|
@@ -168,12 +173,13 @@ const TopProductsWidget = ({
|
|
|
168
173
|
loading,
|
|
169
174
|
error,
|
|
170
175
|
layout: hydrated.layout,
|
|
171
|
-
valueFormatter:
|
|
176
|
+
valueFormatter: money.formatCompact,
|
|
172
177
|
colors: ["emerald"],
|
|
173
178
|
showLegend: false,
|
|
174
179
|
emptyMessage: t("dashboards.analytics.widgets.topProducts.empty", "No product sales data for this period")
|
|
175
180
|
}
|
|
176
|
-
) })
|
|
181
|
+
) }),
|
|
182
|
+
/* @__PURE__ */ jsx(UnlabelledAmountNotice, { currency, loading, error })
|
|
177
183
|
] });
|
|
178
184
|
};
|
|
179
185
|
var widget_client_default = TopProductsWidget;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport { DateRangeSelect, InlineDateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopProductsSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport {
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport { DateRangeSelect, InlineDateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopProductsSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { createCurrencyFormatters } from '../../../lib/formatters'\nimport { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'top-products' })\n\nasync function fetchTopProductsData(settings: TopProductsSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:order_lines',\n metric: {\n field: 'totalGrossAmount',\n aggregate: 'sum',\n },\n groupBy: {\n field: 'productId',\n limit: settings.limit,\n resolveLabels: true,\n },\n dateRange: {\n field: 'createdAt',\n preset: settings.dateRange,\n },\n }\n\n return fetchWidgetData<WidgetDataResponse>(body)\n}\n\nfunction truncateLabel(\n label: unknown,\n t: (key: string, fallback: string) => string,\n maxLength: number = 20\n): string {\n if (label == null || label === '') return t('dashboards.analytics.labels.unknownProduct', 'Unknown Product')\n const labelStr = String(label)\n // Check for UUID-like strings or meaningless values\n if (labelStr === '0' || labelStr === 'null' || labelStr === 'undefined') {\n return t('dashboards.analytics.labels.unknownProduct', 'Unknown Product')\n }\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(labelStr)) {\n return t('dashboards.analytics.labels.unnamedProduct', 'Unnamed Product')\n }\n if (labelStr.length <= maxLength) return labelStr\n return labelStr.slice(0, maxLength - 3) + '...'\n}\n\nconst TopProductsWidget: React.FC<DashboardWidgetComponentProps<TopProductsSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const locale = useLocale()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [currency, setCurrency] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const fetchingRef = React.useRef(false)\n const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n if (fetchingRef.current) return\n fetchingRef.current = true\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchTopProductsData(hydrated, fetchWidgetData)\n const chartData = result.data.map((item, index) => ({\n name: truncateLabel(item.groupLabel ?? item.groupKey ?? `Product ${index + 1}`, t),\n Revenue: item.value ?? 0,\n }))\n setData(chartData)\n setCurrency(result.metadata?.currency ?? null)\n } catch (err) {\n logger.error('Failed to load top products data', { err })\n setError(t('dashboards.analytics.widgets.topProducts.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n fetchingRef.current = false\n }\n }, [hydrated, fetchWidgetData, 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-products-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-products-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-products-limit\"\n type=\"number\"\n min={1}\n max={20}\n className=\"w-24\"\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 className=\"space-y-1.5\">\n <label\n htmlFor=\"top-products-layout\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.chartLayout', 'Chart Layout')}\n </label>\n <Select\n value={hydrated.layout}\n onValueChange={(value) => onSettingsChange({ ...hydrated, layout: value as 'horizontal' | 'vertical' })}\n >\n <SelectTrigger id=\"top-products-layout\" size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"horizontal\">{t('dashboards.analytics.settings.horizontal', 'Horizontal')}</SelectItem>\n <SelectItem value=\"vertical\">{t('dashboards.analytics.settings.vertical', 'Vertical')}</SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex justify-end mb-2\">\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n </div>\n <div className=\"flex-1 min-h-0\">\n <BarChart\n data={data}\n index=\"name\"\n categories={['Revenue']}\n categoryLabels={{ Revenue: t('dashboards.analytics.widgets.topCustomers.column.revenue', 'Revenue') }}\n loading={loading}\n error={error}\n layout={hydrated.layout}\n valueFormatter={money.formatCompact}\n colors={['emerald']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.topProducts.empty', 'No product sales data for this period')}\n />\n </div>\n <UnlabelledAmountNotice currency={currency} loading={loading} error={error} />\n </div>\n )\n}\n\nexport default TopProductsWidget\n"],
|
|
5
|
+
"mappings": ";AAgHQ,cAMA,YANA;AA9GR,YAAY,WAAW;AAEvB,SAAS,qBAA6C;AACtD,SAAS,WAAW,YAAY;AAChC,SAAS,gBAAuC;AAChD,SAAS,iBAAiB,6BAAmD;AAC7E,SAAS,aAAa;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kBAAkB,uBAAiD;AAE5E,SAAS,gCAAgC;AACzC,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,YAAY,EAAE,MAAM,EAAE,WAAW,eAAe,CAAC;AAE7E,eAAe,qBAAqB,UAA+B,iBAAiE;AAClI,QAAM,OAAO;AAAA,IACX,YAAY;AAAA,IACZ,QAAQ;AAAA,MACN,OAAO;AAAA,MACP,WAAW;AAAA,IACb;AAAA,IACA,SAAS;AAAA,MACP,OAAO;AAAA,MACP,OAAO,SAAS;AAAA,MAChB,eAAe;AAAA,IACjB;AAAA,IACA,WAAW;AAAA,MACT,OAAO;AAAA,MACP,QAAQ,SAAS;AAAA,IACnB;AAAA,EACF;AAEA,SAAO,gBAAoC,IAAI;AACjD;AAEA,SAAS,cACP,OACA,GACA,YAAoB,IACZ;AACR,MAAI,SAAS,QAAQ,UAAU,GAAI,QAAO,EAAE,8CAA8C,iBAAiB;AAC3G,QAAM,WAAW,OAAO,KAAK;AAE7B,MAAI,aAAa,OAAO,aAAa,UAAU,aAAa,aAAa;AACvE,WAAO,EAAE,8CAA8C,iBAAiB;AAAA,EAC1E;AACA,MAAI,kEAAkE,KAAK,QAAQ,GAAG;AACpF,WAAO,EAAE,8CAA8C,iBAAiB;AAAA,EAC1E;AACA,MAAI,SAAS,UAAU,UAAW,QAAO;AACzC,SAAO,SAAS,MAAM,GAAG,YAAY,CAAC,IAAI;AAC5C;AAEA,MAAM,oBAAkF,CAAC;AAAA,EACvF;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,WAAW,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,GAAG,CAAC,QAAQ,CAAC;AAC1E,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAA6B,CAAC,CAAC;AAC7D,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,SAAwB,IAAI;AAClE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,cAAc,MAAM,OAAO,KAAK;AACtC,QAAM,QAAQ,MAAM,QAAQ,MAAM,yBAAyB,UAAU,MAAM,MAAM,GAAG,CAAC,UAAU,MAAM,CAAC;AAEtG,QAAM,kBAAkB,cAAc;AACtC,QAAM,UAAU,MAAM,YAAY,YAAY;AAC5C,QAAI,YAAY,QAAS;AACzB,gBAAY,UAAU;AACtB,2BAAuB,IAAI;AAC3B,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,SAAS,MAAM,qBAAqB,UAAU,eAAe;AACnE,YAAM,YAAY,OAAO,KAAK,IAAI,CAAC,MAAM,WAAW;AAAA,QAClD,MAAM,cAAc,KAAK,cAAc,KAAK,YAAY,WAAW,QAAQ,CAAC,IAAI,CAAC;AAAA,QACjF,SAAS,KAAK,SAAS;AAAA,MACzB,EAAE;AACF,cAAQ,SAAS;AACjB,kBAAY,OAAO,UAAU,YAAY,IAAI;AAAA,IAC/C,SAAS,KAAK;AACZ,aAAO,MAAM,oCAAoC,EAAE,IAAI,CAAC;AACxD,eAAS,EAAE,kDAAkD,qBAAqB,CAAC;AAAA,IACrF,UAAE;AACA,iBAAW,KAAK;AAChB,6BAAuB,KAAK;AAC5B,kBAAY,UAAU;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,UAAU,iBAAiB,sBAAsB,CAAC,CAAC;AAEvD,QAAM,UAAU,MAAM;AACpB,YAAQ,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAC1B,GAAG,CAAC,SAAS,YAAY,CAAC;AAE1B,MAAI,SAAS,YAAY;AACvB,WACE,qBAAC,SAAI,WAAU,qBACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,OAAO,EAAE,2CAA2C,YAAY;AAAA,UAChE,OAAO,SAAS;AAAA,UAChB,UAAU,CAAC,cAA+B,iBAAiB,EAAE,GAAG,UAAU,UAAU,CAAC;AAAA;AAAA,MACvF;AAAA,MACA,qBAAC,SAAI,WAAU,eACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,WAAU;AAAA,YAET,YAAE,uCAAuC,iBAAiB;AAAA;AAAA,QAC7D;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,MAAK;AAAA,YACL,KAAK;AAAA,YACL,KAAK;AAAA,YACL,WAAU;AAAA,YACV,OAAO,SAAS;AAAA,YAChB,UAAU,CAAC,MAAM;AACf,oBAAM,OAAO,OAAO,EAAE,OAAO,KAAK;AAClC,+BAAiB,EAAE,GAAG,UAAU,OAAO,OAAO,SAAS,IAAI,IAAI,OAAO,SAAS,MAAM,CAAC;AAAA,YACxF;AAAA;AAAA,QACF;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,eACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,WAAU;AAAA,YAET,YAAE,6CAA6C,cAAc;AAAA;AAAA,QAChE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,SAAS;AAAA,YAChB,eAAe,CAAC,UAAU,iBAAiB,EAAE,GAAG,UAAU,QAAQ,MAAmC,CAAC;AAAA,YAEtG;AAAA,kCAAC,iBAAc,IAAG,uBAAsB,MAAK,MAC3C,8BAAC,eAAY,GACf;AAAA,cACA,qBAAC,iBACC;AAAA,oCAAC,cAAW,OAAM,cAAc,YAAE,4CAA4C,YAAY,GAAE;AAAA,gBAC5F,oBAAC,cAAW,OAAM,YAAY,YAAE,0CAA0C,UAAU,GAAE;AAAA,iBACxF;AAAA;AAAA;AAAA,QACF;AAAA,SACF;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,wBACb;AAAA,wBAAC,SAAI,WAAU,yBACb;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,SAAS;AAAA,QAChB,UAAU,CAAC,cAAc,iBAAiB,EAAE,GAAG,UAAU,UAAU,CAAC;AAAA;AAAA,IACtE,GACF;AAAA,IACA,oBAAC,SAAI,WAAU,kBACb;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,OAAM;AAAA,QACN,YAAY,CAAC,SAAS;AAAA,QACtB,gBAAgB,EAAE,SAAS,EAAE,4DAA4D,SAAS,EAAE;AAAA,QACpG;AAAA,QACA;AAAA,QACA,QAAQ,SAAS;AAAA,QACjB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,CAAC,SAAS;AAAA,QAClB,YAAY;AAAA,QACZ,cAAc,EAAE,kDAAkD,uCAAuC;AAAA;AAAA,IAC3G,GACF;AAAA,IACA,oBAAC,0BAAuB,UAAoB,SAAkB,OAAc;AAAA,KAC9E;AAEJ;AAEA,IAAO,wBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
3
|
+
import {
|
|
4
|
+
buildCollectionTags,
|
|
5
|
+
debugCrudCache,
|
|
6
|
+
isCrudCacheEnabled,
|
|
7
|
+
resolveCrudCache
|
|
8
|
+
} from "@open-mercato/shared/lib/crud/cache";
|
|
2
9
|
import { Notification } from "../data/entities.js";
|
|
3
10
|
import { listNotificationsSchema, createNotificationSchema } from "../data/validators.js";
|
|
4
11
|
import { toNotificationDto } from "../lib/notificationMapper.js";
|
|
@@ -19,14 +26,50 @@ const metadata = {
|
|
|
19
26
|
GET: { requireAuth: true },
|
|
20
27
|
POST: { requireAuth: true, requireFeatures: ["notifications.create"] }
|
|
21
28
|
};
|
|
29
|
+
const NOTIFICATIONS_LIST_TTL_MS = 1e4;
|
|
30
|
+
const NOTIFICATIONS_LIST_CACHE_VERSION = 1;
|
|
31
|
+
const NOTIFICATIONS_LIST_RESOURCE = "notifications.notification";
|
|
32
|
+
function buildNotificationsListCacheKey(userId, input) {
|
|
33
|
+
const filterSignature = JSON.stringify({
|
|
34
|
+
status: Array.isArray(input.status) ? [...input.status].sort((left, right) => left.localeCompare(right)) : input.status ?? null,
|
|
35
|
+
type: input.type ?? null,
|
|
36
|
+
severity: input.severity ?? null,
|
|
37
|
+
sourceEntityType: input.sourceEntityType ?? null,
|
|
38
|
+
sourceEntityId: input.sourceEntityId ?? null,
|
|
39
|
+
since: input.since ?? null,
|
|
40
|
+
page: input.page,
|
|
41
|
+
pageSize: input.pageSize
|
|
42
|
+
});
|
|
43
|
+
return `notifications:list:v${NOTIFICATIONS_LIST_CACHE_VERSION}:u=${userId}:filters=${filterSignature}`;
|
|
44
|
+
}
|
|
45
|
+
function isNotificationsListPayload(value) {
|
|
46
|
+
if (!value || typeof value !== "object") return false;
|
|
47
|
+
const payload = value;
|
|
48
|
+
return Array.isArray(payload.items) && typeof payload.total === "number" && typeof payload.page === "number" && typeof payload.pageSize === "number" && typeof payload.totalPages === "number";
|
|
49
|
+
}
|
|
22
50
|
async function GET(req) {
|
|
23
51
|
const { ctx, scope } = await resolveNotificationContext(req);
|
|
24
52
|
const em = ctx.container.resolve("em");
|
|
25
53
|
const url = new URL(req.url);
|
|
26
54
|
const queryParams = Object.fromEntries(url.searchParams.entries());
|
|
27
55
|
const input = listNotificationsSchema.parse(queryParams);
|
|
56
|
+
const userId = scope.userId;
|
|
57
|
+
const cache = userId && isCrudCacheEnabled() ? resolveCrudCache(ctx.container) : null;
|
|
58
|
+
const cacheKey = cache && userId ? buildNotificationsListCacheKey(userId, input) : null;
|
|
59
|
+
if (cache && cacheKey) {
|
|
60
|
+
try {
|
|
61
|
+
const cached = await runWithCacheTenant(scope.tenantId, () => cache.get(cacheKey));
|
|
62
|
+
if (isNotificationsListPayload(cached)) {
|
|
63
|
+
return Response.json(cached);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
debugCrudCache("notifications-list-cache-read-failed", {
|
|
67
|
+
error: error instanceof Error ? error.message : String(error)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
28
71
|
const filters = {
|
|
29
|
-
recipientUserId:
|
|
72
|
+
recipientUserId: userId,
|
|
30
73
|
tenantId: scope.tenantId,
|
|
31
74
|
...buildNotificationReadScopeWhere(scope)
|
|
32
75
|
};
|
|
@@ -59,13 +102,29 @@ async function GET(req) {
|
|
|
59
102
|
em.count(Notification, filters)
|
|
60
103
|
]);
|
|
61
104
|
const items = notifications.map(toNotificationDto);
|
|
62
|
-
|
|
105
|
+
const payload = {
|
|
63
106
|
items,
|
|
64
107
|
total,
|
|
65
108
|
page: input.page,
|
|
66
109
|
pageSize: input.pageSize,
|
|
67
110
|
totalPages: Math.ceil(total / input.pageSize)
|
|
68
|
-
}
|
|
111
|
+
};
|
|
112
|
+
if (cache && cacheKey) {
|
|
113
|
+
try {
|
|
114
|
+
await runWithCacheTenant(
|
|
115
|
+
scope.tenantId,
|
|
116
|
+
() => cache.set(cacheKey, payload, {
|
|
117
|
+
ttl: NOTIFICATIONS_LIST_TTL_MS,
|
|
118
|
+
tags: buildCollectionTags(NOTIFICATIONS_LIST_RESOURCE, scope.tenantId, [null])
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
debugCrudCache("notifications-list-cache-write-failed", {
|
|
123
|
+
error: error instanceof Error ? error.message : String(error)
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return Response.json(payload);
|
|
69
128
|
}
|
|
70
129
|
async function POST(req) {
|
|
71
130
|
const { service, scope, ctx } = await resolveNotificationContext(req);
|