@open-mercato/core 0.4.2-canary-cf7d9b4116 → 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.
- package/dist/modules/auth/lib/setup-app.js +2 -0
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/catalog/analytics.js +27 -0
- package/dist/modules/catalog/analytics.js.map +7 -0
- package/dist/modules/customers/analytics.js +50 -0
- package/dist/modules/customers/analytics.js.map +7 -0
- package/dist/modules/dashboards/acl.js +2 -1
- package/dist/modules/dashboards/acl.js.map +2 -2
- package/dist/modules/dashboards/api/widgets/data/route.js +187 -0
- package/dist/modules/dashboards/api/widgets/data/route.js.map +7 -0
- package/dist/modules/dashboards/cli.js +142 -1
- package/dist/modules/dashboards/cli.js.map +2 -2
- package/dist/modules/dashboards/di.js +11 -0
- package/dist/modules/dashboards/di.js.map +7 -0
- package/dist/modules/dashboards/lib/aggregations.js +162 -0
- package/dist/modules/dashboards/lib/aggregations.js.map +7 -0
- package/dist/modules/dashboards/lib/formatters.js +34 -0
- package/dist/modules/dashboards/lib/formatters.js.map +7 -0
- package/dist/modules/dashboards/seed/analytics.js +383 -0
- package/dist/modules/dashboards/seed/analytics.js.map +7 -0
- package/dist/modules/dashboards/services/analyticsRegistry.js +52 -0
- package/dist/modules/dashboards/services/analyticsRegistry.js.map +7 -0
- package/dist/modules/dashboards/services/widgetDataService.js +207 -0
- package/dist/modules/dashboards/services/widgetDataService.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +128 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +126 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +151 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +126 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +16 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +123 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js +18 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +128 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js +21 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +211 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js +19 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +131 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js +19 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +153 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/config.js +22 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/config.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +180 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +7 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js +25 -0
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js.map +7 -0
- package/dist/modules/sales/analytics.js +67 -0
- package/dist/modules/sales/analytics.js.map +7 -0
- package/package.json +2 -2
- package/src/modules/auth/lib/setup-app.ts +2 -0
- package/src/modules/catalog/analytics.ts +24 -0
- package/src/modules/customers/analytics.ts +47 -0
- package/src/modules/dashboards/acl.ts +1 -0
- package/src/modules/dashboards/api/widgets/data/route.ts +221 -0
- package/src/modules/dashboards/cli.ts +164 -1
- package/src/modules/dashboards/di.ts +9 -0
- package/src/modules/dashboards/i18n/de.json +115 -1
- package/src/modules/dashboards/i18n/en.json +115 -1
- package/src/modules/dashboards/i18n/es.json +115 -1
- package/src/modules/dashboards/i18n/pl.json +115 -1
- package/src/modules/dashboards/lib/__tests__/aggregations.test.ts +327 -0
- package/src/modules/dashboards/lib/__tests__/formatters.test.ts +128 -0
- package/src/modules/dashboards/lib/aggregations.ts +225 -0
- package/src/modules/dashboards/lib/formatters.ts +36 -0
- package/src/modules/dashboards/seed/analytics.ts +405 -0
- package/src/modules/dashboards/services/analyticsRegistry.ts +79 -0
- package/src/modules/dashboards/services/widgetDataService.ts +329 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +135 -0
- package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +133 -0
- package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +154 -0
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +133 -0
- package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +17 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +137 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/config.ts +20 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +135 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/config.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +220 -0
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/config.ts +21 -0
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +131 -0
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/top-customers/config.ts +21 -0
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +161 -0
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.ts +24 -0
- package/src/modules/dashboards/widgets/dashboard/top-products/config.ts +27 -0
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +181 -0
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.ts +24 -0
- package/src/modules/sales/analytics.ts +64 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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 { 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, InlineDateRangeSelect, type DateRangePreset } from '@open-mercato/ui/backend/date-range'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopProductsSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyCompact } from '../../../lib/formatters'\n\nasync function fetchTopProductsData(settings: TopProductsSettings): 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 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 products data')\n }\n\n return call.result as WidgetDataResponse\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 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 const fetchingRef = React.useRef(false)\n\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)\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 } catch (err) {\n console.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, 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 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 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 id=\"top-products-layout\"\n 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\"\n value={hydrated.layout}\n onChange={(e) => onSettingsChange({ ...hydrated, layout: e.target.value as 'horizontal' | 'vertical' })}\n >\n <option value=\"horizontal\">{t('dashboards.analytics.settings.horizontal', 'Horizontal')}</option>\n <option value=\"vertical\">{t('dashboards.analytics.settings.vertical', 'Vertical')}</option>\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={formatCurrencyCompact}\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 </div>\n )\n}\n\nexport default TopProductsWidget\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import TopProductsWidget from "./widget.client.js";
|
|
2
|
+
import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
|
|
3
|
+
const widget = {
|
|
4
|
+
metadata: {
|
|
5
|
+
id: "dashboards.analytics.topProducts",
|
|
6
|
+
title: "Top Products",
|
|
7
|
+
description: "Top-selling products by revenue",
|
|
8
|
+
features: ["analytics.view", "sales.orders.view"],
|
|
9
|
+
defaultSize: "md",
|
|
10
|
+
defaultEnabled: false,
|
|
11
|
+
defaultSettings: DEFAULT_SETTINGS,
|
|
12
|
+
tags: ["analytics", "sales", "products", "chart"],
|
|
13
|
+
category: "analytics",
|
|
14
|
+
icon: "bar-chart-2",
|
|
15
|
+
supportsRefresh: true
|
|
16
|
+
},
|
|
17
|
+
Widget: TopProductsWidget,
|
|
18
|
+
hydrateSettings,
|
|
19
|
+
dehydrateSettings: (s) => ({ dateRange: s.dateRange, limit: s.limit, layout: s.layout })
|
|
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-products/widget.ts"],
|
|
4
|
+
"sourcesContent": ["import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport TopProductsWidget from './widget.client'\nimport { DEFAULT_SETTINGS, hydrateSettings, type TopProductsSettings } from './config'\n\nconst widget: DashboardWidgetModule<TopProductsSettings> = {\n metadata: {\n id: 'dashboards.analytics.topProducts',\n title: 'Top Products',\n description: 'Top-selling products by revenue',\n features: ['analytics.view', 'sales.orders.view'],\n defaultSize: 'md',\n defaultEnabled: false,\n defaultSettings: DEFAULT_SETTINGS,\n tags: ['analytics', 'sales', 'products', 'chart'],\n category: 'analytics',\n icon: 'bar-chart-2',\n supportsRefresh: true,\n },\n Widget: TopProductsWidget,\n hydrateSettings,\n dehydrateSettings: (s) => ({ dateRange: s.dateRange, limit: s.limit, layout: s.layout }),\n}\n\nexport default widget\n"],
|
|
5
|
+
"mappings": "AACA,OAAO,uBAAuB;AAC9B,SAAS,kBAAkB,uBAAiD;AAE5E,MAAM,SAAqD;AAAA,EACzD,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,YAAY,OAAO;AAAA,IAChD,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,OAAO,QAAQ,EAAE,OAAO;AACxF;AAEA,IAAO,iBAAQ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const analyticsConfig = {
|
|
2
|
+
entities: [
|
|
3
|
+
{
|
|
4
|
+
entityId: "sales:orders",
|
|
5
|
+
requiredFeatures: ["sales.orders.view"],
|
|
6
|
+
entityConfig: {
|
|
7
|
+
tableName: "sales_orders",
|
|
8
|
+
dateField: "placed_at",
|
|
9
|
+
defaultScopeFields: ["tenant_id", "organization_id"]
|
|
10
|
+
},
|
|
11
|
+
fieldMappings: {
|
|
12
|
+
id: { dbColumn: "id", type: "uuid" },
|
|
13
|
+
grandTotalGrossAmount: { dbColumn: "grand_total_gross_amount", type: "numeric" },
|
|
14
|
+
grandTotalNetAmount: { dbColumn: "grand_total_net_amount", type: "numeric" },
|
|
15
|
+
subtotalGrossAmount: { dbColumn: "subtotal_gross_amount", type: "numeric" },
|
|
16
|
+
subtotalNetAmount: { dbColumn: "subtotal_net_amount", type: "numeric" },
|
|
17
|
+
discountTotalAmount: { dbColumn: "discount_total_amount", type: "numeric" },
|
|
18
|
+
taxTotalAmount: { dbColumn: "tax_total_amount", type: "numeric" },
|
|
19
|
+
lineItemCount: { dbColumn: "line_item_count", type: "numeric" },
|
|
20
|
+
status: { dbColumn: "status", type: "text" },
|
|
21
|
+
fulfillmentStatus: { dbColumn: "fulfillment_status", type: "text" },
|
|
22
|
+
paymentStatus: { dbColumn: "payment_status", type: "text" },
|
|
23
|
+
customerEntityId: { dbColumn: "customer_entity_id", type: "uuid" },
|
|
24
|
+
channelId: { dbColumn: "channel_id", type: "uuid" },
|
|
25
|
+
placedAt: { dbColumn: "placed_at", type: "timestamp" },
|
|
26
|
+
currencyCode: { dbColumn: "currency_code", type: "text" },
|
|
27
|
+
shippingAddressSnapshot: { dbColumn: "shipping_address_snapshot", type: "jsonb" }
|
|
28
|
+
},
|
|
29
|
+
labelResolvers: {
|
|
30
|
+
customerEntityId: { table: "customer_entities", idColumn: "id", labelColumn: "display_name" },
|
|
31
|
+
channelId: { table: "sales_channels", idColumn: "id", labelColumn: "name" }
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
entityId: "sales:order_lines",
|
|
36
|
+
requiredFeatures: ["sales.orders.view"],
|
|
37
|
+
entityConfig: {
|
|
38
|
+
tableName: "sales_order_lines",
|
|
39
|
+
dateField: "created_at",
|
|
40
|
+
defaultScopeFields: ["tenant_id", "organization_id"]
|
|
41
|
+
},
|
|
42
|
+
fieldMappings: {
|
|
43
|
+
id: { dbColumn: "id", type: "uuid" },
|
|
44
|
+
totalGrossAmount: { dbColumn: "total_gross_amount", type: "numeric" },
|
|
45
|
+
totalNetAmount: { dbColumn: "total_net_amount", type: "numeric" },
|
|
46
|
+
unitGrossPrice: { dbColumn: "unit_gross_price", type: "numeric" },
|
|
47
|
+
quantity: { dbColumn: "quantity", type: "numeric" },
|
|
48
|
+
productId: { dbColumn: "product_id", type: "uuid" },
|
|
49
|
+
productVariantId: { dbColumn: "product_variant_id", type: "uuid" },
|
|
50
|
+
status: { dbColumn: "status", type: "text" },
|
|
51
|
+
createdAt: { dbColumn: "created_at", type: "timestamp" }
|
|
52
|
+
},
|
|
53
|
+
labelResolvers: {
|
|
54
|
+
productId: { table: "catalog_products", idColumn: "id", labelColumn: "title" },
|
|
55
|
+
productVariantId: { table: "catalog_product_variants", idColumn: "id", labelColumn: "name" }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
};
|
|
60
|
+
var analytics_default = analyticsConfig;
|
|
61
|
+
const config = analyticsConfig;
|
|
62
|
+
export {
|
|
63
|
+
analyticsConfig,
|
|
64
|
+
config,
|
|
65
|
+
analytics_default as default
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=analytics.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/modules/sales/analytics.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AnalyticsModuleConfig } from '@open-mercato/shared/modules/analytics'\n\nexport const analyticsConfig: AnalyticsModuleConfig = {\n entities: [\n {\n entityId: 'sales:orders',\n requiredFeatures: ['sales.orders.view'],\n entityConfig: {\n tableName: 'sales_orders',\n dateField: 'placed_at',\n defaultScopeFields: ['tenant_id', 'organization_id'],\n },\n fieldMappings: {\n id: { dbColumn: 'id', type: 'uuid' },\n grandTotalGrossAmount: { dbColumn: 'grand_total_gross_amount', type: 'numeric' },\n grandTotalNetAmount: { dbColumn: 'grand_total_net_amount', type: 'numeric' },\n subtotalGrossAmount: { dbColumn: 'subtotal_gross_amount', type: 'numeric' },\n subtotalNetAmount: { dbColumn: 'subtotal_net_amount', type: 'numeric' },\n discountTotalAmount: { dbColumn: 'discount_total_amount', type: 'numeric' },\n taxTotalAmount: { dbColumn: 'tax_total_amount', type: 'numeric' },\n lineItemCount: { dbColumn: 'line_item_count', type: 'numeric' },\n status: { dbColumn: 'status', type: 'text' },\n fulfillmentStatus: { dbColumn: 'fulfillment_status', type: 'text' },\n paymentStatus: { dbColumn: 'payment_status', type: 'text' },\n customerEntityId: { dbColumn: 'customer_entity_id', type: 'uuid' },\n channelId: { dbColumn: 'channel_id', type: 'uuid' },\n placedAt: { dbColumn: 'placed_at', type: 'timestamp' },\n currencyCode: { dbColumn: 'currency_code', type: 'text' },\n shippingAddressSnapshot: { dbColumn: 'shipping_address_snapshot', type: 'jsonb' },\n },\n labelResolvers: {\n customerEntityId: { table: 'customer_entities', idColumn: 'id', labelColumn: 'display_name' },\n channelId: { table: 'sales_channels', idColumn: 'id', labelColumn: 'name' },\n },\n },\n {\n entityId: 'sales:order_lines',\n requiredFeatures: ['sales.orders.view'],\n entityConfig: {\n tableName: 'sales_order_lines',\n dateField: 'created_at',\n defaultScopeFields: ['tenant_id', 'organization_id'],\n },\n fieldMappings: {\n id: { dbColumn: 'id', type: 'uuid' },\n totalGrossAmount: { dbColumn: 'total_gross_amount', type: 'numeric' },\n totalNetAmount: { dbColumn: 'total_net_amount', type: 'numeric' },\n unitGrossPrice: { dbColumn: 'unit_gross_price', type: 'numeric' },\n quantity: { dbColumn: 'quantity', type: 'numeric' },\n productId: { dbColumn: 'product_id', type: 'uuid' },\n productVariantId: { dbColumn: 'product_variant_id', type: 'uuid' },\n status: { dbColumn: 'status', type: 'text' },\n createdAt: { dbColumn: 'created_at', type: 'timestamp' },\n },\n labelResolvers: {\n productId: { table: 'catalog_products', idColumn: 'id', labelColumn: 'title' },\n productVariantId: { table: 'catalog_product_variants', idColumn: 'id', labelColumn: 'name' },\n },\n },\n ],\n}\n\nexport default analyticsConfig\nexport const config = analyticsConfig\n"],
|
|
5
|
+
"mappings": "AAEO,MAAM,kBAAyC;AAAA,EACpD,UAAU;AAAA,IACR;AAAA,MACE,UAAU;AAAA,MACV,kBAAkB,CAAC,mBAAmB;AAAA,MACtC,cAAc;AAAA,QACZ,WAAW;AAAA,QACX,WAAW;AAAA,QACX,oBAAoB,CAAC,aAAa,iBAAiB;AAAA,MACrD;AAAA,MACA,eAAe;AAAA,QACb,IAAI,EAAE,UAAU,MAAM,MAAM,OAAO;AAAA,QACnC,uBAAuB,EAAE,UAAU,4BAA4B,MAAM,UAAU;AAAA,QAC/E,qBAAqB,EAAE,UAAU,0BAA0B,MAAM,UAAU;AAAA,QAC3E,qBAAqB,EAAE,UAAU,yBAAyB,MAAM,UAAU;AAAA,QAC1E,mBAAmB,EAAE,UAAU,uBAAuB,MAAM,UAAU;AAAA,QACtE,qBAAqB,EAAE,UAAU,yBAAyB,MAAM,UAAU;AAAA,QAC1E,gBAAgB,EAAE,UAAU,oBAAoB,MAAM,UAAU;AAAA,QAChE,eAAe,EAAE,UAAU,mBAAmB,MAAM,UAAU;AAAA,QAC9D,QAAQ,EAAE,UAAU,UAAU,MAAM,OAAO;AAAA,QAC3C,mBAAmB,EAAE,UAAU,sBAAsB,MAAM,OAAO;AAAA,QAClE,eAAe,EAAE,UAAU,kBAAkB,MAAM,OAAO;AAAA,QAC1D,kBAAkB,EAAE,UAAU,sBAAsB,MAAM,OAAO;AAAA,QACjE,WAAW,EAAE,UAAU,cAAc,MAAM,OAAO;AAAA,QAClD,UAAU,EAAE,UAAU,aAAa,MAAM,YAAY;AAAA,QACrD,cAAc,EAAE,UAAU,iBAAiB,MAAM,OAAO;AAAA,QACxD,yBAAyB,EAAE,UAAU,6BAA6B,MAAM,QAAQ;AAAA,MAClF;AAAA,MACA,gBAAgB;AAAA,QACd,kBAAkB,EAAE,OAAO,qBAAqB,UAAU,MAAM,aAAa,eAAe;AAAA,QAC5F,WAAW,EAAE,OAAO,kBAAkB,UAAU,MAAM,aAAa,OAAO;AAAA,MAC5E;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU;AAAA,MACV,kBAAkB,CAAC,mBAAmB;AAAA,MACtC,cAAc;AAAA,QACZ,WAAW;AAAA,QACX,WAAW;AAAA,QACX,oBAAoB,CAAC,aAAa,iBAAiB;AAAA,MACrD;AAAA,MACA,eAAe;AAAA,QACb,IAAI,EAAE,UAAU,MAAM,MAAM,OAAO;AAAA,QACnC,kBAAkB,EAAE,UAAU,sBAAsB,MAAM,UAAU;AAAA,QACpE,gBAAgB,EAAE,UAAU,oBAAoB,MAAM,UAAU;AAAA,QAChE,gBAAgB,EAAE,UAAU,oBAAoB,MAAM,UAAU;AAAA,QAChE,UAAU,EAAE,UAAU,YAAY,MAAM,UAAU;AAAA,QAClD,WAAW,EAAE,UAAU,cAAc,MAAM,OAAO;AAAA,QAClD,kBAAkB,EAAE,UAAU,sBAAsB,MAAM,OAAO;AAAA,QACjE,QAAQ,EAAE,UAAU,UAAU,MAAM,OAAO;AAAA,QAC3C,WAAW,EAAE,UAAU,cAAc,MAAM,YAAY;AAAA,MACzD;AAAA,MACA,gBAAgB;AAAA,QACd,WAAW,EAAE,OAAO,oBAAoB,UAAU,MAAM,aAAa,QAAQ;AAAA,QAC7E,kBAAkB,EAAE,OAAO,4BAA4B,UAAU,MAAM,aAAa,OAAO;AAAA,MAC7F;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,oBAAQ;AACR,MAAM,SAAS;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.2-canary-
|
|
3
|
+
"version": "0.4.2-canary-e6bf6a353e",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.2-canary-
|
|
210
|
+
"@open-mercato/shared": "0.4.2-canary-e6bf6a353e",
|
|
211
211
|
"@xyflow/react": "^12.6.0",
|
|
212
212
|
"date-fns": "^4.1.0",
|
|
213
213
|
"date-fns-tz": "^3.2.0"
|
|
@@ -394,6 +394,7 @@ async function ensureDefaultRoleAcls(
|
|
|
394
394
|
'example.*',
|
|
395
395
|
'dashboards.*',
|
|
396
396
|
'dashboards.admin.assign-widgets',
|
|
397
|
+
'analytics.view',
|
|
397
398
|
'api_keys.*',
|
|
398
399
|
'perspectives.use',
|
|
399
400
|
'perspectives.role_defaults',
|
|
@@ -424,6 +425,7 @@ async function ensureDefaultRoleAcls(
|
|
|
424
425
|
'example.widgets.*',
|
|
425
426
|
'dashboards.view',
|
|
426
427
|
'dashboards.configure',
|
|
428
|
+
'analytics.view',
|
|
427
429
|
'audit_logs.undo_self',
|
|
428
430
|
'perspectives.use',
|
|
429
431
|
'staff.leave_requests.send',
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnalyticsModuleConfig } from '@open-mercato/shared/modules/analytics'
|
|
2
|
+
|
|
3
|
+
export const analyticsConfig: AnalyticsModuleConfig = {
|
|
4
|
+
entities: [
|
|
5
|
+
{
|
|
6
|
+
entityId: 'catalog:products',
|
|
7
|
+
requiredFeatures: ['catalog.view'],
|
|
8
|
+
entityConfig: {
|
|
9
|
+
tableName: 'catalog_products',
|
|
10
|
+
dateField: 'created_at',
|
|
11
|
+
defaultScopeFields: ['tenant_id', 'organization_id'],
|
|
12
|
+
},
|
|
13
|
+
fieldMappings: {
|
|
14
|
+
id: { dbColumn: 'id', type: 'uuid' },
|
|
15
|
+
name: { dbColumn: 'name', type: 'text' },
|
|
16
|
+
status: { dbColumn: 'status', type: 'text' },
|
|
17
|
+
createdAt: { dbColumn: 'created_at', type: 'timestamp' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default analyticsConfig
|
|
24
|
+
export const config = analyticsConfig
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AnalyticsModuleConfig } from '@open-mercato/shared/modules/analytics'
|
|
2
|
+
|
|
3
|
+
export const analyticsConfig: AnalyticsModuleConfig = {
|
|
4
|
+
entities: [
|
|
5
|
+
{
|
|
6
|
+
entityId: 'customers:entities',
|
|
7
|
+
requiredFeatures: ['customers.view'],
|
|
8
|
+
entityConfig: {
|
|
9
|
+
tableName: 'customer_entities',
|
|
10
|
+
dateField: 'created_at',
|
|
11
|
+
defaultScopeFields: ['tenant_id', 'organization_id'],
|
|
12
|
+
},
|
|
13
|
+
fieldMappings: {
|
|
14
|
+
id: { dbColumn: 'id', type: 'uuid' },
|
|
15
|
+
kind: { dbColumn: 'kind', type: 'text' },
|
|
16
|
+
status: { dbColumn: 'status', type: 'text' },
|
|
17
|
+
lifecycleStage: { dbColumn: 'lifecycle_stage', type: 'text' },
|
|
18
|
+
createdAt: { dbColumn: 'created_at', type: 'timestamp' },
|
|
19
|
+
displayName: { dbColumn: 'display_name', type: 'text' },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
entityId: 'customers:deals',
|
|
24
|
+
requiredFeatures: ['customers.deals.view'],
|
|
25
|
+
entityConfig: {
|
|
26
|
+
tableName: 'customer_deals',
|
|
27
|
+
dateField: 'created_at',
|
|
28
|
+
defaultScopeFields: ['tenant_id', 'organization_id'],
|
|
29
|
+
},
|
|
30
|
+
fieldMappings: {
|
|
31
|
+
id: { dbColumn: 'id', type: 'uuid' },
|
|
32
|
+
valueAmount: { dbColumn: 'value_amount', type: 'numeric' },
|
|
33
|
+
status: { dbColumn: 'status', type: 'text' },
|
|
34
|
+
pipelineStage: { dbColumn: 'pipeline_stage', type: 'text' },
|
|
35
|
+
probability: { dbColumn: 'probability', type: 'numeric' },
|
|
36
|
+
createdAt: { dbColumn: 'created_at', type: 'timestamp' },
|
|
37
|
+
expectedCloseAt: { dbColumn: 'expected_close_at', type: 'timestamp' },
|
|
38
|
+
},
|
|
39
|
+
labelResolvers: {
|
|
40
|
+
customerEntityId: { table: 'customer_entities', idColumn: 'id', labelColumn: 'display_name' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default analyticsConfig
|
|
47
|
+
export const config = analyticsConfig
|
|
@@ -2,6 +2,7 @@ export const features = [
|
|
|
2
2
|
{ id: 'dashboards.view', title: 'View dashboard', module: 'dashboards' },
|
|
3
3
|
{ id: 'dashboards.configure', title: 'Customize dashboard layout', module: 'dashboards' },
|
|
4
4
|
{ id: 'dashboards.admin.assign-widgets', title: 'Manage dashboard widget availability', module: 'dashboards' },
|
|
5
|
+
{ id: 'analytics.view', title: 'View analytics widgets', module: 'dashboards' },
|
|
5
6
|
]
|
|
6
7
|
|
|
7
8
|
export default features
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
4
|
+
import type { CacheStrategy } from '@open-mercato/cache'
|
|
5
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
6
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
|
+
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
8
|
+
import {
|
|
9
|
+
createWidgetDataService,
|
|
10
|
+
type WidgetDataRequest,
|
|
11
|
+
WidgetDataValidationError,
|
|
12
|
+
} from '../../../services/widgetDataService'
|
|
13
|
+
import type { AnalyticsRegistry } from '../../../services/analyticsRegistry'
|
|
14
|
+
import type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
15
|
+
import { dashboardsTag, dashboardsErrorSchema } from '../../openapi'
|
|
16
|
+
|
|
17
|
+
export const metadata = {
|
|
18
|
+
POST: { requireAuth: true, requireFeatures: ['analytics.view'] },
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const aggregateFunctionSchema = z.enum(['count', 'sum', 'avg', 'min', 'max'])
|
|
22
|
+
const dateGranularitySchema = z.enum(['day', 'week', 'month', 'quarter', 'year'])
|
|
23
|
+
const dateRangePresetSchema = z.enum([
|
|
24
|
+
'today',
|
|
25
|
+
'yesterday',
|
|
26
|
+
'this_week',
|
|
27
|
+
'last_week',
|
|
28
|
+
'this_month',
|
|
29
|
+
'last_month',
|
|
30
|
+
'this_quarter',
|
|
31
|
+
'last_quarter',
|
|
32
|
+
'this_year',
|
|
33
|
+
'last_year',
|
|
34
|
+
'last_7_days',
|
|
35
|
+
'last_30_days',
|
|
36
|
+
'last_90_days',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
const filterOperatorSchema = z.enum([
|
|
40
|
+
'eq',
|
|
41
|
+
'neq',
|
|
42
|
+
'gt',
|
|
43
|
+
'gte',
|
|
44
|
+
'lt',
|
|
45
|
+
'lte',
|
|
46
|
+
'in',
|
|
47
|
+
'not_in',
|
|
48
|
+
'is_null',
|
|
49
|
+
'is_not_null',
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
const widgetDataRequestSchema = z.object({
|
|
53
|
+
entityType: z.string().min(1),
|
|
54
|
+
metric: z.object({
|
|
55
|
+
field: z.string().min(1),
|
|
56
|
+
aggregate: aggregateFunctionSchema,
|
|
57
|
+
}),
|
|
58
|
+
groupBy: z
|
|
59
|
+
.object({
|
|
60
|
+
field: z.string().min(1),
|
|
61
|
+
granularity: dateGranularitySchema.optional(),
|
|
62
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
63
|
+
resolveLabels: z.boolean().optional(),
|
|
64
|
+
})
|
|
65
|
+
.optional(),
|
|
66
|
+
filters: z
|
|
67
|
+
.array(
|
|
68
|
+
z.object({
|
|
69
|
+
field: z.string().min(1),
|
|
70
|
+
operator: filterOperatorSchema,
|
|
71
|
+
value: z.unknown().optional(),
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
.optional(),
|
|
75
|
+
dateRange: z
|
|
76
|
+
.object({
|
|
77
|
+
field: z.string().min(1),
|
|
78
|
+
preset: dateRangePresetSchema,
|
|
79
|
+
})
|
|
80
|
+
.optional(),
|
|
81
|
+
comparison: z
|
|
82
|
+
.object({
|
|
83
|
+
type: z.enum(['previous_period', 'previous_year']),
|
|
84
|
+
})
|
|
85
|
+
.optional(),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const widgetDataItemSchema = z.object({
|
|
89
|
+
groupKey: z.unknown(),
|
|
90
|
+
groupLabel: z.string().optional(),
|
|
91
|
+
value: z.number().nullable(),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const widgetDataResponseSchema = z.object({
|
|
95
|
+
value: z.number().nullable(),
|
|
96
|
+
data: z.array(widgetDataItemSchema),
|
|
97
|
+
comparison: z
|
|
98
|
+
.object({
|
|
99
|
+
value: z.number().nullable(),
|
|
100
|
+
change: z.number(),
|
|
101
|
+
direction: z.enum(['up', 'down', 'unchanged']),
|
|
102
|
+
})
|
|
103
|
+
.optional(),
|
|
104
|
+
metadata: z.object({
|
|
105
|
+
fetchedAt: z.string(),
|
|
106
|
+
recordCount: z.number(),
|
|
107
|
+
}),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
export async function POST(req: Request) {
|
|
111
|
+
const auth = await getAuthFromRequest(req)
|
|
112
|
+
if (!auth) {
|
|
113
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let body: unknown
|
|
117
|
+
try {
|
|
118
|
+
body = await req.json()
|
|
119
|
+
} catch {
|
|
120
|
+
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parsed = widgetDataRequestSchema.safeParse(body)
|
|
124
|
+
if (!parsed.success) {
|
|
125
|
+
return NextResponse.json(
|
|
126
|
+
{ error: 'Invalid request payload', issues: parsed.error.issues },
|
|
127
|
+
{ status: 400 },
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const container = await createRequestContainer()
|
|
132
|
+
const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')
|
|
133
|
+
|
|
134
|
+
const entityFeatures = analyticsRegistry.getRequiredFeatures(parsed.data.entityType)
|
|
135
|
+
if (entityFeatures && entityFeatures.length > 0) {
|
|
136
|
+
const rbacService = container.resolve<{
|
|
137
|
+
userHasAllFeatures: (
|
|
138
|
+
userId: string,
|
|
139
|
+
features: string[],
|
|
140
|
+
scope: { tenantId: string; organizationId?: string | null },
|
|
141
|
+
) => Promise<boolean>
|
|
142
|
+
}>('rbacService')
|
|
143
|
+
const hasAccess = await rbacService.userHasAllFeatures(auth.sub, entityFeatures, {
|
|
144
|
+
tenantId: auth.tenantId!,
|
|
145
|
+
organizationId: auth.orgId,
|
|
146
|
+
})
|
|
147
|
+
if (!hasAccess) {
|
|
148
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const em = (container.resolve('em') as EntityManager).fork({
|
|
153
|
+
clear: true,
|
|
154
|
+
freshEventManager: true,
|
|
155
|
+
useContext: true,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const tenantId = auth.tenantId ?? null
|
|
159
|
+
if (!tenantId) {
|
|
160
|
+
return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
|
|
164
|
+
|
|
165
|
+
const organizationIds = (() => {
|
|
166
|
+
if (scope?.selectedId) return [scope.selectedId]
|
|
167
|
+
if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds
|
|
168
|
+
if (scope?.allowedIds === null) return undefined
|
|
169
|
+
if (auth.orgId) return [auth.orgId]
|
|
170
|
+
return undefined
|
|
171
|
+
})()
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const cache = container.resolve<CacheStrategy>('cache')
|
|
175
|
+
const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)
|
|
176
|
+
const result = await service.fetchWidgetData(parsed.data as WidgetDataRequest)
|
|
177
|
+
return NextResponse.json(result)
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error('[widgets/data] Error:', err)
|
|
180
|
+
if (err instanceof WidgetDataValidationError) {
|
|
181
|
+
return NextResponse.json({ error: err.message }, { status: 400 })
|
|
182
|
+
}
|
|
183
|
+
return NextResponse.json(
|
|
184
|
+
{ error: 'An error occurred while processing your request' },
|
|
185
|
+
{ status: 500 },
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const widgetDataPostDoc: OpenApiMethodDoc = {
|
|
191
|
+
summary: 'Fetch aggregated data for dashboard widgets',
|
|
192
|
+
description:
|
|
193
|
+
'Executes an aggregation query against the specified entity type and returns the result. Supports date range filtering, grouping, and period-over-period comparison.',
|
|
194
|
+
tags: [dashboardsTag],
|
|
195
|
+
requestBody: {
|
|
196
|
+
contentType: 'application/json',
|
|
197
|
+
schema: widgetDataRequestSchema,
|
|
198
|
+
description: 'Widget data request configuration specifying entity type, metric, filters, and grouping.',
|
|
199
|
+
},
|
|
200
|
+
responses: [
|
|
201
|
+
{
|
|
202
|
+
status: 200,
|
|
203
|
+
description: 'Aggregated data for the widget.',
|
|
204
|
+
schema: widgetDataResponseSchema,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
errors: [
|
|
208
|
+
{ status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },
|
|
209
|
+
{ status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },
|
|
210
|
+
{ status: 403, description: 'Missing analytics.view feature', schema: dashboardsErrorSchema },
|
|
211
|
+
{ status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },
|
|
212
|
+
],
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const openApi: OpenApiRouteDoc = {
|
|
216
|
+
tag: dashboardsTag,
|
|
217
|
+
summary: 'Widget data aggregation endpoint',
|
|
218
|
+
methods: {
|
|
219
|
+
POST: widgetDataPostDoc,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
4
4
|
import { DashboardRoleWidgets } from '@open-mercato/core/modules/dashboards/data/entities'
|
|
5
5
|
import { Role } from '@open-mercato/core/modules/auth/data/entities'
|
|
6
6
|
import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
|
|
7
|
+
import { seedAnalyticsData } from './seed/analytics'
|
|
7
8
|
|
|
8
9
|
type Args = Record<string, string>
|
|
9
10
|
|
|
@@ -119,4 +120,166 @@ const seedDefaults: ModuleCli = {
|
|
|
119
120
|
},
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
const seedAnalytics: ModuleCli = {
|
|
124
|
+
command: 'seed-analytics',
|
|
125
|
+
async run(rest) {
|
|
126
|
+
const args = parseArgs(rest)
|
|
127
|
+
const tenantId = args.tenant || args.tenantId || null
|
|
128
|
+
const organizationId = args.organization || args.organizationId || args.org || null
|
|
129
|
+
const months = args.months ? parseInt(args.months, 10) : 6
|
|
130
|
+
const ordersPerMonth = args.ordersPerMonth ? parseInt(args.ordersPerMonth, 10) : 50
|
|
131
|
+
|
|
132
|
+
if (!tenantId || !organizationId) {
|
|
133
|
+
console.error('Usage: mercato dashboards seed-analytics --tenant <tenantId> --organization <organizationId> [--months 6] [--ordersPerMonth 50]')
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { resolve } = await createRequestContainer()
|
|
138
|
+
const em = resolve('em') as EntityManager
|
|
139
|
+
|
|
140
|
+
console.log(`Seeding analytics data for ${months} months with ~${ordersPerMonth} orders/month...`)
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const result = await em.transactional(async (tem) =>
|
|
144
|
+
seedAnalyticsData(tem, { tenantId, organizationId }, { months, ordersPerMonth })
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if (result.orders === 0) {
|
|
148
|
+
console.log('Analytics data already exists. Skipping seed.')
|
|
149
|
+
} else {
|
|
150
|
+
console.log(`Seeded analytics data:`)
|
|
151
|
+
console.log(` - Orders: ${result.orders}`)
|
|
152
|
+
console.log(` - Customers: ${result.customers}`)
|
|
153
|
+
console.log(` - Products: ${result.products}`)
|
|
154
|
+
console.log(` - Deals: ${result.deals}`)
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('Failed to seed analytics data:', error)
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const debugAnalytics: ModuleCli = {
|
|
163
|
+
command: 'debug-analytics',
|
|
164
|
+
async run(rest) {
|
|
165
|
+
const args = parseArgs(rest)
|
|
166
|
+
const tenantId = args.tenant || args.tenantId || null
|
|
167
|
+
const organizationId = args.organization || args.organizationId || args.org || null
|
|
168
|
+
|
|
169
|
+
if (!tenantId) {
|
|
170
|
+
console.error('Usage: mercato dashboards debug-analytics --tenant <tenantId> [--organization <organizationId>]')
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { resolve } = await createRequestContainer()
|
|
175
|
+
const em = resolve('em') as EntityManager
|
|
176
|
+
const conn = em.getConnection()
|
|
177
|
+
|
|
178
|
+
console.log('Checking analytics data...\n')
|
|
179
|
+
|
|
180
|
+
const ordersResult = await conn.execute(
|
|
181
|
+
`SELECT COUNT(*) as total, MIN(placed_at) as earliest, MAX(placed_at) as latest
|
|
182
|
+
FROM sales_orders
|
|
183
|
+
WHERE tenant_id = ? AND order_number LIKE 'SO-ANALYTICS-%'`,
|
|
184
|
+
[tenantId]
|
|
185
|
+
)
|
|
186
|
+
console.log('Orders summary:', ordersResult[0])
|
|
187
|
+
|
|
188
|
+
const recentOrders = await conn.execute(
|
|
189
|
+
`SELECT order_number, placed_at, status, grand_total_gross_amount::numeric as total
|
|
190
|
+
FROM sales_orders
|
|
191
|
+
WHERE tenant_id = ? AND order_number LIKE 'SO-ANALYTICS-%'
|
|
192
|
+
ORDER BY placed_at DESC LIMIT 5`,
|
|
193
|
+
[tenantId]
|
|
194
|
+
)
|
|
195
|
+
console.log('\nRecent orders:', recentOrders)
|
|
196
|
+
|
|
197
|
+
const januaryOrders = await conn.execute(
|
|
198
|
+
`SELECT COUNT(*) as count, SUM(grand_total_gross_amount::numeric) as total
|
|
199
|
+
FROM sales_orders
|
|
200
|
+
WHERE tenant_id = ?
|
|
201
|
+
AND order_number LIKE 'SO-ANALYTICS-%'
|
|
202
|
+
AND placed_at >= '2026-01-01'
|
|
203
|
+
AND placed_at <= '2026-01-31 23:59:59'`,
|
|
204
|
+
[tenantId]
|
|
205
|
+
)
|
|
206
|
+
console.log('\nJanuary 2026 orders:', januaryOrders[0])
|
|
207
|
+
|
|
208
|
+
const allOrders = await conn.execute(
|
|
209
|
+
`SELECT COUNT(*) as count
|
|
210
|
+
FROM sales_orders
|
|
211
|
+
WHERE tenant_id = ?`,
|
|
212
|
+
[tenantId]
|
|
213
|
+
)
|
|
214
|
+
console.log('\nTotal orders in tenant:', allOrders[0])
|
|
215
|
+
|
|
216
|
+
const orgCheck = await conn.execute(
|
|
217
|
+
`SELECT organization_id, COUNT(*) as count
|
|
218
|
+
FROM sales_orders
|
|
219
|
+
WHERE tenant_id = ? AND order_number LIKE 'SO-ANALYTICS-%'
|
|
220
|
+
GROUP BY organization_id`,
|
|
221
|
+
[tenantId]
|
|
222
|
+
)
|
|
223
|
+
console.log('\nOrders by organization:', orgCheck)
|
|
224
|
+
|
|
225
|
+
if (organizationId) {
|
|
226
|
+
const orgOrders = await conn.execute(
|
|
227
|
+
`SELECT COUNT(*) as count, SUM(grand_total_gross_amount::numeric) as total
|
|
228
|
+
FROM sales_orders
|
|
229
|
+
WHERE tenant_id = ?
|
|
230
|
+
AND organization_id = ?
|
|
231
|
+
AND placed_at >= '2026-01-01'
|
|
232
|
+
AND placed_at <= '2026-01-31 23:59:59'`,
|
|
233
|
+
[tenantId, organizationId]
|
|
234
|
+
)
|
|
235
|
+
console.log(`\nJanuary orders for org ${organizationId}:`, orgOrders[0])
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check for NULL placed_at
|
|
239
|
+
const nullPlacedAt = await conn.execute(
|
|
240
|
+
`SELECT COUNT(*) as count
|
|
241
|
+
FROM sales_orders
|
|
242
|
+
WHERE tenant_id = ? AND placed_at IS NULL`,
|
|
243
|
+
[tenantId]
|
|
244
|
+
)
|
|
245
|
+
console.log('\nOrders with NULL placed_at:', nullPlacedAt[0])
|
|
246
|
+
|
|
247
|
+
// Check non-analytics orders
|
|
248
|
+
const nonAnalytics = await conn.execute(
|
|
249
|
+
`SELECT order_number, placed_at, status, organization_id, grand_total_gross_amount::numeric as total
|
|
250
|
+
FROM sales_orders
|
|
251
|
+
WHERE tenant_id = ? AND order_number NOT LIKE 'SO-ANALYTICS-%'
|
|
252
|
+
ORDER BY placed_at DESC NULLS LAST LIMIT 10`,
|
|
253
|
+
[tenantId]
|
|
254
|
+
)
|
|
255
|
+
console.log('\nNon-analytics orders:', nonAnalytics)
|
|
256
|
+
|
|
257
|
+
// Simulate widget query
|
|
258
|
+
console.log('\n--- Simulating widget query for this_month ---')
|
|
259
|
+
const widgetQuery = await conn.execute(
|
|
260
|
+
`SELECT COALESCE(SUM(grand_total_gross_amount::numeric), 0) AS value
|
|
261
|
+
FROM sales_orders
|
|
262
|
+
WHERE tenant_id = ?
|
|
263
|
+
AND organization_id = ANY(?::uuid[])
|
|
264
|
+
AND deleted_at IS NULL
|
|
265
|
+
AND placed_at >= '2026-01-01'
|
|
266
|
+
AND placed_at <= '2026-01-31 23:59:59'`,
|
|
267
|
+
[tenantId, `{${organizationId}}`]
|
|
268
|
+
)
|
|
269
|
+
console.log('Widget query result (revenue sum):', widgetQuery[0])
|
|
270
|
+
|
|
271
|
+
const widgetCountQuery = await conn.execute(
|
|
272
|
+
`SELECT COUNT(*) AS value
|
|
273
|
+
FROM sales_orders
|
|
274
|
+
WHERE tenant_id = ?
|
|
275
|
+
AND organization_id = ANY(?::uuid[])
|
|
276
|
+
AND deleted_at IS NULL
|
|
277
|
+
AND placed_at >= '2026-01-01'
|
|
278
|
+
AND placed_at <= '2026-01-31 23:59:59'`,
|
|
279
|
+
[tenantId, `{${organizationId}}`]
|
|
280
|
+
)
|
|
281
|
+
console.log('Widget query result (order count):', widgetCountQuery[0])
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export default [seedDefaults, seedAnalytics, debugAnalytics]
|