@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.
Files changed (136) hide show
  1. package/dist/modules/auth/lib/setup-app.js +2 -0
  2. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  3. package/dist/modules/catalog/analytics.js +27 -0
  4. package/dist/modules/catalog/analytics.js.map +7 -0
  5. package/dist/modules/customers/analytics.js +50 -0
  6. package/dist/modules/customers/analytics.js.map +7 -0
  7. package/dist/modules/dashboards/acl.js +2 -1
  8. package/dist/modules/dashboards/acl.js.map +2 -2
  9. package/dist/modules/dashboards/api/widgets/data/route.js +187 -0
  10. package/dist/modules/dashboards/api/widgets/data/route.js.map +7 -0
  11. package/dist/modules/dashboards/cli.js +142 -1
  12. package/dist/modules/dashboards/cli.js.map +2 -2
  13. package/dist/modules/dashboards/di.js +11 -0
  14. package/dist/modules/dashboards/di.js.map +7 -0
  15. package/dist/modules/dashboards/lib/aggregations.js +162 -0
  16. package/dist/modules/dashboards/lib/aggregations.js.map +7 -0
  17. package/dist/modules/dashboards/lib/formatters.js +34 -0
  18. package/dist/modules/dashboards/lib/formatters.js.map +7 -0
  19. package/dist/modules/dashboards/seed/analytics.js +383 -0
  20. package/dist/modules/dashboards/seed/analytics.js.map +7 -0
  21. package/dist/modules/dashboards/services/analyticsRegistry.js +52 -0
  22. package/dist/modules/dashboards/services/analyticsRegistry.js.map +7 -0
  23. package/dist/modules/dashboards/services/widgetDataService.js +207 -0
  24. package/dist/modules/dashboards/services/widgetDataService.js.map +7 -0
  25. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js +18 -0
  26. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/config.js.map +7 -0
  27. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +128 -0
  28. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +7 -0
  29. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js +25 -0
  30. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.js.map +7 -0
  31. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js +18 -0
  32. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/config.js.map +7 -0
  33. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +126 -0
  34. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +7 -0
  35. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js +25 -0
  36. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.js.map +7 -0
  37. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js +18 -0
  38. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/config.js.map +7 -0
  39. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +151 -0
  40. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +7 -0
  41. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js +25 -0
  42. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.js.map +7 -0
  43. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js +18 -0
  44. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/config.js.map +7 -0
  45. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +126 -0
  46. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +7 -0
  47. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js +25 -0
  48. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.js.map +7 -0
  49. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +16 -0
  50. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +7 -0
  51. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +123 -0
  52. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +7 -0
  53. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +25 -0
  54. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +7 -0
  55. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js +18 -0
  56. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/config.js.map +7 -0
  57. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +128 -0
  58. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +7 -0
  59. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js +25 -0
  60. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.js.map +7 -0
  61. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js +21 -0
  62. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/config.js.map +7 -0
  63. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +211 -0
  64. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +7 -0
  65. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js +25 -0
  66. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.js.map +7 -0
  67. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js +19 -0
  68. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/config.js.map +7 -0
  69. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +131 -0
  70. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +7 -0
  71. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js +25 -0
  72. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.js.map +7 -0
  73. package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js +19 -0
  74. package/dist/modules/dashboards/widgets/dashboard/top-customers/config.js.map +7 -0
  75. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +153 -0
  76. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +7 -0
  77. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js +25 -0
  78. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.js.map +7 -0
  79. package/dist/modules/dashboards/widgets/dashboard/top-products/config.js +22 -0
  80. package/dist/modules/dashboards/widgets/dashboard/top-products/config.js.map +7 -0
  81. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +180 -0
  82. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +7 -0
  83. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js +25 -0
  84. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.js.map +7 -0
  85. package/dist/modules/sales/analytics.js +67 -0
  86. package/dist/modules/sales/analytics.js.map +7 -0
  87. package/package.json +2 -2
  88. package/src/modules/auth/lib/setup-app.ts +2 -0
  89. package/src/modules/catalog/analytics.ts +24 -0
  90. package/src/modules/customers/analytics.ts +47 -0
  91. package/src/modules/dashboards/acl.ts +1 -0
  92. package/src/modules/dashboards/api/widgets/data/route.ts +221 -0
  93. package/src/modules/dashboards/cli.ts +164 -1
  94. package/src/modules/dashboards/di.ts +9 -0
  95. package/src/modules/dashboards/i18n/de.json +115 -1
  96. package/src/modules/dashboards/i18n/en.json +115 -1
  97. package/src/modules/dashboards/i18n/es.json +115 -1
  98. package/src/modules/dashboards/i18n/pl.json +115 -1
  99. package/src/modules/dashboards/lib/__tests__/aggregations.test.ts +327 -0
  100. package/src/modules/dashboards/lib/__tests__/formatters.test.ts +128 -0
  101. package/src/modules/dashboards/lib/aggregations.ts +225 -0
  102. package/src/modules/dashboards/lib/formatters.ts +36 -0
  103. package/src/modules/dashboards/seed/analytics.ts +405 -0
  104. package/src/modules/dashboards/services/analyticsRegistry.ts +79 -0
  105. package/src/modules/dashboards/services/widgetDataService.ts +329 -0
  106. package/src/modules/dashboards/widgets/dashboard/aov-kpi/config.ts +20 -0
  107. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +135 -0
  108. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.ts +24 -0
  109. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/config.ts +20 -0
  110. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +133 -0
  111. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.ts +24 -0
  112. package/src/modules/dashboards/widgets/dashboard/orders-by-status/config.ts +20 -0
  113. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +154 -0
  114. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.ts +24 -0
  115. package/src/modules/dashboards/widgets/dashboard/orders-kpi/config.ts +20 -0
  116. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +133 -0
  117. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.ts +24 -0
  118. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +17 -0
  119. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +137 -0
  120. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +24 -0
  121. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/config.ts +20 -0
  122. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +135 -0
  123. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.ts +24 -0
  124. package/src/modules/dashboards/widgets/dashboard/revenue-trend/config.ts +24 -0
  125. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +220 -0
  126. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.ts +24 -0
  127. package/src/modules/dashboards/widgets/dashboard/sales-by-region/config.ts +21 -0
  128. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +131 -0
  129. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.ts +24 -0
  130. package/src/modules/dashboards/widgets/dashboard/top-customers/config.ts +21 -0
  131. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +161 -0
  132. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.ts +24 -0
  133. package/src/modules/dashboards/widgets/dashboard/top-products/config.ts +27 -0
  134. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +181 -0
  135. package/src/modules/dashboards/widgets/dashboard/top-products/widget.ts +24 -0
  136. package/src/modules/sales/analytics.ts +64 -0
@@ -0,0 +1,207 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ resolveDateRange,
4
+ getPreviousPeriod,
5
+ calculatePercentageChange,
6
+ determineChangeDirection,
7
+ isValidDateRangePreset
8
+ } from "@open-mercato/ui/backend/date-range";
9
+ import {
10
+ buildAggregationQuery
11
+ } from "../lib/aggregations.js";
12
+ const WIDGET_DATA_CACHE_TTL = 12e4;
13
+ const SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
14
+ class WidgetDataValidationError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "WidgetDataValidationError";
18
+ }
19
+ }
20
+ function assertSafeIdentifier(value, name) {
21
+ if (!SAFE_IDENTIFIER_PATTERN.test(value)) {
22
+ throw new Error(`Invalid ${name}: ${value}`);
23
+ }
24
+ }
25
+ class WidgetDataService {
26
+ constructor(options) {
27
+ this.em = options.em;
28
+ this.scope = options.scope;
29
+ this.registry = options.registry;
30
+ this.cache = options.cache;
31
+ }
32
+ buildCacheKey(request) {
33
+ const hash = createHash("sha256");
34
+ hash.update(JSON.stringify({ request, scope: this.scope }));
35
+ return `widget-data:${hash.digest("hex").slice(0, 16)}`;
36
+ }
37
+ getCacheTags(entityType) {
38
+ return ["widget-data", `widget-data:${entityType}`];
39
+ }
40
+ async fetchWidgetData(request) {
41
+ this.validateRequest(request);
42
+ if (this.cache) {
43
+ const cacheKey = this.buildCacheKey(request);
44
+ try {
45
+ const cached = await this.cache.get(cacheKey);
46
+ if (cached && typeof cached === "object" && "value" in cached) {
47
+ return cached;
48
+ }
49
+ } catch {
50
+ }
51
+ }
52
+ const now = /* @__PURE__ */ new Date();
53
+ let dateRangeResolved;
54
+ let comparisonRange;
55
+ if (request.dateRange) {
56
+ dateRangeResolved = resolveDateRange(request.dateRange.preset, now);
57
+ if (request.comparison) {
58
+ comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset);
59
+ }
60
+ }
61
+ const mainResult = await this.executeQuery(request, dateRangeResolved);
62
+ let comparisonResult;
63
+ if (comparisonRange && request.dateRange) {
64
+ comparisonResult = await this.executeQuery(request, comparisonRange);
65
+ }
66
+ const response = {
67
+ value: mainResult.value,
68
+ data: mainResult.data,
69
+ metadata: {
70
+ fetchedAt: now.toISOString(),
71
+ recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0)
72
+ }
73
+ };
74
+ if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {
75
+ response.comparison = {
76
+ value: comparisonResult.value,
77
+ change: calculatePercentageChange(mainResult.value, comparisonResult.value),
78
+ direction: determineChangeDirection(mainResult.value, comparisonResult.value)
79
+ };
80
+ }
81
+ if (this.cache) {
82
+ const cacheKey = this.buildCacheKey(request);
83
+ const tags = this.getCacheTags(request.entityType);
84
+ try {
85
+ await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags });
86
+ } catch {
87
+ }
88
+ }
89
+ return response;
90
+ }
91
+ validateRequest(request) {
92
+ if (!this.registry.isValidEntityType(request.entityType)) {
93
+ throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`);
94
+ }
95
+ if (!request.metric?.field || !request.metric?.aggregate) {
96
+ throw new WidgetDataValidationError("Metric field and aggregate are required");
97
+ }
98
+ const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field);
99
+ if (!metricMapping) {
100
+ throw new WidgetDataValidationError(
101
+ `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`
102
+ );
103
+ }
104
+ const validAggregates = ["count", "sum", "avg", "min", "max"];
105
+ if (!validAggregates.includes(request.metric.aggregate)) {
106
+ throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`);
107
+ }
108
+ if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {
109
+ throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`);
110
+ }
111
+ if (request.groupBy) {
112
+ const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field);
113
+ if (!groupMapping) {
114
+ const [baseField] = request.groupBy.field.split(".");
115
+ const baseMapping = this.registry.getFieldMapping(request.entityType, baseField);
116
+ if (!baseMapping || baseMapping.type !== "jsonb") {
117
+ throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ async executeQuery(request, dateRange) {
123
+ const query = buildAggregationQuery({
124
+ entityType: request.entityType,
125
+ metric: request.metric,
126
+ groupBy: request.groupBy,
127
+ dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : void 0,
128
+ filters: request.filters,
129
+ scope: this.scope,
130
+ registry: this.registry
131
+ });
132
+ if (!query) {
133
+ throw new Error("Failed to build aggregation query");
134
+ }
135
+ const rows = await this.em.getConnection().execute(query.sql, query.params);
136
+ const results = Array.isArray(rows) ? rows : [];
137
+ if (request.groupBy) {
138
+ let data = results.map((row) => ({
139
+ groupKey: row.group_key,
140
+ value: row.value !== null ? Number(row.value) : null
141
+ }));
142
+ if (request.groupBy.resolveLabels) {
143
+ data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field);
144
+ }
145
+ const totalValue = data.reduce((sum, item) => sum + (item.value ?? 0), 0);
146
+ return { value: totalValue, data };
147
+ }
148
+ const singleValue = results[0]?.value !== void 0 ? Number(results[0].value) : null;
149
+ return { value: singleValue, data: [] };
150
+ }
151
+ async resolveGroupLabels(data, entityType, groupByField) {
152
+ const config = this.registry.getLabelResolverConfig(entityType, groupByField);
153
+ if (!config) {
154
+ return data.map((item) => ({
155
+ ...item,
156
+ groupLabel: item.groupKey != null && item.groupKey !== "" ? String(item.groupKey) : void 0
157
+ }));
158
+ }
159
+ const ids = data.map((item) => item.groupKey).filter((id) => {
160
+ if (typeof id !== "string" || id.length === 0) return false;
161
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
162
+ });
163
+ if (ids.length === 0) {
164
+ return data.map((item) => ({ ...item, groupLabel: void 0 }));
165
+ }
166
+ const uniqueIds = [...new Set(ids)];
167
+ assertSafeIdentifier(config.table, "table name");
168
+ assertSafeIdentifier(config.idColumn, "id column");
169
+ assertSafeIdentifier(config.labelColumn, "label column");
170
+ const clauses = [`"${config.idColumn}" = ANY(?::uuid[])`, "tenant_id = ?"];
171
+ const params = [`{${uniqueIds.join(",")}}`, this.scope.tenantId];
172
+ if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {
173
+ clauses.push("organization_id = ANY(?::uuid[])");
174
+ params.push(`{${this.scope.organizationIds.join(",")}}`);
175
+ }
176
+ const sql = `SELECT "${config.idColumn}" as id, "${config.labelColumn}" as label FROM "${config.table}" WHERE ${clauses.join(
177
+ " AND "
178
+ )}`;
179
+ try {
180
+ const labelRows = await this.em.getConnection().execute(sql, params);
181
+ const labelMap = /* @__PURE__ */ new Map();
182
+ for (const row of labelRows) {
183
+ if (row.id && row.label != null && row.label !== "") {
184
+ labelMap.set(row.id, row.label);
185
+ }
186
+ }
187
+ return data.map((item) => ({
188
+ ...item,
189
+ groupLabel: typeof item.groupKey === "string" && labelMap.has(item.groupKey) ? labelMap.get(item.groupKey) : void 0
190
+ }));
191
+ } catch {
192
+ return data.map((item) => ({
193
+ ...item,
194
+ groupLabel: void 0
195
+ }));
196
+ }
197
+ }
198
+ }
199
+ function createWidgetDataService(em, scope, registry, cache) {
200
+ return new WidgetDataService({ em, scope, registry, cache });
201
+ }
202
+ export {
203
+ WidgetDataService,
204
+ WidgetDataValidationError,
205
+ createWidgetDataService
206
+ };
207
+ //# sourceMappingURL=widgetDataService.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/dashboards/services/widgetDataService.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n} from '../lib/aggregations'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nfunction assertSafeIdentifier(value: string, name: string): void {\n if (!SAFE_IDENTIFIER_PATTERN.test(value)) {\n throw new Error(`Invalid ${name}: ${value}`)\n }\n}\n\nexport type WidgetDataRequest = {\n entityType: string\n metric: {\n field: string\n aggregate: AggregateFunction\n }\n groupBy?: {\n field: string\n granularity?: DateGranularity\n limit?: number\n resolveLabels?: boolean\n }\n filters?: Array<{\n field: string\n operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'not_in' | 'is_null' | 'is_not_null'\n value?: unknown\n }>\n dateRange?: {\n field: string\n preset: DateRangePreset\n }\n comparison?: {\n type: 'previous_period' | 'previous_year'\n }\n}\n\nexport type WidgetDataItem = {\n groupKey: unknown\n groupLabel?: string\n value: number | null\n}\n\nexport type WidgetDataResponse = {\n value: number | null\n data: WidgetDataItem[]\n comparison?: {\n value: number | null\n change: number\n direction: 'up' | 'down' | 'unchanged'\n }\n metadata: {\n fetchedAt: string\n recordCount: number\n }\n}\n\nexport type WidgetDataScope = {\n tenantId: string\n organizationIds?: string[]\n}\n\nexport type WidgetDataServiceOptions = {\n em: EntityManager\n scope: WidgetDataScope\n registry: AnalyticsRegistry\n cache?: CacheStrategy\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n\n constructor(options: WidgetDataServiceOptions) {\n this.em = options.em\n this.scope = options.scope\n this.registry = options.registry\n this.cache = options.cache\n }\n\n private buildCacheKey(request: WidgetDataRequest): string {\n const hash = createHash('sha256')\n hash.update(JSON.stringify({ request, scope: this.scope }))\n return `widget-data:${hash.digest('hex').slice(0, 16)}`\n }\n\n private getCacheTags(entityType: string): string[] {\n return ['widget-data', `widget-data:${entityType}`]\n }\n\n async fetchWidgetData(request: WidgetDataRequest): Promise<WidgetDataResponse> {\n this.validateRequest(request)\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n try {\n const cached = await this.cache.get(cacheKey)\n if (cached && typeof cached === 'object' && 'value' in (cached as object)) {\n return cached as WidgetDataResponse\n }\n } catch {\n }\n }\n\n const now = new Date()\n let dateRangeResolved: { start: Date; end: Date } | undefined\n let comparisonRange: { start: Date; end: Date } | undefined\n\n if (request.dateRange) {\n dateRangeResolved = resolveDateRange(request.dateRange.preset, now)\n if (request.comparison) {\n comparisonRange = getPreviousPeriod(dateRangeResolved, request.dateRange.preset)\n }\n }\n\n const mainResult = await this.executeQuery(request, dateRangeResolved)\n\n let comparisonResult: { value: number | null; data: WidgetDataItem[] } | undefined\n if (comparisonRange && request.dateRange) {\n comparisonResult = await this.executeQuery(request, comparisonRange)\n }\n\n const response: WidgetDataResponse = {\n value: mainResult.value,\n data: mainResult.data,\n metadata: {\n fetchedAt: now.toISOString(),\n recordCount: mainResult.data.length || (mainResult.value !== null ? 1 : 0),\n },\n }\n\n if (comparisonResult && mainResult.value !== null && comparisonResult.value !== null) {\n response.comparison = {\n value: comparisonResult.value,\n change: calculatePercentageChange(mainResult.value, comparisonResult.value),\n direction: determineChangeDirection(mainResult.value, comparisonResult.value),\n }\n }\n\n if (this.cache) {\n const cacheKey = this.buildCacheKey(request)\n const tags = this.getCacheTags(request.entityType)\n try {\n await this.cache.set(cacheKey, response, { ttl: WIDGET_DATA_CACHE_TTL, tags })\n } catch {\n }\n }\n\n return response\n }\n\n private validateRequest(request: WidgetDataRequest): void {\n if (!this.registry.isValidEntityType(request.entityType)) {\n throw new WidgetDataValidationError(`Invalid entity type: ${request.entityType}`)\n }\n\n if (!request.metric?.field || !request.metric?.aggregate) {\n throw new WidgetDataValidationError('Metric field and aggregate are required')\n }\n\n const metricMapping = this.registry.getFieldMapping(request.entityType, request.metric.field)\n if (!metricMapping) {\n throw new WidgetDataValidationError(\n `Invalid metric field: ${request.metric.field} for entity type: ${request.entityType}`\n )\n }\n\n const validAggregates: AggregateFunction[] = ['count', 'sum', 'avg', 'min', 'max']\n if (!validAggregates.includes(request.metric.aggregate)) {\n throw new WidgetDataValidationError(`Invalid aggregate function: ${request.metric.aggregate}`)\n }\n\n if (request.dateRange && !isValidDateRangePreset(request.dateRange.preset)) {\n throw new WidgetDataValidationError(`Invalid date range preset: ${request.dateRange.preset}`)\n }\n\n if (request.groupBy) {\n const groupMapping = this.registry.getFieldMapping(request.entityType, request.groupBy.field)\n if (!groupMapping) {\n const [baseField] = request.groupBy.field.split('.')\n const baseMapping = this.registry.getFieldMapping(request.entityType, baseField)\n if (!baseMapping || baseMapping.type !== 'jsonb') {\n throw new WidgetDataValidationError(`Invalid groupBy field: ${request.groupBy.field}`)\n }\n }\n }\n }\n\n private async executeQuery(\n request: WidgetDataRequest,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildAggregationQuery({\n entityType: request.entityType,\n metric: request.metric,\n groupBy: request.groupBy,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n })\n\n if (!query) {\n throw new Error('Failed to build aggregation query')\n }\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const results = Array.isArray(rows) ? rows : []\n\n if (request.groupBy) {\n let data: WidgetDataItem[] = results.map((row: Record<string, unknown>) => ({\n groupKey: row.group_key,\n value: row.value !== null ? Number(row.value) : null,\n }))\n\n if (request.groupBy.resolveLabels) {\n data = await this.resolveGroupLabels(data, request.entityType, request.groupBy.field)\n }\n\n const totalValue = data.reduce((sum: number, item: WidgetDataItem) => sum + (item.value ?? 0), 0)\n return { value: totalValue, data }\n }\n\n const singleValue = results[0]?.value !== undefined ? Number(results[0].value) : null\n return { value: singleValue, data: [] }\n }\n\n private async resolveGroupLabels(\n data: WidgetDataItem[],\n entityType: string,\n groupByField: string,\n ): Promise<WidgetDataItem[]> {\n const config = this.registry.getLabelResolverConfig(entityType, groupByField)\n\n if (!config) {\n return data.map((item) => ({\n ...item,\n groupLabel: item.groupKey != null && item.groupKey !== '' ? String(item.groupKey) : undefined,\n }))\n }\n\n const ids = data\n .map((item) => item.groupKey)\n .filter((id): id is string => {\n if (typeof id !== 'string' || id.length === 0) return false\n return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)\n })\n\n if (ids.length === 0) {\n return data.map((item) => ({ ...item, groupLabel: undefined }))\n }\n\n const uniqueIds = [...new Set(ids)]\n\n assertSafeIdentifier(config.table, 'table name')\n assertSafeIdentifier(config.idColumn, 'id column')\n assertSafeIdentifier(config.labelColumn, 'label column')\n\n const clauses = [`\"${config.idColumn}\" = ANY(?::uuid[])`, 'tenant_id = ?']\n const params: unknown[] = [`{${uniqueIds.join(',')}}`, this.scope.tenantId]\n\n if (this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n clauses.push('organization_id = ANY(?::uuid[])')\n params.push(`{${this.scope.organizationIds.join(',')}}`)\n }\n\n const sql = `SELECT \"${config.idColumn}\" as id, \"${config.labelColumn}\" as label FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null }>) {\n if (row.id && row.label != null && row.label !== '') {\n labelMap.set(row.id, row.label)\n }\n }\n\n return data.map((item) => ({\n ...item,\n groupLabel: typeof item.groupKey === 'string' && labelMap.has(item.groupKey)\n ? labelMap.get(item.groupKey)!\n : undefined,\n }))\n } catch {\n return data.map((item) => ({\n ...item,\n groupLabel: undefined,\n }))\n }\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache })\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,18 @@
1
+ import { isValidDateRangePreset } from "@open-mercato/ui/backend/date-range";
2
+ const DEFAULT_SETTINGS = {
3
+ dateRange: "this_month",
4
+ showComparison: true
5
+ };
6
+ function hydrateSettings(raw) {
7
+ if (!raw || typeof raw !== "object") return { ...DEFAULT_SETTINGS };
8
+ const obj = raw;
9
+ return {
10
+ dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
11
+ showComparison: typeof obj.showComparison === "boolean" ? obj.showComparison : DEFAULT_SETTINGS.showComparison
12
+ };
13
+ }
14
+ export {
15
+ DEFAULT_SETTINGS,
16
+ hydrateSettings
17
+ };
18
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/aov-kpi/config.ts"],
4
+ "sourcesContent": ["import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'\n\nexport type AovKpiSettings = {\n dateRange: DateRangePreset\n showComparison: boolean\n}\n\nexport const DEFAULT_SETTINGS: AovKpiSettings = {\n dateRange: 'this_month',\n showComparison: true,\n}\n\nexport function hydrateSettings(raw: unknown): AovKpiSettings {\n if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }\n const obj = raw as Record<string, unknown>\n return {\n dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,\n showComparison: typeof obj.showComparison === 'boolean' ? obj.showComparison : DEFAULT_SETTINGS.showComparison,\n }\n}\n"],
5
+ "mappings": "AAAA,SAA+B,8BAA8B;AAOtD,MAAM,mBAAmC;AAAA,EAC9C,WAAW;AAAA,EACX,gBAAgB;AAClB;AAEO,SAAS,gBAAgB,KAA8B;AAC5D,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,EAAE,GAAG,iBAAiB;AAClE,QAAM,MAAM;AACZ,SAAO;AAAA,IACL,WAAW,uBAAuB,IAAI,SAAS,IAAI,IAAI,YAAY,iBAAiB;AAAA,IACpF,gBAAgB,OAAO,IAAI,mBAAmB,YAAY,IAAI,iBAAiB,iBAAiB;AAAA,EAClG;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,128 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { KpiCard } from "@open-mercato/ui/backend/charts";
7
+ import {
8
+ DateRangeSelect,
9
+ InlineDateRangeSelect,
10
+ getComparisonLabelKey
11
+ } from "@open-mercato/ui/backend/date-range";
12
+ import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
13
+ import { formatCurrencyWithDecimals } from "../../../lib/formatters.js";
14
+ async function fetchAovData(settings) {
15
+ const body = {
16
+ entityType: "sales:orders",
17
+ metric: {
18
+ field: "grandTotalGrossAmount",
19
+ aggregate: "avg"
20
+ },
21
+ dateRange: {
22
+ field: "placedAt",
23
+ preset: settings.dateRange
24
+ },
25
+ comparison: settings.showComparison ? { type: "previous_period" } : void 0
26
+ };
27
+ const call = await apiCall("/api/dashboards/widgets/data", {
28
+ method: "POST",
29
+ headers: { "Content-Type": "application/json" },
30
+ body: JSON.stringify(body)
31
+ });
32
+ if (!call.ok) {
33
+ const errorMsg = call.result?.error;
34
+ throw new Error(typeof errorMsg === "string" ? errorMsg : "Failed to fetch AOV data");
35
+ }
36
+ return call.result;
37
+ }
38
+ const AovKpiWidget = ({
39
+ mode,
40
+ settings = DEFAULT_SETTINGS,
41
+ onSettingsChange,
42
+ refreshToken,
43
+ onRefreshStateChange
44
+ }) => {
45
+ const t = useT();
46
+ const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
47
+ const [value, setValue] = React.useState(null);
48
+ const [trend, setTrend] = React.useState(void 0);
49
+ const [loading, setLoading] = React.useState(true);
50
+ const [error, setError] = React.useState(null);
51
+ const refresh = React.useCallback(async () => {
52
+ onRefreshStateChange?.(true);
53
+ setLoading(true);
54
+ setError(null);
55
+ try {
56
+ const data = await fetchAovData(hydrated);
57
+ setValue(data.value);
58
+ if (data.comparison) {
59
+ setTrend({
60
+ value: data.comparison.change,
61
+ direction: data.comparison.direction
62
+ });
63
+ } else {
64
+ setTrend(void 0);
65
+ }
66
+ } catch (err) {
67
+ console.error("Failed to load AOV KPI data", err);
68
+ setError(t("dashboards.analytics.widgets.aovKpi.error", "Failed to load data"));
69
+ } finally {
70
+ setLoading(false);
71
+ onRefreshStateChange?.(false);
72
+ }
73
+ }, [hydrated, onRefreshStateChange, t]);
74
+ React.useEffect(() => {
75
+ refresh().catch(() => {
76
+ });
77
+ }, [refresh, refreshToken]);
78
+ if (mode === "settings") {
79
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-sm", children: [
80
+ /* @__PURE__ */ jsx(
81
+ DateRangeSelect,
82
+ {
83
+ id: "aov-kpi-date-range",
84
+ label: t("dashboards.analytics.settings.dateRange", "Date Range"),
85
+ value: hydrated.dateRange,
86
+ onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
90
+ /* @__PURE__ */ jsx(
91
+ "input",
92
+ {
93
+ type: "checkbox",
94
+ checked: hydrated.showComparison,
95
+ onChange: (e) => onSettingsChange({ ...hydrated, showComparison: e.target.checked }),
96
+ className: "h-4 w-4 rounded border focus:ring-primary"
97
+ }
98
+ ),
99
+ t("dashboards.analytics.settings.showComparison", "Show comparison")
100
+ ] }) })
101
+ ] });
102
+ }
103
+ const comparisonLabelInfo = getComparisonLabelKey(hydrated.dateRange);
104
+ const comparisonLabel = hydrated.showComparison ? t(comparisonLabelInfo.key, comparisonLabelInfo.fallback) : void 0;
105
+ return /* @__PURE__ */ jsx(
106
+ KpiCard,
107
+ {
108
+ value,
109
+ trend,
110
+ comparisonLabel,
111
+ loading,
112
+ error,
113
+ formatValue: formatCurrencyWithDecimals,
114
+ headerAction: /* @__PURE__ */ jsx(
115
+ InlineDateRangeSelect,
116
+ {
117
+ value: hydrated.dateRange,
118
+ onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
119
+ }
120
+ )
121
+ }
122
+ );
123
+ };
124
+ var widget_client_default = AovKpiWidget;
125
+ export {
126
+ widget_client_default as default
127
+ };
128
+ //# sourceMappingURL=widget.client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/aov-kpi/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 { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n getComparisonLabelKey,\n} from '@open-mercato/ui/backend/date-range'\nimport { DEFAULT_SETTINGS, hydrateSettings, type AovKpiSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyWithDecimals } from '../../../lib/formatters'\n\nasync function fetchAovData(settings: AovKpiSettings): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:orders',\n metric: {\n field: 'grandTotalGrossAmount',\n aggregate: 'avg',\n },\n dateRange: {\n field: 'placedAt',\n preset: settings.dateRange,\n },\n comparison: settings.showComparison ? { type: 'previous_period' } : undefined,\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 AOV data')\n }\n\n return call.result as WidgetDataResponse\n}\n\nconst AovKpiWidget: React.FC<DashboardWidgetComponentProps<AovKpiSettings>> = ({\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 [value, setValue] = React.useState<number | null>(null)\n const [trend, setTrend] = React.useState<KpiTrend | undefined>(undefined)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await fetchAovData(hydrated)\n setValue(data.value)\n if (data.comparison) {\n setTrend({\n value: data.comparison.change,\n direction: data.comparison.direction,\n })\n } else {\n setTrend(undefined)\n }\n } catch (err) {\n console.error('Failed to load AOV KPI data', err)\n setError(t('dashboards.analytics.widgets.aovKpi.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"aov-kpi-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 className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={hydrated.showComparison}\n onChange={(e) => onSettingsChange({ ...hydrated, showComparison: e.target.checked })}\n className=\"h-4 w-4 rounded border focus:ring-primary\"\n />\n {t('dashboards.analytics.settings.showComparison', 'Show comparison')}\n </label>\n </div>\n </div>\n )\n }\n\n const comparisonLabelInfo = getComparisonLabelKey(hydrated.dateRange)\n const comparisonLabel = hydrated.showComparison\n ? t(comparisonLabelInfo.key, comparisonLabelInfo.fallback)\n : undefined\n\n return (\n <KpiCard\n value={value}\n trend={trend}\n comparisonLabel={comparisonLabel}\n loading={loading}\n error={error}\n formatValue={formatCurrencyWithDecimals}\n headerAction={\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n }\n />\n )\n}\n\nexport default AovKpiWidget\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,25 @@
1
+ import AovKpiWidget from "./widget.client.js";
2
+ import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
3
+ const widget = {
4
+ metadata: {
5
+ id: "dashboards.analytics.aovKpi",
6
+ title: "Average Order Value",
7
+ description: "Average order value with period comparison",
8
+ features: ["analytics.view", "sales.orders.view"],
9
+ defaultSize: "sm",
10
+ defaultEnabled: false,
11
+ defaultSettings: DEFAULT_SETTINGS,
12
+ tags: ["analytics", "sales", "kpi"],
13
+ category: "analytics",
14
+ icon: "trending-up",
15
+ supportsRefresh: true
16
+ },
17
+ Widget: AovKpiWidget,
18
+ hydrateSettings,
19
+ dehydrateSettings: (s) => ({ dateRange: s.dateRange, showComparison: s.showComparison })
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/aov-kpi/widget.ts"],
4
+ "sourcesContent": ["import type { DashboardWidgetModule } from '@open-mercato/shared/modules/dashboard/widgets'\nimport AovKpiWidget from './widget.client'\nimport { DEFAULT_SETTINGS, hydrateSettings, type AovKpiSettings } from './config'\n\nconst widget: DashboardWidgetModule<AovKpiSettings> = {\n metadata: {\n id: 'dashboards.analytics.aovKpi',\n title: 'Average Order Value',\n description: 'Average order value with period comparison',\n features: ['analytics.view', 'sales.orders.view'],\n defaultSize: 'sm',\n defaultEnabled: false,\n defaultSettings: DEFAULT_SETTINGS,\n tags: ['analytics', 'sales', 'kpi'],\n category: 'analytics',\n icon: 'trending-up',\n supportsRefresh: true,\n },\n Widget: AovKpiWidget,\n hydrateSettings,\n dehydrateSettings: (s) => ({ dateRange: s.dateRange, showComparison: s.showComparison }),\n}\n\nexport default widget\n"],
5
+ "mappings": "AACA,OAAO,kBAAkB;AACzB,SAAS,kBAAkB,uBAA4C;AAEvE,MAAM,SAAgD;AAAA,EACpD,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,KAAK;AAAA,IAClC,UAAU;AAAA,IACV,MAAM;AAAA,IACN,iBAAiB;AAAA,EACnB;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,mBAAmB,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,gBAAgB,EAAE,eAAe;AACxF;AAEA,IAAO,iBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,18 @@
1
+ import { isValidDateRangePreset } from "@open-mercato/ui/backend/date-range";
2
+ const DEFAULT_SETTINGS = {
3
+ dateRange: "this_month",
4
+ showComparison: true
5
+ };
6
+ function hydrateSettings(raw) {
7
+ if (!raw || typeof raw !== "object") return { ...DEFAULT_SETTINGS };
8
+ const obj = raw;
9
+ return {
10
+ dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,
11
+ showComparison: typeof obj.showComparison === "boolean" ? obj.showComparison : DEFAULT_SETTINGS.showComparison
12
+ };
13
+ }
14
+ export {
15
+ DEFAULT_SETTINGS,
16
+ hydrateSettings
17
+ };
18
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/new-customers-kpi/config.ts"],
4
+ "sourcesContent": ["import { type DateRangePreset, isValidDateRangePreset } from '@open-mercato/ui/backend/date-range'\n\nexport type NewCustomersKpiSettings = {\n dateRange: DateRangePreset\n showComparison: boolean\n}\n\nexport const DEFAULT_SETTINGS: NewCustomersKpiSettings = {\n dateRange: 'this_month',\n showComparison: true,\n}\n\nexport function hydrateSettings(raw: unknown): NewCustomersKpiSettings {\n if (!raw || typeof raw !== 'object') return { ...DEFAULT_SETTINGS }\n const obj = raw as Record<string, unknown>\n return {\n dateRange: isValidDateRangePreset(obj.dateRange) ? obj.dateRange : DEFAULT_SETTINGS.dateRange,\n showComparison: typeof obj.showComparison === 'boolean' ? obj.showComparison : DEFAULT_SETTINGS.showComparison,\n }\n}\n"],
5
+ "mappings": "AAAA,SAA+B,8BAA8B;AAOtD,MAAM,mBAA4C;AAAA,EACvD,WAAW;AAAA,EACX,gBAAgB;AAClB;AAEO,SAAS,gBAAgB,KAAuC;AACrE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,EAAE,GAAG,iBAAiB;AAClE,QAAM,MAAM;AACZ,SAAO;AAAA,IACL,WAAW,uBAAuB,IAAI,SAAS,IAAI,IAAI,YAAY,iBAAiB;AAAA,IACpF,gBAAgB,OAAO,IAAI,mBAAmB,YAAY,IAAI,iBAAiB,iBAAiB;AAAA,EAClG;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,126 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
5
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
6
+ import { KpiCard } from "@open-mercato/ui/backend/charts";
7
+ import {
8
+ DateRangeSelect,
9
+ InlineDateRangeSelect,
10
+ getComparisonLabelKey
11
+ } from "@open-mercato/ui/backend/date-range";
12
+ import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
13
+ async function fetchNewCustomersData(settings) {
14
+ const body = {
15
+ entityType: "customers:entities",
16
+ metric: {
17
+ field: "id",
18
+ aggregate: "count"
19
+ },
20
+ dateRange: {
21
+ field: "createdAt",
22
+ preset: settings.dateRange
23
+ },
24
+ comparison: settings.showComparison ? { type: "previous_period" } : void 0
25
+ };
26
+ const call = await apiCall("/api/dashboards/widgets/data", {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/json" },
29
+ body: JSON.stringify(body)
30
+ });
31
+ if (!call.ok) {
32
+ const errorMsg = call.result?.error;
33
+ throw new Error(typeof errorMsg === "string" ? errorMsg : "Failed to fetch new customers data");
34
+ }
35
+ return call.result;
36
+ }
37
+ const NewCustomersKpiWidget = ({
38
+ mode,
39
+ settings = DEFAULT_SETTINGS,
40
+ onSettingsChange,
41
+ refreshToken,
42
+ onRefreshStateChange
43
+ }) => {
44
+ const t = useT();
45
+ const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
46
+ const [value, setValue] = React.useState(null);
47
+ const [trend, setTrend] = React.useState(void 0);
48
+ const [loading, setLoading] = React.useState(true);
49
+ const [error, setError] = React.useState(null);
50
+ const refresh = React.useCallback(async () => {
51
+ onRefreshStateChange?.(true);
52
+ setLoading(true);
53
+ setError(null);
54
+ try {
55
+ const data = await fetchNewCustomersData(hydrated);
56
+ setValue(data.value);
57
+ if (data.comparison) {
58
+ setTrend({
59
+ value: data.comparison.change,
60
+ direction: data.comparison.direction
61
+ });
62
+ } else {
63
+ setTrend(void 0);
64
+ }
65
+ } catch (err) {
66
+ console.error("Failed to load new customers KPI data", err);
67
+ setError(t("dashboards.analytics.widgets.newCustomersKpi.error", "Failed to load data"));
68
+ } finally {
69
+ setLoading(false);
70
+ onRefreshStateChange?.(false);
71
+ }
72
+ }, [hydrated, onRefreshStateChange, t]);
73
+ React.useEffect(() => {
74
+ refresh().catch(() => {
75
+ });
76
+ }, [refresh, refreshToken]);
77
+ if (mode === "settings") {
78
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4 text-sm", children: [
79
+ /* @__PURE__ */ jsx(
80
+ DateRangeSelect,
81
+ {
82
+ id: "new-customers-kpi-date-range",
83
+ label: t("dashboards.analytics.settings.dateRange", "Date Range"),
84
+ value: hydrated.dateRange,
85
+ onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
86
+ }
87
+ ),
88
+ /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 text-sm", children: [
89
+ /* @__PURE__ */ jsx(
90
+ "input",
91
+ {
92
+ type: "checkbox",
93
+ checked: hydrated.showComparison,
94
+ onChange: (e) => onSettingsChange({ ...hydrated, showComparison: e.target.checked }),
95
+ className: "h-4 w-4 rounded border focus:ring-primary"
96
+ }
97
+ ),
98
+ t("dashboards.analytics.settings.showComparison", "Show comparison")
99
+ ] }) })
100
+ ] });
101
+ }
102
+ const comparisonLabelInfo = getComparisonLabelKey(hydrated.dateRange);
103
+ const comparisonLabel = hydrated.showComparison ? t(comparisonLabelInfo.key, comparisonLabelInfo.fallback) : void 0;
104
+ return /* @__PURE__ */ jsx(
105
+ KpiCard,
106
+ {
107
+ value,
108
+ trend,
109
+ comparisonLabel,
110
+ loading,
111
+ error,
112
+ headerAction: /* @__PURE__ */ jsx(
113
+ InlineDateRangeSelect,
114
+ {
115
+ value: hydrated.dateRange,
116
+ onChange: (dateRange) => onSettingsChange({ ...hydrated, dateRange })
117
+ }
118
+ )
119
+ }
120
+ );
121
+ };
122
+ var widget_client_default = NewCustomersKpiWidget;
123
+ export {
124
+ widget_client_default as default
125
+ };
126
+ //# sourceMappingURL=widget.client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/new-customers-kpi/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 { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n getComparisonLabelKey,\n} from '@open-mercato/ui/backend/date-range'\nimport { DEFAULT_SETTINGS, hydrateSettings, type NewCustomersKpiSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\n\nasync function fetchNewCustomersData(settings: NewCustomersKpiSettings): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'customers:entities',\n metric: {\n field: 'id',\n aggregate: 'count',\n },\n dateRange: {\n field: 'createdAt',\n preset: settings.dateRange,\n },\n comparison: settings.showComparison ? { type: 'previous_period' } : undefined,\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 new customers data')\n }\n\n return call.result as WidgetDataResponse\n}\n\nconst NewCustomersKpiWidget: React.FC<DashboardWidgetComponentProps<NewCustomersKpiSettings>> = ({\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 [value, setValue] = React.useState<number | null>(null)\n const [trend, setTrend] = React.useState<KpiTrend | undefined>(undefined)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await fetchNewCustomersData(hydrated)\n setValue(data.value)\n if (data.comparison) {\n setTrend({\n value: data.comparison.change,\n direction: data.comparison.direction,\n })\n } else {\n setTrend(undefined)\n }\n } catch (err) {\n console.error('Failed to load new customers KPI data', err)\n setError(t('dashboards.analytics.widgets.newCustomersKpi.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"new-customers-kpi-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 className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={hydrated.showComparison}\n onChange={(e) => onSettingsChange({ ...hydrated, showComparison: e.target.checked })}\n className=\"h-4 w-4 rounded border focus:ring-primary\"\n />\n {t('dashboards.analytics.settings.showComparison', 'Show comparison')}\n </label>\n </div>\n </div>\n )\n }\n\n const comparisonLabelInfo = getComparisonLabelKey(hydrated.dateRange)\n const comparisonLabel = hydrated.showComparison\n ? t(comparisonLabelInfo.key, comparisonLabelInfo.fallback)\n : undefined\n\n return (\n <KpiCard\n value={value}\n trend={trend}\n comparisonLabel={comparisonLabel}\n loading={loading}\n error={error}\n headerAction={\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n }\n />\n )\n}\n\nexport default NewCustomersKpiWidget\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }