@open-mercato/core 0.6.7-develop.6751.1.ac823a3d26 → 0.6.7-develop.6758.1.697eade236

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/api/route.js +16 -2
  3. package/dist/modules/attachments/api/route.js.map +2 -2
  4. package/dist/modules/attachments/lib/upload-limits.js +33 -0
  5. package/dist/modules/attachments/lib/upload-limits.js.map +2 -2
  6. package/dist/modules/communication_channels/api/post/test-seed/route.js +52 -2
  7. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +2 -2
  8. package/dist/modules/currencies/di.js +4 -0
  9. package/dist/modules/currencies/di.js.map +2 -2
  10. package/dist/modules/currencies/services/baseCurrencyService.js +42 -0
  11. package/dist/modules/currencies/services/baseCurrencyService.js.map +7 -0
  12. package/dist/modules/customers/analytics.js +3 -1
  13. package/dist/modules/customers/analytics.js.map +2 -2
  14. package/dist/modules/customers/api/deals/aggregate/route.js +5 -4
  15. package/dist/modules/customers/api/deals/aggregate/route.js.map +2 -2
  16. package/dist/modules/customers/api/deals/summary/route.js +5 -4
  17. package/dist/modules/customers/api/deals/summary/route.js.map +2 -2
  18. package/dist/modules/customers/lib/optionalBaseCurrency.js +19 -0
  19. package/dist/modules/customers/lib/optionalBaseCurrency.js.map +7 -0
  20. package/dist/modules/dashboards/api/widgets/data/batch/route.js +8 -1
  21. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
  22. package/dist/modules/dashboards/api/widgets/data/route.js +8 -1
  23. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  24. package/dist/modules/dashboards/api/widgets/data/schema.js +2 -1
  25. package/dist/modules/dashboards/api/widgets/data/schema.js.map +2 -2
  26. package/dist/modules/dashboards/components/UnlabelledAmountNotice.js +33 -0
  27. package/dist/modules/dashboards/components/UnlabelledAmountNotice.js.map +7 -0
  28. package/dist/modules/dashboards/lib/aggregations.js +17 -0
  29. package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
  30. package/dist/modules/dashboards/lib/formatters.js +84 -11
  31. package/dist/modules/dashboards/lib/formatters.js.map +2 -2
  32. package/dist/modules/dashboards/lib/optionalBaseCurrency.js +11 -0
  33. package/dist/modules/dashboards/lib/optionalBaseCurrency.js.map +7 -0
  34. package/dist/modules/dashboards/services/widgetDataService.js +86 -6
  35. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  36. package/dist/modules/dashboards/subscribers/invalidateWidgetDataCache.js +26 -0
  37. package/dist/modules/dashboards/subscribers/invalidateWidgetDataCache.js.map +7 -0
  38. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +9 -3
  39. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
  40. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +10 -4
  41. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
  42. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +9 -3
  43. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
  44. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +8 -3
  45. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
  46. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +26 -18
  47. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
  48. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +22 -14
  49. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
  50. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +10 -4
  51. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
  52. package/dist/modules/notifications/api/route.js +62 -3
  53. package/dist/modules/notifications/api/route.js.map +2 -2
  54. package/dist/modules/sales/analytics.js +7 -3
  55. package/dist/modules/sales/analytics.js.map +2 -2
  56. package/dist/modules/sales/api/document-history/route.js +2 -1
  57. package/dist/modules/sales/api/document-history/route.js.map +2 -2
  58. package/dist/modules/sales/lib/historyHelpers.js +69 -1
  59. package/dist/modules/sales/lib/historyHelpers.js.map +2 -2
  60. package/dist/modules/sales/widgets/injection/document-history/widget.client.js +51 -2
  61. package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
  62. package/package.json +7 -7
  63. package/src/modules/attachments/api/route.ts +16 -2
  64. package/src/modules/attachments/lib/upload-limits.ts +35 -0
  65. package/src/modules/communication_channels/api/post/test-seed/route.ts +69 -2
  66. package/src/modules/currencies/di.ts +4 -1
  67. package/src/modules/currencies/services/baseCurrencyService.ts +59 -0
  68. package/src/modules/customers/analytics.ts +2 -0
  69. package/src/modules/customers/api/deals/aggregate/route.ts +5 -4
  70. package/src/modules/customers/api/deals/summary/route.ts +5 -4
  71. package/src/modules/customers/lib/optionalBaseCurrency.ts +34 -0
  72. package/src/modules/dashboards/api/widgets/data/batch/route.ts +8 -1
  73. package/src/modules/dashboards/api/widgets/data/route.ts +8 -1
  74. package/src/modules/dashboards/api/widgets/data/schema.ts +1 -0
  75. package/src/modules/dashboards/components/UnlabelledAmountNotice.tsx +43 -0
  76. package/src/modules/dashboards/i18n/de.json +2 -0
  77. package/src/modules/dashboards/i18n/en.json +2 -0
  78. package/src/modules/dashboards/i18n/es.json +2 -0
  79. package/src/modules/dashboards/i18n/pl.json +2 -0
  80. package/src/modules/dashboards/lib/aggregations.ts +35 -0
  81. package/src/modules/dashboards/lib/formatters.ts +137 -12
  82. package/src/modules/dashboards/lib/optionalBaseCurrency.ts +22 -0
  83. package/src/modules/dashboards/services/widgetDataService.ts +110 -4
  84. package/src/modules/dashboards/subscribers/invalidateWidgetDataCache.ts +32 -0
  85. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +9 -3
  86. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +9 -3
  87. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +9 -3
  88. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +7 -2
  89. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +25 -15
  90. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +21 -11
  91. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +9 -3
  92. package/src/modules/notifications/api/route.ts +88 -3
  93. package/src/modules/sales/analytics.ts +4 -0
  94. package/src/modules/sales/api/document-history/route.ts +1 -0
  95. package/src/modules/sales/i18n/de.json +19 -0
  96. package/src/modules/sales/i18n/en.json +19 -0
  97. package/src/modules/sales/i18n/es.json +19 -0
  98. package/src/modules/sales/i18n/pl.json +19 -0
  99. package/src/modules/sales/lib/historyHelpers.ts +88 -1
  100. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +64 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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 { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { TenantDek } from '@open-mercato/shared/lib/encryption/kms'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n buildGroupSourceRowsQuery,\n resolveGroupExpression,\n} from '../lib/aggregations'\nimport {\n type ExactDecimal,\n EXACT_DECIMAL_ZERO,\n addExactDecimal,\n compareExactDecimal,\n exactDecimalToNumber,\n parseExactDecimal,\n} from '../lib/exactDecimal'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\nconst WIDGET_DATA_SEGMENT_TTL = 86_400_000\nconst WIDGET_DATA_SEGMENT_KEY = 'widget-data:__segment__'\n\n/**\n * Encrypted group sources cannot be grouped by the database, so the rows are scanned and\n * aggregated in application code. The cap keeps that scan bounded; overflowing it fails loudly\n * instead of charting a silently truncated result (#4622).\n */\nconst ENCRYPTED_GROUP_SCAN_LIMIT = 20_000\n\nconst SAFE_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nconst logger = createLogger('dashboards').child({ component: 'widget-data' })\n\nexport class WidgetDataValidationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataValidationError'\n }\n}\n\nexport class WidgetDataScanLimitError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataScanLimitError'\n }\n}\n\n/**\n * Raised when encryption is configured but the group source cannot currently be resolved \u2014 an\n * unhealthy KMS, an unreadable encryption map, or a missing tenant DEK. Grouping would either read\n * ciphertext in SQL or silently collapse every encrypted row into \"Unknown\", so the request fails\n * closed instead (#4622).\n */\nexport class WidgetDataEncryptionUnavailableError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataEncryptionUnavailableError'\n }\n}\n\ntype EncryptedGroupSource = {\n entityId: string\n dbColumn: string\n jsonPath: string | null\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\n/**\n * Encryption maps may register a field in either casing (`findKey` in the encryption service\n * accepts both), so match the analytics column tolerantly rather than by exact snake_case.\n */\nfunction matchesColumn(fieldName: string, dbColumn: string): boolean {\n const normalize = (value: string) => value.replace(/_/g, '').toLowerCase()\n return normalize(fieldName) === normalize(dbColumn)\n}\n\nfunction readJsonPath(value: unknown, path: string): unknown {\n let current = value\n for (const part of path.split('.')) {\n if (current === null || typeof current !== 'object') return null\n current = (current as Record<string, unknown>)[part]\n }\n return current\n}\n\n/**\n * Running aggregate state for one group. Values are folded in as they are scanned \u2014 the bucket\n * never holds the row set \u2014 so the scan cost stays linear and independent of the row cap.\n */\ntype GroupBucket = {\n /** Rows with a non-null metric column, mirroring SQL `COUNT(column)`. */\n count: number\n /** Rows whose metric parsed as a decimal, mirroring the row set SQL aggregates operate on. */\n numericCount: number\n /** Exact running sum, so money never accumulates in binary floating point. */\n sum: ExactDecimal\n min: ExactDecimal | null\n max: ExactDecimal | null\n}\n\nfunction createGroupBucket(): GroupBucket {\n return { count: 0, numericCount: 0, sum: EXACT_DECIMAL_ZERO, min: null, max: null }\n}\n\nfunction foldMetricValue(bucket: GroupBucket, value: ExactDecimal): void {\n bucket.numericCount += 1\n bucket.sum = addExactDecimal(bucket.sum, value)\n if (bucket.min === null || compareExactDecimal(value, bucket.min) < 0) bucket.min = value\n if (bucket.max === null || compareExactDecimal(value, bucket.max) > 0) bucket.max = value\n}\n\n/**\n * Mirrors the SQL semantics of `buildAggregateExpression` for application-side aggregation. That\n * expression wraps `SUM`/`AVG` in `COALESCE(..., 0)`, so an empty value set aggregates to `0` on\n * both paths, while the uncoalesced `MIN`/`MAX` keep PostgreSQL's `NULL`. An encrypted group source\n * must not report a different value than the same data would report in plaintext (#4622).\n */\nfunction aggregateBucket(aggregate: AggregateFunction, bucket: GroupBucket): number | null {\n switch (aggregate) {\n case 'count':\n return bucket.count\n case 'sum':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum)\n case 'avg':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum) / bucket.numericCount\n case 'min':\n return bucket.min === null ? null : exactDecimalToNumber(bucket.min)\n case 'max':\n return bucket.max === null ? null : exactDecimalToNumber(bucket.max)\n default:\n return bucket.count\n }\n}\n\n/**\n * Mirrors the SQL path's `ORDER BY value DESC NULLS LAST`. PostgreSQL defaults DESC to NULLS FIRST,\n * which would let empty buckets displace real ones under a group limit, so both paths state the\n * policy explicitly: highest value first, buckets without a value last.\n */\nfunction compareWidgetDataItemsByValueDesc(\n left: { value: number | null },\n right: { value: number | null },\n): number {\n if (left.value === null && right.value === null) return 0\n if (left.value === null) return 1\n if (right.value === null) return -1\n return right.value - left.value\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 shouldFetchComparison = Boolean(comparisonRange && request.dateRange)\n\n const [mainResult, comparisonResult] = await Promise.all([\n this.executeQuery(request, dateRangeResolved),\n shouldFetchComparison && comparisonRange\n ? this.executeQuery(request, comparisonRange)\n : Promise.resolve<{ value: number | null; data: WidgetDataItem[] } | undefined>(undefined),\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 await this.cache.set(\n WIDGET_DATA_SEGMENT_KEY,\n { updatedAt: response.metadata.fetchedAt },\n { ttl: WIDGET_DATA_SEGMENT_TTL, tags: ['widget-data'] },\n )\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 if (request.groupBy) {\n const encryptedSource = await this.resolveEncryptedGroupSource(request.entityType, request.groupBy)\n if (encryptedSource) {\n return this.executeEncryptedGroupQuery(request, encryptedSource, dateRange)\n }\n }\n\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 /**\n * Reports the encrypted column a groupBy field reads from, or null when the source is stored in\n * plaintext and can be grouped by the database. Grouping over an encrypted column in SQL buckets\n * ciphertext and renders it to the user (#4622).\n */\n private async resolveEncryptedGroupSource(\n entityType: string,\n groupBy: NonNullable<WidgetDataRequest['groupBy']>,\n ): Promise<EncryptedGroupSource | null> {\n // Encryption is not configured for this deployment, so no column can hold ciphertext and the\n // database may group the source directly.\n if (!isTenantDataEncryptionEnabled()) return null\n\n const resolved = resolveGroupExpression(this.registry, entityType, groupBy)\n if (!resolved) return null\n\n const tableName = this.registry.getEntityTypeConfig(entityType)?.tableName\n if (!tableName) return null\n\n // Without an encryption entity id the table is outside the encryption map entirely, which is\n // the same state the encrypting subscriber sees when it skips the row.\n const entityId = this.resolveEncryptionEntityId(tableName)\n if (!entityId) return null\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n if (!encryptionService) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption service unavailable`,\n )\n }\n\n const organizationId = this.resolveOrganizationId()\n let encryptedFields: string[]\n try {\n // Deliberately independent of KMS health: the map describes how the rows were written, and\n // an unhealthy KMS must not be read as \"this column is plaintext\" (#4622).\n encryptedFields = await encryptionService.getEncryptedFieldNames(\n entityId,\n this.scope.tenantId,\n organizationId,\n { ignoreRuntimeHealth: true },\n )\n } catch (err) {\n logger.error('Failed to resolve encrypted fields for widget grouping', { err, entityId, entityType })\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption map lookup failed`,\n )\n }\n\n if (!encryptedFields.some((field) => matchesColumn(field, resolved.dbColumn))) return null\n\n if (resolved.jsonPath === null && groupBy.granularity) {\n throw new WidgetDataValidationError(\n `Cannot group encrypted field by granularity: ${groupBy.field}`,\n )\n }\n\n return { entityId, dbColumn: resolved.dbColumn, jsonPath: resolved.jsonPath }\n }\n\n /**\n * Aggregates an encrypted group source in application code: rows are scanned, the group column is\n * decrypted, and only the resulting plaintext keys are grouped. Values that cannot be decrypted\n * collapse into the null (\"Unknown\") bucket so ciphertext never reaches the response.\n */\n private async executeEncryptedGroupQuery(\n request: WidgetDataRequest,\n source: EncryptedGroupSource,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildGroupSourceRowsQuery({\n entityType: request.entityType,\n metric: request.metric,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n groupColumn: source.dbColumn,\n rowLimit: ENCRYPTED_GROUP_SCAN_LIMIT,\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 (results.length > ENCRYPTED_GROUP_SCAN_LIMIT) {\n throw new WidgetDataScanLimitError(\n `Too many rows to group encrypted field ${request.groupBy?.field} (limit ${ENCRYPTED_GROUP_SCAN_LIMIT})`,\n )\n }\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n let dek: TenantDek | null = null\n try {\n dek = encryptionService ? await encryptionService.getDek(this.scope.tenantId) : null\n } catch (err) {\n logger.error('Failed to resolve the tenant DEK for widget grouping', { err, entityId: source.entityId })\n dek = null\n }\n\n const buckets = new Map<string | null, GroupBucket>()\n let undecryptableRows = 0\n\n for (const row of results as Array<Record<string, unknown>>) {\n const resolvedKey = this.resolveDecryptedGroupKey(row.group_source, source, dek?.key ?? null)\n // An undecryptable source joins the null (\"Unknown\") bucket rather than being dropped, so the\n // widget total still matches the underlying rows.\n if (resolvedKey === undefined) undecryptableRows += 1\n const groupKey = resolvedKey === undefined ? null : resolvedKey\n\n let bucket = buckets.get(groupKey)\n if (!bucket) {\n bucket = createGroupBucket()\n buckets.set(groupKey, bucket)\n }\n\n const metricValue = row.metric_value\n if (metricValue === null || metricValue === undefined) continue\n bucket.count += 1\n const numeric = parseExactDecimal(metricValue)\n if (numeric) foldMetricValue(bucket, numeric)\n }\n\n if (undecryptableRows > 0) {\n // No DEK at all means every ciphertext row would collapse into \"Unknown\" and the widget would\n // report a confidently wrong distribution, so fail closed rather than chart it (#4622).\n if (!dek) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot group encrypted field ${request.groupBy?.field}: tenant encryption key is unavailable`,\n )\n }\n logger.warn('Grouped rows with an undecryptable group source as unknown', {\n entityId: source.entityId,\n column: source.dbColumn,\n rows: undecryptableRows,\n })\n }\n\n let data: WidgetDataItem[] = Array.from(buckets.entries())\n .map(([groupKey, bucket]) => ({ groupKey, value: aggregateBucket(request.metric.aggregate, bucket) }))\n .sort(compareWidgetDataItemsByValueDesc)\n\n if (request.groupBy?.limit && request.groupBy.limit > 0) {\n data = data.slice(0, Math.min(request.groupBy.limit, 100))\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 /**\n * Returns the plaintext group key for a scanned row, `null` for an empty/absent value, or\n * `undefined` when the source is ciphertext that could not be decrypted.\n */\n private resolveDecryptedGroupKey(\n rawValue: unknown,\n source: EncryptedGroupSource,\n dek: string | null,\n ): string | null | undefined {\n if (rawValue === null || rawValue === undefined) return null\n\n let value: unknown = rawValue\n if (typeof value === 'string' && this.isEncryptedPayload(value)) {\n if (!dek) return undefined\n const decrypted = this.decryptWithDek(value, dek)\n if (decrypted === null) return undefined\n value = parseDecryptedFieldValue(decrypted)\n }\n\n if (source.jsonPath !== null) {\n value = readJsonPath(value, source.jsonPath)\n }\n\n if (value === null || value === undefined || value === '') return null\n if (typeof value === 'object') return null\n const key = String(value)\n // Defense in depth: a nested value that is itself ciphertext must never reach the response.\n return this.isEncryptedPayload(key) ? undefined : key\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 if (item.groupKey == null || item.groupKey === '') return { ...item, groupLabel: undefined }\n const label = String(item.groupKey)\n // A ciphertext group key has no meaningful label; echoing it would render encrypted data\n // into the chart legend (#4622).\n return { ...item, groupLabel: this.isEncryptedPayload(label) ? undefined : label }\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 meta = this.resolveEntityMetadata(config.table)\n const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null\n const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null\n const tenantProp = meta\n ? (this.resolveEntityPropertyName(meta, 'tenant_id') ?? this.resolveEntityPropertyName(meta, 'tenantId'))\n : null\n const organizationProp = meta\n ? (this.resolveEntityPropertyName(meta, 'organization_id') ?? this.resolveEntityPropertyName(meta, 'organizationId'))\n : null\n const entityName = meta ? ((meta as any).class ?? meta.className ?? meta.name) : null\n\n if (meta && idProp && labelProp && tenantProp && entityName) {\n const where: Record<string, unknown> = {\n [idProp]: { $in: uniqueIds },\n [tenantProp]: this.scope.tenantId,\n }\n if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n where[organizationProp] = { $in: this.scope.organizationIds }\n }\n\n try {\n const records = await findWithDecryption(\n this.em,\n entityName,\n where,\n { fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },\n { tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() },\n )\n\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n let hasEncryptedLabels = false\n let hasDecryptedLabels = false\n\n const labelMap = new Map<string, string>()\n for (const record of records as Array<Record<string, unknown>>) {\n const id = record[idProp]\n let labelValue = record[labelProp]\n if (typeof labelValue === 'string' && this.isEncryptedPayload(labelValue)) {\n hasEncryptedLabels = true\n if (dek?.key) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n hasDecryptedLabels = true\n }\n }\n } else if (labelValue != null && labelValue !== '') {\n hasDecryptedLabels = true\n }\n\n if (typeof id === 'string' && labelValue != null && labelValue !== '') {\n labelMap.set(id, String(labelValue))\n }\n }\n\n if (labelMap.size > 0 && (!hasEncryptedLabels || hasDecryptedLabels)) {\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 }\n } catch {\n // fall through to SQL resolution\n }\n }\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, tenant_id, organization_id FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n const entityId = this.resolveEntityId(meta)\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const organizationId = this.resolveOrganizationId()\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null; tenant_id?: string | null; organization_id?: string | null }>) {\n let labelValue = row.label\n if (entityId && encryptionService?.isEnabled() && labelValue != null) {\n const rowOrgId = row.organization_id ?? organizationId ?? null\n const decrypted = await encryptionService.decryptEntityPayload(\n entityId,\n { [config.labelColumn]: labelValue },\n this.scope.tenantId,\n rowOrgId,\n )\n const resolved = decrypted[config.labelColumn]\n if (typeof resolved === 'string' || typeof resolved === 'number') {\n labelValue = String(resolved)\n }\n }\n\n if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n }\n }\n\n if (row.id && labelValue != null && labelValue !== '') {\n labelMap.set(row.id, labelValue)\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 private resolveOrganizationId(): string | null {\n if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null\n return this.scope.organizationIds[0] ?? null\n }\n\n private resolveEntityMetadata(tableName: string): Record<string, any> | null {\n const registry = (this.em as any)?.getMetadata?.()\n if (!registry) return null\n const entries =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}))\n const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {})\n const match = metas.find((meta: any) => {\n const table = meta?.tableName ?? meta?.collection\n if (typeof table !== 'string') return false\n if (table === tableName) return true\n return table.split('.').pop() === tableName\n })\n return match ?? null\n }\n\n private resolveEntityPropertyName(meta: Record<string, any>, columnName: string): string | null {\n const properties = meta?.properties ? Object.values(meta.properties) : []\n for (const prop of properties as Array<Record<string, any>>) {\n const fieldName = prop?.fieldName\n const fieldNames = prop?.fieldNames\n if (typeof fieldName === 'string' && fieldName === columnName) return prop?.name ?? null\n if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null\n if (prop?.name === columnName) return prop?.name ?? null\n }\n return null\n }\n\n private resolveEntityId(meta: Record<string, any> | null): string | null {\n if (!meta) return null\n try {\n return resolveEntityIdFromMetadata(meta as any)\n } catch {\n return null\n }\n }\n\n /**\n * Resolves the encryption entity id for an analytics table. A request-scoped `EntityManager` fork\n * frequently reports an empty metadata registry, which used to make every group source look\n * unencrypted and send the request back to the ciphertext-grouping SQL path (#4622). The table\n * name alone is enough for the entity-id lookup, so it is the fallback.\n */\n private resolveEncryptionEntityId(tableName: string): string | null {\n return this.resolveEntityId(this.resolveEntityMetadata(tableName)) ?? this.resolveEntityId({ tableName })\n }\n\n private isEncryptedPayload(value: string): boolean {\n const parts = value.split(':')\n return parts.length === 4 && parts[3] === 'v1'\n }\n\n private decryptWithDek(value: string, dek: string): string | null {\n const first = decryptWithAesGcm(value, dek)\n if (first === null) return null\n if (!this.isEncryptedPayload(first)) return first\n return decryptWithAesGcm(first, dek) ?? first\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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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { createHash } from 'node:crypto'\nimport { decryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEntityIdFromMetadata } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n type DateRangePreset,\n resolveDateRange,\n getPreviousPeriod,\n calculatePercentageChange,\n determineChangeDirection,\n isValidDateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { TenantDek } from '@open-mercato/shared/lib/encryption/kms'\nimport { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'\nimport {\n type AggregateFunction,\n type DateGranularity,\n buildAggregationQuery,\n buildDistinctCurrencyQuery,\n buildGroupSourceRowsQuery,\n resolveGroupExpression,\n} from '../lib/aggregations'\nimport {\n type ExactDecimal,\n EXACT_DECIMAL_ZERO,\n addExactDecimal,\n compareExactDecimal,\n exactDecimalToNumber,\n parseExactDecimal,\n} from '../lib/exactDecimal'\nimport type { AnalyticsRegistry } from './analyticsRegistry'\nimport type { BaseCurrencyResolver } from '../lib/optionalBaseCurrency'\n\nconst logger = createLogger('dashboards').child({ component: 'widget-data-service' })\n\nconst WIDGET_DATA_CACHE_TTL = 120_000\nconst WIDGET_DATA_SEGMENT_TTL = 86_400_000\nconst WIDGET_DATA_SEGMENT_KEY = 'widget-data:__segment__'\n\n/**\n * Encrypted group sources cannot be grouped by the database, so the rows are scanned and\n * aggregated in application code. The cap keeps that scan bounded; overflowing it fails loudly\n * instead of charting a silently truncated result (#4622).\n */\nconst ENCRYPTED_GROUP_SCAN_LIMIT = 20_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\nexport class WidgetDataScanLimitError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataScanLimitError'\n }\n}\n\n/**\n * Raised when encryption is configured but the group source cannot currently be resolved \u2014 an\n * unhealthy KMS, an unreadable encryption map, or a missing tenant DEK. Grouping would either read\n * ciphertext in SQL or silently collapse every encrypted row into \"Unknown\", so the request fails\n * closed instead (#4622).\n */\nexport class WidgetDataEncryptionUnavailableError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'WidgetDataEncryptionUnavailableError'\n }\n}\n\ntype EncryptedGroupSource = {\n entityId: string\n dbColumn: string\n jsonPath: string | null\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\n/**\n * Encryption maps may register a field in either casing (`findKey` in the encryption service\n * accepts both), so match the analytics column tolerantly rather than by exact snake_case.\n */\nfunction matchesColumn(fieldName: string, dbColumn: string): boolean {\n const normalize = (value: string) => value.replace(/_/g, '').toLowerCase()\n return normalize(fieldName) === normalize(dbColumn)\n}\n\nfunction readJsonPath(value: unknown, path: string): unknown {\n let current = value\n for (const part of path.split('.')) {\n if (current === null || typeof current !== 'object') return null\n current = (current as Record<string, unknown>)[part]\n }\n return current\n}\n\n/**\n * Running aggregate state for one group. Values are folded in as they are scanned \u2014 the bucket\n * never holds the row set \u2014 so the scan cost stays linear and independent of the row cap.\n */\ntype GroupBucket = {\n /** Rows with a non-null metric column, mirroring SQL `COUNT(column)`. */\n count: number\n /** Rows whose metric parsed as a decimal, mirroring the row set SQL aggregates operate on. */\n numericCount: number\n /** Exact running sum, so money never accumulates in binary floating point. */\n sum: ExactDecimal\n min: ExactDecimal | null\n max: ExactDecimal | null\n}\n\nfunction createGroupBucket(): GroupBucket {\n return { count: 0, numericCount: 0, sum: EXACT_DECIMAL_ZERO, min: null, max: null }\n}\n\nfunction foldMetricValue(bucket: GroupBucket, value: ExactDecimal): void {\n bucket.numericCount += 1\n bucket.sum = addExactDecimal(bucket.sum, value)\n if (bucket.min === null || compareExactDecimal(value, bucket.min) < 0) bucket.min = value\n if (bucket.max === null || compareExactDecimal(value, bucket.max) > 0) bucket.max = value\n}\n\n/**\n * Mirrors the SQL semantics of `buildAggregateExpression` for application-side aggregation. That\n * expression wraps `SUM`/`AVG` in `COALESCE(..., 0)`, so an empty value set aggregates to `0` on\n * both paths, while the uncoalesced `MIN`/`MAX` keep PostgreSQL's `NULL`. An encrypted group source\n * must not report a different value than the same data would report in plaintext (#4622).\n */\nfunction aggregateBucket(aggregate: AggregateFunction, bucket: GroupBucket): number | null {\n switch (aggregate) {\n case 'count':\n return bucket.count\n case 'sum':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum)\n case 'avg':\n return bucket.numericCount === 0 ? 0 : exactDecimalToNumber(bucket.sum) / bucket.numericCount\n case 'min':\n return bucket.min === null ? null : exactDecimalToNumber(bucket.min)\n case 'max':\n return bucket.max === null ? null : exactDecimalToNumber(bucket.max)\n default:\n return bucket.count\n }\n}\n\n/**\n * Mirrors the SQL path's `ORDER BY value DESC NULLS LAST`. PostgreSQL defaults DESC to NULLS FIRST,\n * which would let empty buckets displace real ones under a group limit, so both paths state the\n * policy explicitly: highest value first, buckets without a value last.\n */\nfunction compareWidgetDataItemsByValueDesc(\n left: { value: number | null },\n right: { value: number | null },\n): number {\n if (left.value === null && right.value === null) return 0\n if (left.value === null) return 1\n if (right.value === null) return -1\n return right.value - left.value\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 currency?: string | null\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 baseCurrencyResolver?: BaseCurrencyResolver\n}\n\nexport class WidgetDataService {\n private em: EntityManager\n private scope: WidgetDataScope\n private registry: AnalyticsRegistry\n private cache?: CacheStrategy\n private baseCurrencyResolver?: BaseCurrencyResolver\n private baseCurrencyPromise?: Promise<string | null>\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 this.baseCurrencyResolver = options.baseCurrencyResolver\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 shouldFetchComparison = Boolean(comparisonRange && request.dateRange)\n\n const aggregatedRanges =\n shouldFetchComparison && comparisonRange ? [dateRangeResolved, comparisonRange] : [dateRangeResolved]\n\n const [mainResult, comparisonResult, currency] = await Promise.all([\n this.executeQuery(request, dateRangeResolved),\n shouldFetchComparison && comparisonRange\n ? this.executeQuery(request, comparisonRange)\n : Promise.resolve<{ value: number | null; data: WidgetDataItem[] } | undefined>(undefined),\n this.resolveCurrencyLabel(request, aggregatedRanges),\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 currency,\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 await this.cache.set(\n WIDGET_DATA_SEGMENT_KEY,\n { updatedAt: response.metadata.fetchedAt },\n { ttl: WIDGET_DATA_SEGMENT_TTL, tags: ['widget-data'] },\n )\n } catch {\n }\n }\n\n return response\n }\n\n /**\n * Resolves the currency the response may be labelled with. The base currency of the\n * scope is only half the answer: it describes how the organizations are configured, not\n * what the aggregated rows are actually denominated in. A PLN-based organization holding\n * both PLN and EUR orders would otherwise sum them and present the total as PLN (#4676),\n * so the base currency survives only when every aggregated row carries exactly that code.\n *\n * The check is deliberately conservative \u2014 anything it cannot prove resolves to `null`\n * and the widgets render an explicitly unlabelled number, which is recoverable, while a\n * confident wrong label is not.\n */\n private async resolveCurrencyLabel(\n request: WidgetDataRequest,\n aggregatedRanges: Array<{ start: Date; end: Date } | undefined>,\n ): Promise<string | null> {\n const baseCurrency = await this.resolveBaseCurrency()\n if (!baseCurrency) return null\n\n try {\n for (const range of aggregatedRanges) {\n const uniform = await this.rowsShareCurrency(request, range, baseCurrency)\n if (!uniform) return null\n }\n return baseCurrency\n } catch (err) {\n logger.warn('Row-currency uniformity check failed; leaving the amount unlabelled', {\n err,\n entityType: request.entityType,\n })\n return null\n }\n }\n\n /**\n * Reads the distinct per-row currencies of the rows one aggregation range would sum.\n * Entities that declare no `currencyField` carry no per-row currency to contradict the\n * base one, so they pass. An empty range has nothing to mislabel and also passes.\n *\n * A missing currency cannot prove the row uses the scope's base currency, so it fails\n * closed just like a conflicting code. An empty range has no row to mislabel and passes.\n */\n private async rowsShareCurrency(\n request: WidgetDataRequest,\n dateRange: { start: Date; end: Date } | undefined,\n expectedCode: string,\n ): Promise<boolean> {\n const query = buildDistinctCurrencyQuery({\n entityType: request.entityType,\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) return true\n\n const rows = await this.em.getConnection().execute(query.sql, query.params)\n const resultRows = (Array.isArray(rows) ? rows : []) as Array<Record<string, unknown>>\n\n const recordedCodes = new Set<string>()\n for (const row of resultRows) {\n const code = typeof row.code === 'string' ? row.code.trim().toUpperCase() : ''\n if (!code) return false\n recordedCodes.add(code)\n }\n\n if (recordedCodes.size === 0) return true\n if (recordedCodes.size > 1) return false\n return recordedCodes.has(expectedCode)\n }\n\n /**\n * Resolves the base currency of the current scope so money widgets can label amounts\n * with the tenant's own currency instead of a hard-coded default (#4620). The lookup\n * is soft: a scope spanning organizations with different base currencies, a missing\n * base currency, or an unavailable currencies module all resolve to `null`, and the\n * widgets then render unlabelled numbers rather than a wrong currency.\n */\n private resolveBaseCurrency(): Promise<string | null> {\n if (!this.baseCurrencyPromise) {\n const organizationIds = [...new Set(this.scope.organizationIds ?? [])]\n if (!this.baseCurrencyResolver || organizationIds.length === 0) {\n this.baseCurrencyPromise = Promise.resolve(null)\n } else {\n this.baseCurrencyPromise = this.baseCurrencyResolver\n .resolveBaseCurrency({ tenantId: this.scope.tenantId, organizationIds })\n .then((result) => result.status === 'resolved' ? result.code : null)\n .catch(() => null)\n }\n }\n return this.baseCurrencyPromise\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 if (request.groupBy) {\n const encryptedSource = await this.resolveEncryptedGroupSource(request.entityType, request.groupBy)\n if (encryptedSource) {\n return this.executeEncryptedGroupQuery(request, encryptedSource, dateRange)\n }\n }\n\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 /**\n * Reports the encrypted column a groupBy field reads from, or null when the source is stored in\n * plaintext and can be grouped by the database. Grouping over an encrypted column in SQL buckets\n * ciphertext and renders it to the user (#4622).\n */\n private async resolveEncryptedGroupSource(\n entityType: string,\n groupBy: NonNullable<WidgetDataRequest['groupBy']>,\n ): Promise<EncryptedGroupSource | null> {\n // Encryption is not configured for this deployment, so no column can hold ciphertext and the\n // database may group the source directly.\n if (!isTenantDataEncryptionEnabled()) return null\n\n const resolved = resolveGroupExpression(this.registry, entityType, groupBy)\n if (!resolved) return null\n\n const tableName = this.registry.getEntityTypeConfig(entityType)?.tableName\n if (!tableName) return null\n\n // Without an encryption entity id the table is outside the encryption map entirely, which is\n // the same state the encrypting subscriber sees when it skips the row.\n const entityId = this.resolveEncryptionEntityId(tableName)\n if (!entityId) return null\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n if (!encryptionService) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption service unavailable`,\n )\n }\n\n const organizationId = this.resolveOrganizationId()\n let encryptedFields: string[]\n try {\n // Deliberately independent of KMS health: the map describes how the rows were written, and\n // an unhealthy KMS must not be read as \"this column is plaintext\" (#4622).\n encryptedFields = await encryptionService.getEncryptedFieldNames(\n entityId,\n this.scope.tenantId,\n organizationId,\n { ignoreRuntimeHealth: true },\n )\n } catch (err) {\n logger.error('Failed to resolve encrypted fields for widget grouping', { err, entityId, entityType })\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot determine whether ${groupBy.field} is encrypted: encryption map lookup failed`,\n )\n }\n\n if (!encryptedFields.some((field) => matchesColumn(field, resolved.dbColumn))) return null\n\n if (resolved.jsonPath === null && groupBy.granularity) {\n throw new WidgetDataValidationError(\n `Cannot group encrypted field by granularity: ${groupBy.field}`,\n )\n }\n\n return { entityId, dbColumn: resolved.dbColumn, jsonPath: resolved.jsonPath }\n }\n\n /**\n * Aggregates an encrypted group source in application code: rows are scanned, the group column is\n * decrypted, and only the resulting plaintext keys are grouped. Values that cannot be decrypted\n * collapse into the null (\"Unknown\") bucket so ciphertext never reaches the response.\n */\n private async executeEncryptedGroupQuery(\n request: WidgetDataRequest,\n source: EncryptedGroupSource,\n dateRange?: { start: Date; end: Date },\n ): Promise<{ value: number | null; data: WidgetDataItem[] }> {\n const query = buildGroupSourceRowsQuery({\n entityType: request.entityType,\n metric: request.metric,\n dateRange: dateRange && request.dateRange ? { field: request.dateRange.field, ...dateRange } : undefined,\n filters: request.filters,\n scope: this.scope,\n registry: this.registry,\n groupColumn: source.dbColumn,\n rowLimit: ENCRYPTED_GROUP_SCAN_LIMIT,\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 (results.length > ENCRYPTED_GROUP_SCAN_LIMIT) {\n throw new WidgetDataScanLimitError(\n `Too many rows to group encrypted field ${request.groupBy?.field} (limit ${ENCRYPTED_GROUP_SCAN_LIMIT})`,\n )\n }\n\n const encryptionService = resolveTenantEncryptionService(this.em)\n let dek: TenantDek | null = null\n try {\n dek = encryptionService ? await encryptionService.getDek(this.scope.tenantId) : null\n } catch (err) {\n logger.error('Failed to resolve the tenant DEK for widget grouping', { err, entityId: source.entityId })\n dek = null\n }\n\n const buckets = new Map<string | null, GroupBucket>()\n let undecryptableRows = 0\n\n for (const row of results as Array<Record<string, unknown>>) {\n const resolvedKey = this.resolveDecryptedGroupKey(row.group_source, source, dek?.key ?? null)\n // An undecryptable source joins the null (\"Unknown\") bucket rather than being dropped, so the\n // widget total still matches the underlying rows.\n if (resolvedKey === undefined) undecryptableRows += 1\n const groupKey = resolvedKey === undefined ? null : resolvedKey\n\n let bucket = buckets.get(groupKey)\n if (!bucket) {\n bucket = createGroupBucket()\n buckets.set(groupKey, bucket)\n }\n\n const metricValue = row.metric_value\n if (metricValue === null || metricValue === undefined) continue\n bucket.count += 1\n const numeric = parseExactDecimal(metricValue)\n if (numeric) foldMetricValue(bucket, numeric)\n }\n\n if (undecryptableRows > 0) {\n // No DEK at all means every ciphertext row would collapse into \"Unknown\" and the widget would\n // report a confidently wrong distribution, so fail closed rather than chart it (#4622).\n if (!dek) {\n throw new WidgetDataEncryptionUnavailableError(\n `Cannot group encrypted field ${request.groupBy?.field}: tenant encryption key is unavailable`,\n )\n }\n logger.warn('Grouped rows with an undecryptable group source as unknown', {\n entityId: source.entityId,\n column: source.dbColumn,\n rows: undecryptableRows,\n })\n }\n\n let data: WidgetDataItem[] = Array.from(buckets.entries())\n .map(([groupKey, bucket]) => ({ groupKey, value: aggregateBucket(request.metric.aggregate, bucket) }))\n .sort(compareWidgetDataItemsByValueDesc)\n\n if (request.groupBy?.limit && request.groupBy.limit > 0) {\n data = data.slice(0, Math.min(request.groupBy.limit, 100))\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 /**\n * Returns the plaintext group key for a scanned row, `null` for an empty/absent value, or\n * `undefined` when the source is ciphertext that could not be decrypted.\n */\n private resolveDecryptedGroupKey(\n rawValue: unknown,\n source: EncryptedGroupSource,\n dek: string | null,\n ): string | null | undefined {\n if (rawValue === null || rawValue === undefined) return null\n\n let value: unknown = rawValue\n if (typeof value === 'string' && this.isEncryptedPayload(value)) {\n if (!dek) return undefined\n const decrypted = this.decryptWithDek(value, dek)\n if (decrypted === null) return undefined\n value = parseDecryptedFieldValue(decrypted)\n }\n\n if (source.jsonPath !== null) {\n value = readJsonPath(value, source.jsonPath)\n }\n\n if (value === null || value === undefined || value === '') return null\n if (typeof value === 'object') return null\n const key = String(value)\n // Defense in depth: a nested value that is itself ciphertext must never reach the response.\n return this.isEncryptedPayload(key) ? undefined : key\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 if (item.groupKey == null || item.groupKey === '') return { ...item, groupLabel: undefined }\n const label = String(item.groupKey)\n // A ciphertext group key has no meaningful label; echoing it would render encrypted data\n // into the chart legend (#4622).\n return { ...item, groupLabel: this.isEncryptedPayload(label) ? undefined : label }\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 meta = this.resolveEntityMetadata(config.table)\n const idProp = meta ? this.resolveEntityPropertyName(meta, config.idColumn) : null\n const labelProp = meta ? this.resolveEntityPropertyName(meta, config.labelColumn) : null\n const tenantProp = meta\n ? (this.resolveEntityPropertyName(meta, 'tenant_id') ?? this.resolveEntityPropertyName(meta, 'tenantId'))\n : null\n const organizationProp = meta\n ? (this.resolveEntityPropertyName(meta, 'organization_id') ?? this.resolveEntityPropertyName(meta, 'organizationId'))\n : null\n const entityName = meta ? ((meta as any).class ?? meta.className ?? meta.name) : null\n\n if (meta && idProp && labelProp && tenantProp && entityName) {\n const where: Record<string, unknown> = {\n [idProp]: { $in: uniqueIds },\n [tenantProp]: this.scope.tenantId,\n }\n if (organizationProp && this.scope.organizationIds && this.scope.organizationIds.length > 0) {\n where[organizationProp] = { $in: this.scope.organizationIds }\n }\n\n try {\n const records = await findWithDecryption(\n this.em,\n entityName,\n where,\n { fields: [idProp, labelProp, tenantProp, organizationProp].filter(Boolean) },\n { tenantId: this.scope.tenantId, organizationId: this.resolveOrganizationId() },\n )\n\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n let hasEncryptedLabels = false\n let hasDecryptedLabels = false\n\n const labelMap = new Map<string, string>()\n for (const record of records as Array<Record<string, unknown>>) {\n const id = record[idProp]\n let labelValue = record[labelProp]\n if (typeof labelValue === 'string' && this.isEncryptedPayload(labelValue)) {\n hasEncryptedLabels = true\n if (dek?.key) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n hasDecryptedLabels = true\n }\n }\n } else if (labelValue != null && labelValue !== '') {\n hasDecryptedLabels = true\n }\n\n if (typeof id === 'string' && labelValue != null && labelValue !== '') {\n labelMap.set(id, String(labelValue))\n }\n }\n\n if (labelMap.size > 0 && (!hasEncryptedLabels || hasDecryptedLabels)) {\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 }\n } catch {\n // fall through to SQL resolution\n }\n }\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, tenant_id, organization_id FROM \"${config.table}\" WHERE ${clauses.join(\n ' AND ',\n )}`\n\n try {\n const labelRows = await this.em.getConnection().execute(sql, params)\n const entityId = this.resolveEntityId(meta)\n const encryptionService = resolveTenantEncryptionService(this.em as any)\n const organizationId = this.resolveOrganizationId()\n const dek = encryptionService?.isEnabled() ? await encryptionService.getDek(this.scope.tenantId) : null\n\n const labelMap = new Map<string, string>()\n for (const row of labelRows as Array<{ id: string; label: string | null; tenant_id?: string | null; organization_id?: string | null }>) {\n let labelValue = row.label\n if (entityId && encryptionService?.isEnabled() && labelValue != null) {\n const rowOrgId = row.organization_id ?? organizationId ?? null\n const decrypted = await encryptionService.decryptEntityPayload(\n entityId,\n { [config.labelColumn]: labelValue },\n this.scope.tenantId,\n rowOrgId,\n )\n const resolved = decrypted[config.labelColumn]\n if (typeof resolved === 'string' || typeof resolved === 'number') {\n labelValue = String(resolved)\n }\n }\n\n if (labelValue && dek?.key && this.isEncryptedPayload(labelValue)) {\n const decrypted = this.decryptWithDek(labelValue, dek.key)\n if (decrypted !== null) {\n labelValue = decrypted\n }\n }\n\n if (row.id && labelValue != null && labelValue !== '') {\n labelMap.set(row.id, labelValue)\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 private resolveOrganizationId(): string | null {\n if (!this.scope.organizationIds || this.scope.organizationIds.length !== 1) return null\n return this.scope.organizationIds[0] ?? null\n }\n\n private resolveEntityMetadata(tableName: string): Record<string, any> | null {\n const registry = (this.em as any)?.getMetadata?.()\n if (!registry) return null\n const entries =\n (typeof registry.getAll === 'function' && registry.getAll()) ||\n (Array.isArray(registry.metadata) ? registry.metadata : Object.values(registry.metadata ?? {}))\n const metas = Array.isArray(entries) ? entries : Object.values(entries ?? {})\n const match = metas.find((meta: any) => {\n const table = meta?.tableName ?? meta?.collection\n if (typeof table !== 'string') return false\n if (table === tableName) return true\n return table.split('.').pop() === tableName\n })\n return match ?? null\n }\n\n private resolveEntityPropertyName(meta: Record<string, any>, columnName: string): string | null {\n const properties = meta?.properties ? Object.values(meta.properties) : []\n for (const prop of properties as Array<Record<string, any>>) {\n const fieldName = prop?.fieldName\n const fieldNames = prop?.fieldNames\n if (typeof fieldName === 'string' && fieldName === columnName) return prop?.name ?? null\n if (Array.isArray(fieldNames) && fieldNames.includes(columnName)) return prop?.name ?? null\n if (prop?.name === columnName) return prop?.name ?? null\n }\n return null\n }\n\n private resolveEntityId(meta: Record<string, any> | null): string | null {\n if (!meta) return null\n try {\n return resolveEntityIdFromMetadata(meta as any)\n } catch {\n return null\n }\n }\n\n /**\n * Resolves the encryption entity id for an analytics table. A request-scoped `EntityManager` fork\n * frequently reports an empty metadata registry, which used to make every group source look\n * unencrypted and send the request back to the ciphertext-grouping SQL path (#4622). The table\n * name alone is enough for the entity-id lookup, so it is the fallback.\n */\n private resolveEncryptionEntityId(tableName: string): string | null {\n return this.resolveEntityId(this.resolveEntityMetadata(tableName)) ?? this.resolveEntityId({ tableName })\n }\n\n private isEncryptedPayload(value: string): boolean {\n const parts = value.split(':')\n return parts.length === 4 && parts[3] === 'v1'\n }\n\n private decryptWithDek(value: string, dek: string): string | null {\n const first = decryptWithAesGcm(value, dek)\n if (first === null) return null\n if (!this.isEncryptedPayload(first)) return first\n return decryptWithAesGcm(first, dek) ?? first\n }\n}\n\nexport function createWidgetDataService(\n em: EntityManager,\n scope: WidgetDataScope,\n registry: AnalyticsRegistry,\n cache?: CacheStrategy,\n baseCurrencyResolver?: BaseCurrencyResolver,\n): WidgetDataService {\n return new WidgetDataService({ em, scope, registry, cache, baseCurrencyResolver })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,26 @@
1
+ import { runWithCacheTenant } from "@open-mercato/cache";
2
+ const metadata = {
3
+ event: "currencies.currency.*",
4
+ persistent: false,
5
+ id: "dashboards:invalidate-widget-data-on-currency-change"
6
+ };
7
+ async function handle(payload, context) {
8
+ const data = payload ?? {};
9
+ const tenantId = typeof data.tenantId === "string" ? data.tenantId : null;
10
+ if (!tenantId) return;
11
+ let cache;
12
+ try {
13
+ cache = context.resolve("cache");
14
+ } catch {
15
+ return;
16
+ }
17
+ try {
18
+ await runWithCacheTenant(tenantId, () => cache.deleteByTags(["widget-data"]));
19
+ } catch {
20
+ }
21
+ }
22
+ export {
23
+ handle as default,
24
+ metadata
25
+ };
26
+ //# sourceMappingURL=invalidateWidgetDataCache.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/dashboards/subscribers/invalidateWidgetDataCache.ts"],
4
+ "sourcesContent": ["import { runWithCacheTenant } from '@open-mercato/cache'\n\ntype CacheService = {\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport const metadata = {\n event: 'currencies.currency.*',\n persistent: false,\n id: 'dashboards:invalidate-widget-data-on-currency-change',\n}\n\nexport default async function handle(\n payload: unknown,\n context: { resolve: <T = unknown>(name: string) => T },\n): Promise<void> {\n const data = (payload ?? {}) as Record<string, unknown>\n const tenantId = typeof data.tenantId === 'string' ? data.tenantId : null\n if (!tenantId) return\n\n let cache: CacheService\n try {\n cache = context.resolve<CacheService>('cache')\n } catch {\n return\n }\n\n try {\n await runWithCacheTenant(tenantId, () => cache.deleteByTags(['widget-data']))\n } catch {\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,0BAA0B;AAM5B,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,eAAO,OACL,SACA,SACe;AACf,QAAM,OAAQ,WAAW,CAAC;AAC1B,QAAM,WAAW,OAAO,KAAK,aAAa,WAAW,KAAK,WAAW;AACrE,MAAI,CAAC,SAAU;AAEf,MAAI;AACJ,MAAI;AACF,YAAQ,QAAQ,QAAsB,OAAO;AAAA,EAC/C,QAAQ;AACN;AAAA,EACF;AAEA,MAAI;AACF,UAAM,mBAAmB,UAAU,MAAM,MAAM,aAAa,CAAC,aAAa,CAAC,CAAC;AAAA,EAC9E,QAAQ;AAAA,EACR;AACF;",
6
+ "names": []
7
+ }
@@ -2,7 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useWidgetData } from "@open-mercato/ui/backend/dashboard/widgetData";
5
- import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
6
6
  import { KpiCard } from "@open-mercato/ui/backend/charts";
7
7
  import {
8
8
  DateRangeSelect,
@@ -10,7 +10,8 @@ import {
10
10
  getComparisonLabelKey
11
11
  } from "@open-mercato/ui/backend/date-range";
12
12
  import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
13
- import { formatCurrencyWithDecimals } from "../../../lib/formatters.js";
13
+ import { createCurrencyFormatters } from "../../../lib/formatters.js";
14
+ import { UnlabelledAmountNotice } from "../../../components/UnlabelledAmountNotice.js";
14
15
  import { createLogger } from "@open-mercato/shared/lib/logger";
15
16
  const logger = createLogger("dashboards").child({ component: "aov-kpi" });
16
17
  async function fetchAovData(settings, fetchWidgetData) {
@@ -36,11 +37,14 @@ const AovKpiWidget = ({
36
37
  onRefreshStateChange
37
38
  }) => {
38
39
  const t = useT();
40
+ const locale = useLocale();
39
41
  const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
40
42
  const [value, setValue] = React.useState(null);
41
43
  const [trend, setTrend] = React.useState(void 0);
44
+ const [currency, setCurrency] = React.useState(null);
42
45
  const [loading, setLoading] = React.useState(true);
43
46
  const [error, setError] = React.useState(null);
47
+ const money = React.useMemo(() => createCurrencyFormatters(currency, "--", locale), [currency, locale]);
44
48
  const fetchWidgetData = useWidgetData();
45
49
  const refresh = React.useCallback(async () => {
46
50
  onRefreshStateChange?.(true);
@@ -49,6 +53,7 @@ const AovKpiWidget = ({
49
53
  try {
50
54
  const data = await fetchAovData(hydrated, fetchWidgetData);
51
55
  setValue(data.value);
56
+ setCurrency(data.metadata?.currency ?? null);
52
57
  if (data.comparison) {
53
58
  setTrend({
54
59
  value: data.comparison.change,
@@ -104,7 +109,8 @@ const AovKpiWidget = ({
104
109
  comparisonLabel,
105
110
  loading,
106
111
  error,
107
- formatValue: formatCurrencyWithDecimals,
112
+ formatValue: money.formatWithDecimals,
113
+ footer: /* @__PURE__ */ jsx(UnlabelledAmountNotice, { currency, loading, error }),
108
114
  headerAction: /* @__PURE__ */ jsx(
109
115
  InlineDateRangeSelect,
110
116
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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 { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\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'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'aov-kpi' })\n\nasync function fetchAovData(settings: AovKpiSettings, fetchWidgetData: WidgetDataFetcher): 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 return fetchWidgetData<WidgetDataResponse>(body)\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 fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await fetchAovData(hydrated, fetchWidgetData)\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 logger.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, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"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-visible:ring-ring\"\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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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { 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 { createCurrencyFormatters } from '../../../lib/formatters'\nimport { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'aov-kpi' })\n\nasync function fetchAovData(settings: AovKpiSettings, fetchWidgetData: WidgetDataFetcher): 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 return fetchWidgetData<WidgetDataResponse>(body)\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 locale = useLocale()\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 [currency, setCurrency] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await fetchAovData(hydrated, fetchWidgetData)\n setValue(data.value)\n setCurrency(data.metadata?.currency ?? null)\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 logger.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, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"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-visible:ring-ring\"\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={money.formatWithDecimals}\n footer={<UnlabelledAmountNotice currency={currency} loading={loading} error={error} />}\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
6
  "names": []
7
7
  }
@@ -2,7 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useWidgetData } from "@open-mercato/ui/backend/dashboard/widgetData";
5
- import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
6
6
  import { BarChart } from "@open-mercato/ui/backend/charts";
7
7
  import {
8
8
  DateRangeSelect,
@@ -14,7 +14,8 @@ import {
14
14
  buildPipelineDataRequest,
15
15
  hydrateSettings
16
16
  } from "./config.js";
17
- import { formatCurrencyCompact } from "../../../lib/formatters.js";
17
+ import { createCurrencyFormatters } from "../../../lib/formatters.js";
18
+ import { UnlabelledAmountNotice } from "../../../components/UnlabelledAmountNotice.js";
18
19
  import { createLogger } from "@open-mercato/shared/lib/logger";
19
20
  const logger = createLogger("dashboards").child({ component: "pipeline-summary" });
20
21
  async function fetchPipelineData(settings, fetchWidgetData) {
@@ -36,10 +37,13 @@ const PipelineSummaryWidget = ({
36
37
  onRefreshStateChange
37
38
  }) => {
38
39
  const t = useT();
40
+ const locale = useLocale();
39
41
  const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
40
42
  const [data, setData] = React.useState([]);
43
+ const [currency, setCurrency] = React.useState(null);
41
44
  const [loading, setLoading] = React.useState(true);
42
45
  const [error, setError] = React.useState(null);
46
+ const money = React.useMemo(() => createCurrencyFormatters(currency, "--", locale), [currency, locale]);
43
47
  const fetchWidgetData = useWidgetData();
44
48
  const refresh = React.useCallback(async () => {
45
49
  onRefreshStateChange?.(true);
@@ -52,6 +56,7 @@ const PipelineSummaryWidget = ({
52
56
  Value: item.value ?? 0
53
57
  }));
54
58
  setData(chartData);
59
+ setCurrency(result.metadata?.currency ?? null);
55
60
  } catch (err) {
56
61
  logger.error("Failed to load pipeline data", { err });
57
62
  setError(t("dashboards.analytics.widgets.pipelineSummary.error", "Failed to load data"));
@@ -118,12 +123,13 @@ const PipelineSummaryWidget = ({
118
123
  categoryLabels: { Value: t("dashboards.analytics.labels.value", "Value") },
119
124
  loading,
120
125
  error,
121
- valueFormatter: formatCurrencyCompact,
126
+ valueFormatter: money.formatCompact,
122
127
  colors: ["violet"],
123
128
  showLegend: false,
124
129
  emptyMessage: t("dashboards.analytics.widgets.pipelineSummary.empty", "No deal data for this period")
125
130
  }
126
- ) })
131
+ ) }),
132
+ /* @__PURE__ */ jsx(UnlabelledAmountNotice, { currency, loading, error })
127
133
  ] });
128
134
  };
129
135
  var widget_client_default = PipelineSummaryWidget;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport {\n DEFAULT_SETTINGS,\n buildPipelineDataRequest,\n hydrateSettings,\n type PipelineStatusScope,\n type PipelineSummarySettings,\n} from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyCompact } from '../../../lib/formatters'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'pipeline-summary' })\n\nasync function fetchPipelineData(settings: PipelineSummarySettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n return fetchWidgetData<WidgetDataResponse>(buildPipelineDataRequest(settings))\n}\n\nfunction formatStageLabel(stage: unknown, t: (key: string, fallback: string) => string): string {\n if (stage == null || stage === '') return t('dashboards.analytics.labels.unknown', 'Unknown')\n const stageStr = String(stage)\n if (stageStr === '0' || stageStr === 'null' || stageStr === 'undefined') {\n return t('dashboards.analytics.labels.unknown', 'Unknown')\n }\n return stageStr\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (l) => l.toUpperCase())\n}\n\nconst PipelineSummaryWidget: React.FC<DashboardWidgetComponentProps<PipelineSummarySettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchPipelineData(hydrated, fetchWidgetData)\n const chartData = result.data\n .filter((item) => item.groupKey != null && item.groupKey !== '' && String(item.groupKey) !== '0')\n .map((item) => ({\n stage: formatStageLabel(item.groupLabel ?? item.groupKey, t),\n Value: item.value ?? 0,\n }))\n setData(chartData)\n } catch (err) {\n logger.error('Failed to load pipeline data', { err })\n setError(t('dashboards.analytics.widgets.pipelineSummary.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"pipeline-summary-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=\"pipeline-summary-status-scope\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.dealStatusScope', 'Deals included')}\n </label>\n <Select\n value={hydrated.statusScope}\n onValueChange={(value) => onSettingsChange({ ...hydrated, statusScope: value as PipelineStatusScope })}\n >\n <SelectTrigger id=\"pipeline-summary-status-scope\" size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"open\">\n {t('dashboards.analytics.settings.dealStatusScopeOpen', 'Open deals only')}\n </SelectItem>\n <SelectItem value=\"all\">\n {t('dashboards.analytics.settings.dealStatusScopeAll', 'All deals, including won and lost')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex justify-end mb-2\">\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n </div>\n <div className=\"flex-1 min-h-0\">\n <BarChart\n data={data}\n index=\"stage\"\n categories={['Value']}\n categoryLabels={{ Value: t('dashboards.analytics.labels.value', 'Value') }}\n loading={loading}\n error={error}\n valueFormatter={formatCurrencyCompact}\n colors={['violet']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.pipelineSummary.empty', 'No deal data for this period')}\n />\n </div>\n </div>\n )\n}\n\nexport default PipelineSummaryWidget\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { BarChart, type BarChartDataItem } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n} from '@open-mercato/ui/backend/date-range'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport {\n DEFAULT_SETTINGS,\n buildPipelineDataRequest,\n hydrateSettings,\n type PipelineStatusScope,\n type PipelineSummarySettings,\n} from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { createCurrencyFormatters } from '../../../lib/formatters'\nimport { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'pipeline-summary' })\n\nasync function fetchPipelineData(settings: PipelineSummarySettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n return fetchWidgetData<WidgetDataResponse>(buildPipelineDataRequest(settings))\n}\n\nfunction formatStageLabel(stage: unknown, t: (key: string, fallback: string) => string): string {\n if (stage == null || stage === '') return t('dashboards.analytics.labels.unknown', 'Unknown')\n const stageStr = String(stage)\n if (stageStr === '0' || stageStr === 'null' || stageStr === 'undefined') {\n return t('dashboards.analytics.labels.unknown', 'Unknown')\n }\n return stageStr\n .replace(/_/g, ' ')\n .replace(/\\b\\w/g, (l) => l.toUpperCase())\n}\n\nconst PipelineSummaryWidget: React.FC<DashboardWidgetComponentProps<PipelineSummarySettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const locale = useLocale()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [data, setData] = React.useState<BarChartDataItem[]>([])\n const [currency, setCurrency] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const result = await fetchPipelineData(hydrated, fetchWidgetData)\n const chartData = result.data\n .filter((item) => item.groupKey != null && item.groupKey !== '' && String(item.groupKey) !== '0')\n .map((item) => ({\n stage: formatStageLabel(item.groupLabel ?? item.groupKey, t),\n Value: item.value ?? 0,\n }))\n setData(chartData)\n setCurrency(result.metadata?.currency ?? null)\n } catch (err) {\n logger.error('Failed to load pipeline data', { err })\n setError(t('dashboards.analytics.widgets.pipelineSummary.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"pipeline-summary-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=\"pipeline-summary-status-scope\"\n className=\"text-xs font-semibold uppercase text-muted-foreground\"\n >\n {t('dashboards.analytics.settings.dealStatusScope', 'Deals included')}\n </label>\n <Select\n value={hydrated.statusScope}\n onValueChange={(value) => onSettingsChange({ ...hydrated, statusScope: value as PipelineStatusScope })}\n >\n <SelectTrigger id=\"pipeline-summary-status-scope\" size=\"sm\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"open\">\n {t('dashboards.analytics.settings.dealStatusScopeOpen', 'Open deals only')}\n </SelectItem>\n <SelectItem value=\"all\">\n {t('dashboards.analytics.settings.dealStatusScopeAll', 'All deals, including won and lost')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n </div>\n )\n }\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex justify-end mb-2\">\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n </div>\n <div className=\"flex-1 min-h-0\">\n <BarChart\n data={data}\n index=\"stage\"\n categories={['Value']}\n categoryLabels={{ Value: t('dashboards.analytics.labels.value', 'Value') }}\n loading={loading}\n error={error}\n valueFormatter={money.formatCompact}\n colors={['violet']}\n showLegend={false}\n emptyMessage={t('dashboards.analytics.widgets.pipelineSummary.empty', 'No deal data for this period')}\n />\n </div>\n <UnlabelledAmountNotice currency={currency} loading={loading} error={error} />\n </div>\n )\n}\n\nexport default PipelineSummaryWidget\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,7 +2,7 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useWidgetData } from "@open-mercato/ui/backend/dashboard/widgetData";
5
- import { useT } from "@open-mercato/shared/lib/i18n/context";
5
+ import { useLocale, useT } from "@open-mercato/shared/lib/i18n/context";
6
6
  import { KpiCard } from "@open-mercato/ui/backend/charts";
7
7
  import {
8
8
  DateRangeSelect,
@@ -10,7 +10,8 @@ import {
10
10
  getComparisonLabelKey
11
11
  } from "@open-mercato/ui/backend/date-range";
12
12
  import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
13
- import { formatCurrency } from "../../../lib/formatters.js";
13
+ import { createCurrencyFormatters } from "../../../lib/formatters.js";
14
+ import { UnlabelledAmountNotice } from "../../../components/UnlabelledAmountNotice.js";
14
15
  import { createLogger } from "@open-mercato/shared/lib/logger";
15
16
  const logger = createLogger("dashboards").child({ component: "revenue-kpi" });
16
17
  async function fetchRevenueData(settings, fetchWidgetData) {
@@ -36,11 +37,14 @@ const RevenueKpiWidget = ({
36
37
  onRefreshStateChange
37
38
  }) => {
38
39
  const t = useT();
40
+ const locale = useLocale();
39
41
  const hydrated = React.useMemo(() => hydrateSettings(settings), [settings]);
40
42
  const [value, setValue] = React.useState(null);
41
43
  const [trend, setTrend] = React.useState(void 0);
44
+ const [currency, setCurrency] = React.useState(null);
42
45
  const [loading, setLoading] = React.useState(true);
43
46
  const [error, setError] = React.useState(null);
47
+ const money = React.useMemo(() => createCurrencyFormatters(currency, "--", locale), [currency, locale]);
44
48
  const fetchWidgetData = useWidgetData();
45
49
  const refresh = React.useCallback(async () => {
46
50
  onRefreshStateChange?.(true);
@@ -49,6 +53,7 @@ const RevenueKpiWidget = ({
49
53
  try {
50
54
  const data = await fetchRevenueData(hydrated, fetchWidgetData);
51
55
  setValue(data.value);
56
+ setCurrency(data.metadata?.currency ?? null);
52
57
  if (data.comparison) {
53
58
  setTrend({
54
59
  value: data.comparison.change,
@@ -104,7 +109,8 @@ const RevenueKpiWidget = ({
104
109
  comparisonLabel,
105
110
  loading,
106
111
  error,
107
- formatValue: formatCurrency,
112
+ formatValue: money.format,
113
+ footer: /* @__PURE__ */ jsx(UnlabelledAmountNotice, { currency, loading, error }),
108
114
  headerAction: /* @__PURE__ */ jsx(
109
115
  InlineDateRangeSelect,
110
116
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/widgets/dashboard/revenue-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 { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\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 RevenueKpiSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrency } from '../../../lib/formatters'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'revenue-kpi' })\n\nasync function fetchRevenueData(settings: RevenueKpiSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:orders',\n metric: {\n field: 'grandTotalGrossAmount',\n aggregate: 'sum',\n },\n dateRange: {\n field: 'placedAt',\n preset: settings.dateRange,\n },\n comparison: settings.showComparison ? { type: 'previous_period' } : undefined,\n }\n\n return fetchWidgetData<WidgetDataResponse>(body)\n}\n\nconst RevenueKpiWidget: React.FC<DashboardWidgetComponentProps<RevenueKpiSettings>> = ({\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 fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await fetchRevenueData(hydrated, fetchWidgetData)\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 logger.error('Failed to load revenue KPI data', { err })\n setError(t('dashboards.analytics.widgets.revenueKpi.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"revenue-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-visible:ring-ring\"\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={formatCurrency}\n headerAction={\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n }\n />\n )\n}\n\nexport default RevenueKpiWidget\n"],
5
- "mappings": ";AAmFQ,cAOE,YAPF;AAjFR,YAAY,WAAW;AAEvB,SAAS,qBAA6C;AACtD,SAAS,YAAY;AACrB,SAAS,eAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AACP,SAAS,kBAAkB,uBAAgD;AAE3E,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,YAAY,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAE5E,eAAe,iBAAiB,UAA8B,iBAAiE;AAC7H,QAAM,OAAO;AAAA,IACX,YAAY;AAAA,IACZ,QAAQ;AAAA,MACN,OAAO;AAAA,MACP,WAAW;AAAA,IACb;AAAA,IACA,WAAW;AAAA,MACT,OAAO;AAAA,MACP,QAAQ,SAAS;AAAA,IACnB;AAAA,IACA,YAAY,SAAS,iBAAiB,EAAE,MAAM,kBAAkB,IAAI;AAAA,EACtE;AAEA,SAAO,gBAAoC,IAAI;AACjD;AAEA,MAAM,mBAAgF,CAAC;AAAA,EACrF;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,IAAI,KAAK;AACf,QAAM,WAAW,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,GAAG,CAAC,QAAQ,CAAC;AAC1E,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAA+B,MAAS;AACxE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAE5D,QAAM,kBAAkB,cAAc;AACtC,QAAM,UAAU,MAAM,YAAY,YAAY;AAC5C,2BAAuB,IAAI;AAC3B,eAAW,IAAI;AACf,aAAS,IAAI;AACb,QAAI;AACF,YAAM,OAAO,MAAM,iBAAiB,UAAU,eAAe;AAC7D,eAAS,KAAK,KAAK;AACnB,UAAI,KAAK,YAAY;AACnB,iBAAS;AAAA,UACP,OAAO,KAAK,WAAW;AAAA,UACvB,WAAW,KAAK,WAAW;AAAA,QAC7B,CAAC;AAAA,MACH,OAAO;AACL,iBAAS,MAAS;AAAA,MACpB;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,MAAM,mCAAmC,EAAE,IAAI,CAAC;AACvD,eAAS,EAAE,iDAAiD,qBAAqB,CAAC;AAAA,IACpF,UAAE;AACA,iBAAW,KAAK;AAChB,6BAAuB,KAAK;AAAA,IAC9B;AAAA,EACF,GAAG,CAAC,UAAU,iBAAiB,sBAAsB,CAAC,CAAC;AAEvD,QAAM,UAAU,MAAM;AACpB,YAAQ,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAC1B,GAAG,CAAC,SAAS,YAAY,CAAC;AAE1B,MAAI,SAAS,YAAY;AACvB,WACE,qBAAC,SAAI,WAAU,qBACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,OAAO,EAAE,2CAA2C,YAAY;AAAA,UAChE,OAAO,SAAS;AAAA,UAChB,UAAU,CAAC,cAA+B,iBAAiB,EAAE,GAAG,UAAU,UAAU,CAAC;AAAA;AAAA,MACvF;AAAA,MACA,oBAAC,SAAI,WAAU,eACb,+BAAC,WAAM,WAAU,mCACf;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS,SAAS;AAAA,YAClB,UAAU,CAAC,MAAM,iBAAiB,EAAE,GAAG,UAAU,gBAAgB,EAAE,OAAO,QAAQ,CAAC;AAAA,YACnF,WAAU;AAAA;AAAA,QACZ;AAAA,QACC,EAAE,gDAAgD,iBAAiB;AAAA,SACtE,GACF;AAAA,OACF;AAAA,EAEJ;AAEA,QAAM,sBAAsB,sBAAsB,SAAS,SAAS;AACpE,QAAM,kBAAkB,SAAS,iBAC7B,EAAE,oBAAoB,KAAK,oBAAoB,QAAQ,IACvD;AAEJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,aAAa;AAAA,MACb,cACE;AAAA,QAAC;AAAA;AAAA,UACC,OAAO,SAAS;AAAA,UAChB,UAAU,CAAC,cAAc,iBAAiB,EAAE,GAAG,UAAU,UAAU,CAAC;AAAA;AAAA,MACtE;AAAA;AAAA,EAEJ;AAEJ;AAEA,IAAO,wBAAQ;",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { DashboardWidgetComponentProps } from '@open-mercato/shared/modules/dashboard/widgets'\nimport { useWidgetData, type WidgetDataFetcher } from '@open-mercato/ui/backend/dashboard/widgetData'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { 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 RevenueKpiSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { createCurrencyFormatters } from '../../../lib/formatters'\nimport { UnlabelledAmountNotice } from '../../../components/UnlabelledAmountNotice'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('dashboards').child({ component: 'revenue-kpi' })\n\nasync function fetchRevenueData(settings: RevenueKpiSettings, fetchWidgetData: WidgetDataFetcher): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:orders',\n metric: {\n field: 'grandTotalGrossAmount',\n aggregate: 'sum',\n },\n dateRange: {\n field: 'placedAt',\n preset: settings.dateRange,\n },\n comparison: settings.showComparison ? { type: 'previous_period' } : undefined,\n }\n\n return fetchWidgetData<WidgetDataResponse>(body)\n}\n\nconst RevenueKpiWidget: React.FC<DashboardWidgetComponentProps<RevenueKpiSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const locale = useLocale()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [value, setValue] = React.useState<number | null>(null)\n const [trend, setTrend] = React.useState<KpiTrend | undefined>(undefined)\n const [currency, setCurrency] = React.useState<string | null>(null)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const money = React.useMemo(() => createCurrencyFormatters(currency, '--', locale), [currency, locale])\n\n const fetchWidgetData = useWidgetData()\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await fetchRevenueData(hydrated, fetchWidgetData)\n setValue(data.value)\n setCurrency(data.metadata?.currency ?? null)\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 logger.error('Failed to load revenue KPI data', { err })\n setError(t('dashboards.analytics.widgets.revenueKpi.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, fetchWidgetData, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"revenue-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-visible:ring-ring\"\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={money.format}\n footer={<UnlabelledAmountNotice currency={currency} loading={loading} error={error} />}\n headerAction={\n <InlineDateRangeSelect\n value={hydrated.dateRange}\n onChange={(dateRange) => onSettingsChange({ ...hydrated, dateRange })}\n />\n }\n />\n )\n}\n\nexport default RevenueKpiWidget\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }