@open-mercato/core 0.6.4-develop.4121.1.0d7f20d229 → 0.6.4-develop.4133.1.48fc6c8f7b

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 (44) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/dashboards/api/widgets/data/batch/route.js +137 -0
  3. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +7 -0
  4. package/dist/modules/dashboards/api/widgets/data/route.js +1 -75
  5. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  6. package/dist/modules/dashboards/api/widgets/data/schema.js +85 -0
  7. package/dist/modules/dashboards/api/widgets/data/schema.js.map +7 -0
  8. package/dist/modules/dashboards/lib/widgetDataBatch.js +49 -0
  9. package/dist/modules/dashboards/lib/widgetDataBatch.js.map +7 -0
  10. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js +6 -14
  11. package/dist/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.js.map +2 -2
  12. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js +6 -14
  13. package/dist/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.js.map +2 -2
  14. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +6 -14
  15. package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +2 -2
  16. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js +6 -14
  17. package/dist/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.js.map +2 -2
  18. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +6 -14
  19. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
  20. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js +6 -14
  21. package/dist/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.js.map +2 -2
  22. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +6 -14
  23. package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
  24. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +6 -14
  25. package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
  26. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +6 -14
  27. package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
  28. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +6 -14
  29. package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
  30. package/package.json +7 -7
  31. package/src/modules/dashboards/api/widgets/data/batch/route.ts +168 -0
  32. package/src/modules/dashboards/api/widgets/data/route.ts +1 -90
  33. package/src/modules/dashboards/api/widgets/data/schema.ts +90 -0
  34. package/src/modules/dashboards/lib/widgetDataBatch.ts +89 -0
  35. package/src/modules/dashboards/widgets/dashboard/aov-kpi/widget.client.tsx +6 -16
  36. package/src/modules/dashboards/widgets/dashboard/new-customers-kpi/widget.client.tsx +6 -16
  37. package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +6 -16
  38. package/src/modules/dashboards/widgets/dashboard/orders-kpi/widget.client.tsx +6 -16
  39. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +6 -16
  40. package/src/modules/dashboards/widgets/dashboard/revenue-kpi/widget.client.tsx +6 -16
  41. package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +6 -16
  42. package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +6 -16
  43. package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +6 -16
  44. package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +6 -16
@@ -1,4 +1,4 @@
1
- [build:core] found 2645 entry points
1
+ [build:core] found 2648 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 172 entry points
4
4
  [build:core:generated] built successfully
@@ -0,0 +1,137 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
6
+ import {
7
+ createWidgetDataService,
8
+ WidgetDataValidationError
9
+ } from "../../../../services/widgetDataService.js";
10
+ import { runWidgetDataBatch } from "../../../../lib/widgetDataBatch.js";
11
+ import { dashboardsTag, dashboardsErrorSchema } from "../../../openapi.js";
12
+ import { widgetDataRequestSchema, widgetDataResponseSchema } from "../schema.js";
13
+ const metadata = {
14
+ POST: { requireAuth: true, requireFeatures: ["analytics.view"] }
15
+ };
16
+ const MAX_BATCH_SIZE = 50;
17
+ const widgetDataBatchRequestSchema = z.object({
18
+ requests: z.array(
19
+ z.object({
20
+ id: z.string().min(1),
21
+ request: widgetDataRequestSchema
22
+ })
23
+ ).min(1).max(MAX_BATCH_SIZE)
24
+ });
25
+ const widgetDataBatchResponseSchema = z.object({
26
+ results: z.array(
27
+ z.discriminatedUnion("ok", [
28
+ z.object({
29
+ id: z.string(),
30
+ ok: z.literal(true),
31
+ data: widgetDataResponseSchema
32
+ }),
33
+ z.object({
34
+ id: z.string(),
35
+ ok: z.literal(false),
36
+ error: z.string()
37
+ })
38
+ ])
39
+ )
40
+ });
41
+ async function POST(req) {
42
+ const auth = await getAuthFromRequest(req);
43
+ if (!auth) {
44
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
45
+ }
46
+ let body;
47
+ try {
48
+ body = await req.json();
49
+ } catch {
50
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
51
+ }
52
+ const parsed = widgetDataBatchRequestSchema.safeParse(body);
53
+ if (!parsed.success) {
54
+ return NextResponse.json(
55
+ { error: "Invalid request payload", issues: parsed.error.issues },
56
+ { status: 400 }
57
+ );
58
+ }
59
+ const tenantId = auth.tenantId ?? null;
60
+ if (!tenantId) {
61
+ return NextResponse.json({ error: "Tenant context is required" }, { status: 400 });
62
+ }
63
+ const container = await createRequestContainer();
64
+ const analyticsRegistry = container.resolve("analyticsRegistry");
65
+ const em = container.resolve("em").fork({
66
+ clear: true,
67
+ freshEventManager: true,
68
+ useContext: true
69
+ });
70
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
71
+ const organizationIds = (() => {
72
+ if (scope?.selectedId) return [scope.selectedId];
73
+ if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds;
74
+ if (scope?.allowedIds === null) return void 0;
75
+ if (auth.orgId) return [auth.orgId];
76
+ return void 0;
77
+ })();
78
+ const cache = container.resolve("cache");
79
+ const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache);
80
+ const rbacService = container.resolve("rbacService");
81
+ try {
82
+ const results = await runWidgetDataBatch(parsed.data.requests, {
83
+ getRequiredFeatures: (entityType) => analyticsRegistry.getRequiredFeatures(entityType),
84
+ checkFeatures: (features) => {
85
+ if (features.length === 0) return Promise.resolve(true);
86
+ return rbacService.userHasAllFeatures(auth.sub, features, {
87
+ tenantId,
88
+ organizationId: auth.orgId
89
+ });
90
+ },
91
+ fetchOne: (request) => service.fetchWidgetData(request),
92
+ describeError: (error) => error instanceof WidgetDataValidationError ? error.message : "An error occurred while processing your request"
93
+ });
94
+ return NextResponse.json({ results });
95
+ } catch (err) {
96
+ console.error("[widgets/data/batch] Error:", err);
97
+ return NextResponse.json(
98
+ { error: "An error occurred while processing your request" },
99
+ { status: 500 }
100
+ );
101
+ }
102
+ }
103
+ const widgetDataBatchPostDoc = {
104
+ summary: "Fetch aggregated data for multiple dashboard widgets in one request",
105
+ description: "Resolves a batch of widget data requests with a single authentication, RBAC, organization-scope, and database-context setup. Each request is keyed by an opaque widget id and resolved independently, so a failure in one widget does not fail the batch.",
106
+ tags: [dashboardsTag],
107
+ requestBody: {
108
+ contentType: "application/json",
109
+ schema: widgetDataBatchRequestSchema,
110
+ description: "A list of id-keyed widget data requests to resolve together."
111
+ },
112
+ responses: [
113
+ {
114
+ status: 200,
115
+ description: "Per-widget aggregation results keyed by request id.",
116
+ schema: widgetDataBatchResponseSchema
117
+ }
118
+ ],
119
+ errors: [
120
+ { status: 400, description: "Invalid request payload", schema: dashboardsErrorSchema },
121
+ { status: 401, description: "Authentication required", schema: dashboardsErrorSchema },
122
+ { status: 500, description: "Internal server error", schema: dashboardsErrorSchema }
123
+ ]
124
+ };
125
+ const openApi = {
126
+ tag: dashboardsTag,
127
+ summary: "Batch widget data aggregation endpoint",
128
+ methods: {
129
+ POST: widgetDataBatchPostDoc
130
+ }
131
+ };
132
+ export {
133
+ POST,
134
+ metadata,
135
+ openApi
136
+ };
137
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/dashboards/api/widgets/data/batch/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n} from '../../../../services/widgetDataService'\nimport { runWidgetDataBatch } from '../../../../lib/widgetDataBatch'\nimport type { AnalyticsRegistry } from '../../../../services/analyticsRegistry'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../../openapi'\nimport { widgetDataRequestSchema, widgetDataResponseSchema } from '../schema'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nconst MAX_BATCH_SIZE = 50\n\nconst widgetDataBatchRequestSchema = z.object({\n requests: z\n .array(\n z.object({\n id: z.string().min(1),\n request: widgetDataRequestSchema,\n }),\n )\n .min(1)\n .max(MAX_BATCH_SIZE),\n})\n\nconst widgetDataBatchResponseSchema = z.object({\n results: z.array(\n z.discriminatedUnion('ok', [\n z.object({\n id: z.string(),\n ok: z.literal(true),\n data: widgetDataResponseSchema,\n }),\n z.object({\n id: z.string(),\n ok: z.literal(false),\n error: z.string(),\n }),\n ]),\n ),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataBatchRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n // Build the per-request DI/RBAC/org-scope stack exactly once for the whole\n // batch instead of once per widget (see issue #2273).\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n\n try {\n const results = await runWidgetDataBatch(parsed.data.requests as Array<{ id: string; request: WidgetDataRequest }>, {\n getRequiredFeatures: (entityType) => analyticsRegistry.getRequiredFeatures(entityType),\n checkFeatures: (features) => {\n if (features.length === 0) return Promise.resolve(true)\n return rbacService.userHasAllFeatures(auth.sub, features, {\n tenantId,\n organizationId: auth.orgId,\n })\n },\n fetchOne: (request) => service.fetchWidgetData(request),\n describeError: (error) =>\n error instanceof WidgetDataValidationError\n ? error.message\n : 'An error occurred while processing your request',\n })\n return NextResponse.json({ results })\n } catch (err) {\n console.error('[widgets/data/batch] Error:', err)\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataBatchPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for multiple dashboard widgets in one request',\n description:\n 'Resolves a batch of widget data requests with a single authentication, RBAC, organization-scope, and database-context setup. Each request is keyed by an opaque widget id and resolved independently, so a failure in one widget does not fail the batch.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataBatchRequestSchema,\n description: 'A list of id-keyed widget data requests to resolve together.',\n },\n responses: [\n {\n status: 200,\n description: 'Per-widget aggregation results keyed by request id.',\n schema: widgetDataBatchResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Batch widget data aggregation endpoint',\n methods: {\n POST: widgetDataBatchPostDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,5 +1,4 @@
1
1
  import { NextResponse } from "next/server";
2
- import { z } from "zod";
3
2
  import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
4
  import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
@@ -8,83 +7,10 @@ import {
8
7
  WidgetDataValidationError
9
8
  } from "../../../services/widgetDataService.js";
10
9
  import { dashboardsTag, dashboardsErrorSchema } from "../../openapi.js";
10
+ import { widgetDataRequestSchema, widgetDataResponseSchema } from "./schema.js";
11
11
  const metadata = {
12
12
  POST: { requireAuth: true, requireFeatures: ["analytics.view"] }
13
13
  };
14
- const aggregateFunctionSchema = z.enum(["count", "sum", "avg", "min", "max"]);
15
- const dateGranularitySchema = z.enum(["day", "week", "month", "quarter", "year"]);
16
- const dateRangePresetSchema = z.enum([
17
- "today",
18
- "yesterday",
19
- "this_week",
20
- "last_week",
21
- "this_month",
22
- "last_month",
23
- "this_quarter",
24
- "last_quarter",
25
- "this_year",
26
- "last_year",
27
- "last_7_days",
28
- "last_30_days",
29
- "last_90_days"
30
- ]);
31
- const filterOperatorSchema = z.enum([
32
- "eq",
33
- "neq",
34
- "gt",
35
- "gte",
36
- "lt",
37
- "lte",
38
- "in",
39
- "not_in",
40
- "is_null",
41
- "is_not_null"
42
- ]);
43
- const widgetDataRequestSchema = z.object({
44
- entityType: z.string().min(1),
45
- metric: z.object({
46
- field: z.string().min(1),
47
- aggregate: aggregateFunctionSchema
48
- }),
49
- groupBy: z.object({
50
- field: z.string().min(1),
51
- granularity: dateGranularitySchema.optional(),
52
- limit: z.number().int().min(1).max(100).optional(),
53
- resolveLabels: z.boolean().optional()
54
- }).optional(),
55
- filters: z.array(
56
- z.object({
57
- field: z.string().min(1),
58
- operator: filterOperatorSchema,
59
- value: z.unknown().optional()
60
- })
61
- ).optional(),
62
- dateRange: z.object({
63
- field: z.string().min(1),
64
- preset: dateRangePresetSchema
65
- }).optional(),
66
- comparison: z.object({
67
- type: z.enum(["previous_period", "previous_year"])
68
- }).optional()
69
- });
70
- const widgetDataItemSchema = z.object({
71
- groupKey: z.unknown(),
72
- groupLabel: z.string().optional(),
73
- value: z.number().nullable()
74
- });
75
- const widgetDataResponseSchema = z.object({
76
- value: z.number().nullable(),
77
- data: z.array(widgetDataItemSchema),
78
- comparison: z.object({
79
- value: z.number().nullable(),
80
- change: z.number(),
81
- direction: z.enum(["up", "down", "unchanged"])
82
- }).optional(),
83
- metadata: z.object({
84
- fetchedAt: z.string(),
85
- recordCount: z.number()
86
- })
87
- });
88
14
  async function POST(req) {
89
15
  const auth = await getAuthFromRequest(req);
90
16
  if (!auth) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/dashboards/api/widgets/data/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n} from '../../../services/widgetDataService'\nimport type { AnalyticsRegistry } from '../../../services/analyticsRegistry'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../openapi'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nconst aggregateFunctionSchema = z.enum(['count', 'sum', 'avg', 'min', 'max'])\nconst dateGranularitySchema = z.enum(['day', 'week', 'month', 'quarter', 'year'])\nconst dateRangePresetSchema = z.enum([\n 'today',\n 'yesterday',\n 'this_week',\n 'last_week',\n 'this_month',\n 'last_month',\n 'this_quarter',\n 'last_quarter',\n 'this_year',\n 'last_year',\n 'last_7_days',\n 'last_30_days',\n 'last_90_days',\n])\n\nconst filterOperatorSchema = z.enum([\n 'eq',\n 'neq',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'in',\n 'not_in',\n 'is_null',\n 'is_not_null',\n])\n\nconst widgetDataRequestSchema = z.object({\n entityType: z.string().min(1),\n metric: z.object({\n field: z.string().min(1),\n aggregate: aggregateFunctionSchema,\n }),\n groupBy: z\n .object({\n field: z.string().min(1),\n granularity: dateGranularitySchema.optional(),\n limit: z.number().int().min(1).max(100).optional(),\n resolveLabels: z.boolean().optional(),\n })\n .optional(),\n filters: z\n .array(\n z.object({\n field: z.string().min(1),\n operator: filterOperatorSchema,\n value: z.unknown().optional(),\n }),\n )\n .optional(),\n dateRange: z\n .object({\n field: z.string().min(1),\n preset: dateRangePresetSchema,\n })\n .optional(),\n comparison: z\n .object({\n type: z.enum(['previous_period', 'previous_year']),\n })\n .optional(),\n})\n\nconst widgetDataItemSchema = z.object({\n groupKey: z.unknown(),\n groupLabel: z.string().optional(),\n value: z.number().nullable(),\n})\n\nconst widgetDataResponseSchema = z.object({\n value: z.number().nullable(),\n data: z.array(widgetDataItemSchema),\n comparison: z\n .object({\n value: z.number().nullable(),\n change: z.number(),\n direction: z.enum(['up', 'down', 'unchanged']),\n })\n .optional(),\n metadata: z.object({\n fetchedAt: z.string(),\n recordCount: z.number(),\n }),\n})\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const entityFeatures = analyticsRegistry.getRequiredFeatures(parsed.data.entityType)\n if (entityFeatures && entityFeatures.length > 0) {\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, entityFeatures, {\n tenantId: auth.tenantId!,\n organizationId: auth.orgId,\n })\n if (!hasAccess) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n try {\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n const result = await service.fetchWidgetData(parsed.data as WidgetDataRequest)\n return NextResponse.json(result)\n } catch (err) {\n console.error('[widgets/data] Error:', err)\n if (err instanceof WidgetDataValidationError) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n }\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for dashboard widgets',\n description:\n 'Executes an aggregation query against the specified entity type and returns the result. Supports date range filtering, grouping, and period-over-period comparison.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataRequestSchema,\n description: 'Widget data request configuration specifying entity type, metric, filters, and grouping.',\n },\n responses: [\n {\n status: 200,\n description: 'Aggregated data for the widget.',\n schema: widgetDataResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing analytics.view feature', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Widget data aggregation endpoint',\n methods: {\n POST: widgetDataPostDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CacheStrategy } from '@open-mercato/cache'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport {\n createWidgetDataService,\n type WidgetDataRequest,\n WidgetDataValidationError,\n} from '../../../services/widgetDataService'\nimport type { AnalyticsRegistry } from '../../../services/analyticsRegistry'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { dashboardsTag, dashboardsErrorSchema } from '../../openapi'\nimport { widgetDataRequestSchema, widgetDataResponseSchema } from './schema'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['analytics.view'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n\n const parsed = widgetDataRequestSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'Invalid request payload', issues: parsed.error.issues },\n { status: 400 },\n )\n }\n\n const container = await createRequestContainer()\n const analyticsRegistry = container.resolve<AnalyticsRegistry>('analyticsRegistry')\n\n const entityFeatures = analyticsRegistry.getRequiredFeatures(parsed.data.entityType)\n if (entityFeatures && entityFeatures.length > 0) {\n const rbacService = container.resolve<{\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string; organizationId?: string | null },\n ) => Promise<boolean>\n }>('rbacService')\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, entityFeatures, {\n tenantId: auth.tenantId!,\n organizationId: auth.orgId,\n })\n if (!hasAccess) {\n return NextResponse.json({ error: 'Forbidden' }, { status: 403 })\n }\n }\n\n const em = (container.resolve('em') as EntityManager).fork({\n clear: true,\n freshEventManager: true,\n useContext: true,\n })\n\n const tenantId = auth.tenantId ?? null\n if (!tenantId) {\n return NextResponse.json({ error: 'Tenant context is required' }, { status: 400 })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n\n const organizationIds = (() => {\n if (scope?.selectedId) return [scope.selectedId]\n if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds\n if (scope?.allowedIds === null) return undefined\n if (auth.orgId) return [auth.orgId]\n return undefined\n })()\n\n try {\n const cache = container.resolve<CacheStrategy>('cache')\n const service = createWidgetDataService(em, { tenantId, organizationIds }, analyticsRegistry, cache)\n const result = await service.fetchWidgetData(parsed.data as WidgetDataRequest)\n return NextResponse.json(result)\n } catch (err) {\n console.error('[widgets/data] Error:', err)\n if (err instanceof WidgetDataValidationError) {\n return NextResponse.json({ error: err.message }, { status: 400 })\n }\n return NextResponse.json(\n { error: 'An error occurred while processing your request' },\n { status: 500 },\n )\n }\n}\n\nconst widgetDataPostDoc: OpenApiMethodDoc = {\n summary: 'Fetch aggregated data for dashboard widgets',\n description:\n 'Executes an aggregation query against the specified entity type and returns the result. Supports date range filtering, grouping, and period-over-period comparison.',\n tags: [dashboardsTag],\n requestBody: {\n contentType: 'application/json',\n schema: widgetDataRequestSchema,\n description: 'Widget data request configuration specifying entity type, metric, filters, and grouping.',\n },\n responses: [\n {\n status: 200,\n description: 'Aggregated data for the widget.',\n schema: widgetDataResponseSchema,\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request payload', schema: dashboardsErrorSchema },\n { status: 401, description: 'Authentication required', schema: dashboardsErrorSchema },\n { status: 403, description: 'Missing analytics.view feature', schema: dashboardsErrorSchema },\n { status: 500, description: 'Internal server error', schema: dashboardsErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: dashboardsTag,\n summary: 'Widget data aggregation endpoint',\n methods: {\n POST: widgetDataPostDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ const aggregateFunctionSchema = z.enum(["count", "sum", "avg", "min", "max"]);
3
+ const dateGranularitySchema = z.enum(["day", "week", "month", "quarter", "year"]);
4
+ const dateRangePresetSchema = z.enum([
5
+ "today",
6
+ "yesterday",
7
+ "this_week",
8
+ "last_week",
9
+ "this_month",
10
+ "last_month",
11
+ "this_quarter",
12
+ "last_quarter",
13
+ "this_year",
14
+ "last_year",
15
+ "last_7_days",
16
+ "last_30_days",
17
+ "last_90_days"
18
+ ]);
19
+ const filterOperatorSchema = z.enum([
20
+ "eq",
21
+ "neq",
22
+ "gt",
23
+ "gte",
24
+ "lt",
25
+ "lte",
26
+ "in",
27
+ "not_in",
28
+ "is_null",
29
+ "is_not_null"
30
+ ]);
31
+ const widgetDataRequestSchema = z.object({
32
+ entityType: z.string().min(1),
33
+ metric: z.object({
34
+ field: z.string().min(1),
35
+ aggregate: aggregateFunctionSchema
36
+ }),
37
+ groupBy: z.object({
38
+ field: z.string().min(1),
39
+ granularity: dateGranularitySchema.optional(),
40
+ limit: z.number().int().min(1).max(100).optional(),
41
+ resolveLabels: z.boolean().optional()
42
+ }).optional(),
43
+ filters: z.array(
44
+ z.object({
45
+ field: z.string().min(1),
46
+ operator: filterOperatorSchema,
47
+ value: z.unknown().optional()
48
+ })
49
+ ).optional(),
50
+ dateRange: z.object({
51
+ field: z.string().min(1),
52
+ preset: dateRangePresetSchema
53
+ }).optional(),
54
+ comparison: z.object({
55
+ type: z.enum(["previous_period", "previous_year"])
56
+ }).optional()
57
+ });
58
+ const widgetDataItemSchema = z.object({
59
+ groupKey: z.unknown(),
60
+ groupLabel: z.string().optional(),
61
+ value: z.number().nullable()
62
+ });
63
+ const widgetDataResponseSchema = z.object({
64
+ value: z.number().nullable(),
65
+ data: z.array(widgetDataItemSchema),
66
+ comparison: z.object({
67
+ value: z.number().nullable(),
68
+ change: z.number(),
69
+ direction: z.enum(["up", "down", "unchanged"])
70
+ }).optional(),
71
+ metadata: z.object({
72
+ fetchedAt: z.string(),
73
+ recordCount: z.number()
74
+ })
75
+ });
76
+ export {
77
+ aggregateFunctionSchema,
78
+ dateGranularitySchema,
79
+ dateRangePresetSchema,
80
+ filterOperatorSchema,
81
+ widgetDataItemSchema,
82
+ widgetDataRequestSchema,
83
+ widgetDataResponseSchema
84
+ };
85
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/dashboards/api/widgets/data/schema.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\n\nexport const aggregateFunctionSchema = z.enum(['count', 'sum', 'avg', 'min', 'max'])\nexport const dateGranularitySchema = z.enum(['day', 'week', 'month', 'quarter', 'year'])\nexport const dateRangePresetSchema = z.enum([\n 'today',\n 'yesterday',\n 'this_week',\n 'last_week',\n 'this_month',\n 'last_month',\n 'this_quarter',\n 'last_quarter',\n 'this_year',\n 'last_year',\n 'last_7_days',\n 'last_30_days',\n 'last_90_days',\n])\n\nexport const filterOperatorSchema = z.enum([\n 'eq',\n 'neq',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'in',\n 'not_in',\n 'is_null',\n 'is_not_null',\n])\n\nexport const widgetDataRequestSchema = z.object({\n entityType: z.string().min(1),\n metric: z.object({\n field: z.string().min(1),\n aggregate: aggregateFunctionSchema,\n }),\n groupBy: z\n .object({\n field: z.string().min(1),\n granularity: dateGranularitySchema.optional(),\n limit: z.number().int().min(1).max(100).optional(),\n resolveLabels: z.boolean().optional(),\n })\n .optional(),\n filters: z\n .array(\n z.object({\n field: z.string().min(1),\n operator: filterOperatorSchema,\n value: z.unknown().optional(),\n }),\n )\n .optional(),\n dateRange: z\n .object({\n field: z.string().min(1),\n preset: dateRangePresetSchema,\n })\n .optional(),\n comparison: z\n .object({\n type: z.enum(['previous_period', 'previous_year']),\n })\n .optional(),\n})\n\nexport const widgetDataItemSchema = z.object({\n groupKey: z.unknown(),\n groupLabel: z.string().optional(),\n value: z.number().nullable(),\n})\n\nexport const widgetDataResponseSchema = z.object({\n value: z.number().nullable(),\n data: z.array(widgetDataItemSchema),\n comparison: z\n .object({\n value: z.number().nullable(),\n change: z.number(),\n direction: z.enum(['up', 'down', 'unchanged']),\n })\n .optional(),\n metadata: z.object({\n fetchedAt: z.string(),\n recordCount: z.number(),\n }),\n})\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAEX,MAAM,0BAA0B,EAAE,KAAK,CAAC,SAAS,OAAO,OAAO,OAAO,KAAK,CAAC;AAC5E,MAAM,wBAAwB,EAAE,KAAK,CAAC,OAAO,QAAQ,SAAS,WAAW,MAAM,CAAC;AAChF,MAAM,wBAAwB,EAAE,KAAK;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,MAAM,uBAAuB,EAAE,KAAK;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,QAAQ,EAAE,OAAO;AAAA,IACf,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,WAAW;AAAA,EACb,CAAC;AAAA,EACD,SAAS,EACN,OAAO;AAAA,IACN,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,aAAa,sBAAsB,SAAS;AAAA,IAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AAAA,IACjD,eAAe,EAAE,QAAQ,EAAE,SAAS;AAAA,EACtC,CAAC,EACA,SAAS;AAAA,EACZ,SAAS,EACN;AAAA,IACC,EAAE,OAAO;AAAA,MACP,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACvB,UAAU;AAAA,MACV,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC9B,CAAC;AAAA,EACH,EACC,SAAS;AAAA,EACZ,WAAW,EACR,OAAO;AAAA,IACN,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,QAAQ;AAAA,EACV,CAAC,EACA,SAAS;AAAA,EACZ,YAAY,EACT,OAAO;AAAA,IACN,MAAM,EAAE,KAAK,CAAC,mBAAmB,eAAe,CAAC;AAAA,EACnD,CAAC,EACA,SAAS;AACd,CAAC;AAEM,MAAM,uBAAuB,EAAE,OAAO;AAAA,EAC3C,UAAU,EAAE,QAAQ;AAAA,EACpB,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,OAAO,EAAE,OAAO,EAAE,SAAS;AAC7B,CAAC;AAEM,MAAM,2BAA2B,EAAE,OAAO;AAAA,EAC/C,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,MAAM,EAAE,MAAM,oBAAoB;AAAA,EAClC,YAAY,EACT,OAAO;AAAA,IACN,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,IAC3B,QAAQ,EAAE,OAAO;AAAA,IACjB,WAAW,EAAE,KAAK,CAAC,MAAM,QAAQ,WAAW,CAAC;AAAA,EAC/C,CAAC,EACA,SAAS;AAAA,EACZ,UAAU,EAAE,OAAO;AAAA,IACjB,WAAW,EAAE,OAAO;AAAA,IACpB,aAAa,EAAE,OAAO;AAAA,EACxB,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,49 @@
1
+ async function resolveEntityFeatureAccess(entityTypes, getRequiredFeatures, checkFeatures) {
2
+ const access = /* @__PURE__ */ new Map();
3
+ const featuresByEntity = /* @__PURE__ */ new Map();
4
+ const unionFeatures = /* @__PURE__ */ new Set();
5
+ for (const entityType of new Set(entityTypes)) {
6
+ const features = getRequiredFeatures(entityType) ?? [];
7
+ featuresByEntity.set(entityType, features);
8
+ if (features.length === 0) {
9
+ access.set(entityType, true);
10
+ } else {
11
+ for (const feature of features) unionFeatures.add(feature);
12
+ }
13
+ }
14
+ const gated = [...featuresByEntity.entries()].filter(([, features]) => features.length > 0);
15
+ if (gated.length === 0) return access;
16
+ if (await checkFeatures([...unionFeatures])) {
17
+ for (const [entityType] of gated) access.set(entityType, true);
18
+ return access;
19
+ }
20
+ for (const [entityType, features] of gated) {
21
+ access.set(entityType, await checkFeatures(features));
22
+ }
23
+ return access;
24
+ }
25
+ async function runWidgetDataBatch(entries, deps) {
26
+ const access = await resolveEntityFeatureAccess(
27
+ entries.map((entry) => entry.request.entityType),
28
+ deps.getRequiredFeatures,
29
+ deps.checkFeatures
30
+ );
31
+ return Promise.all(
32
+ entries.map(async (entry) => {
33
+ if (access.get(entry.request.entityType) === false) {
34
+ return { id: entry.id, ok: false, error: "Forbidden" };
35
+ }
36
+ try {
37
+ const data = await deps.fetchOne(entry.request);
38
+ return { id: entry.id, ok: true, data };
39
+ } catch (error) {
40
+ return { id: entry.id, ok: false, error: deps.describeError(error) };
41
+ }
42
+ })
43
+ );
44
+ }
45
+ export {
46
+ resolveEntityFeatureAccess,
47
+ runWidgetDataBatch
48
+ };
49
+ //# sourceMappingURL=widgetDataBatch.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/dashboards/lib/widgetDataBatch.ts"],
4
+ "sourcesContent": ["import type { WidgetDataRequest, WidgetDataResponse } from '../services/widgetDataService'\n\nexport type WidgetDataBatchEntry = {\n id: string\n request: WidgetDataRequest\n}\n\nexport type WidgetDataBatchResult =\n | { id: string; ok: true; data: WidgetDataResponse }\n | { id: string; ok: false; error: string }\n\nexport type WidgetDataBatchDeps = {\n getRequiredFeatures: (entityType: string) => string[] | null\n checkFeatures: (features: string[]) => Promise<boolean>\n fetchOne: (request: WidgetDataRequest) => Promise<WidgetDataResponse>\n describeError: (error: unknown) => string\n}\n\n/**\n * Resolves per-entity-type feature access for a batch of widget requests while\n * collapsing the common case to a single RBAC resolution. The happy path checks\n * the union of all required features once; only when the union check fails do we\n * fall back to per-entity-type checks so a single privileged entity type does\n * not reject widgets the caller is allowed to see.\n */\nexport async function resolveEntityFeatureAccess(\n entityTypes: string[],\n getRequiredFeatures: (entityType: string) => string[] | null,\n checkFeatures: (features: string[]) => Promise<boolean>,\n): Promise<Map<string, boolean>> {\n const access = new Map<string, boolean>()\n const featuresByEntity = new Map<string, string[]>()\n const unionFeatures = new Set<string>()\n\n for (const entityType of new Set(entityTypes)) {\n const features = getRequiredFeatures(entityType) ?? []\n featuresByEntity.set(entityType, features)\n if (features.length === 0) {\n access.set(entityType, true)\n } else {\n for (const feature of features) unionFeatures.add(feature)\n }\n }\n\n const gated = [...featuresByEntity.entries()].filter(([, features]) => features.length > 0)\n if (gated.length === 0) return access\n\n if (await checkFeatures([...unionFeatures])) {\n for (const [entityType] of gated) access.set(entityType, true)\n return access\n }\n\n for (const [entityType, features] of gated) {\n access.set(entityType, await checkFeatures(features))\n }\n return access\n}\n\n/**\n * Runs a batch of widget-data requests against shared request-scoped\n * dependencies (a single container, RBAC resolution, org-scope, and EM fork).\n * Feature access is resolved once up front; each request is then executed\n * concurrently with per-widget error isolation so one bad request never fails\n * the whole batch.\n */\nexport async function runWidgetDataBatch(\n entries: WidgetDataBatchEntry[],\n deps: WidgetDataBatchDeps,\n): Promise<WidgetDataBatchResult[]> {\n const access = await resolveEntityFeatureAccess(\n entries.map((entry) => entry.request.entityType),\n deps.getRequiredFeatures,\n deps.checkFeatures,\n )\n\n return Promise.all(\n entries.map(async (entry): Promise<WidgetDataBatchResult> => {\n if (access.get(entry.request.entityType) === false) {\n return { id: entry.id, ok: false, error: 'Forbidden' }\n }\n try {\n const data = await deps.fetchOne(entry.request)\n return { id: entry.id, ok: true, data }\n } catch (error) {\n return { id: entry.id, ok: false, error: deps.describeError(error) }\n }\n }),\n )\n}\n"],
5
+ "mappings": "AAyBA,eAAsB,2BACpB,aACA,qBACA,eAC+B;AAC/B,QAAM,SAAS,oBAAI,IAAqB;AACxC,QAAM,mBAAmB,oBAAI,IAAsB;AACnD,QAAM,gBAAgB,oBAAI,IAAY;AAEtC,aAAW,cAAc,IAAI,IAAI,WAAW,GAAG;AAC7C,UAAM,WAAW,oBAAoB,UAAU,KAAK,CAAC;AACrD,qBAAiB,IAAI,YAAY,QAAQ;AACzC,QAAI,SAAS,WAAW,GAAG;AACzB,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B,OAAO;AACL,iBAAW,WAAW,SAAU,eAAc,IAAI,OAAO;AAAA,IAC3D;AAAA,EACF;AAEA,QAAM,QAAQ,CAAC,GAAG,iBAAiB,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,QAAQ,MAAM,SAAS,SAAS,CAAC;AAC1F,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,MAAI,MAAM,cAAc,CAAC,GAAG,aAAa,CAAC,GAAG;AAC3C,eAAW,CAAC,UAAU,KAAK,MAAO,QAAO,IAAI,YAAY,IAAI;AAC7D,WAAO;AAAA,EACT;AAEA,aAAW,CAAC,YAAY,QAAQ,KAAK,OAAO;AAC1C,WAAO,IAAI,YAAY,MAAM,cAAc,QAAQ,CAAC;AAAA,EACtD;AACA,SAAO;AACT;AASA,eAAsB,mBACpB,SACA,MACkC;AAClC,QAAM,SAAS,MAAM;AAAA,IACnB,QAAQ,IAAI,CAAC,UAAU,MAAM,QAAQ,UAAU;AAAA,IAC/C,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AAEA,SAAO,QAAQ;AAAA,IACb,QAAQ,IAAI,OAAO,UAA0C;AAC3D,UAAI,OAAO,IAAI,MAAM,QAAQ,UAAU,MAAM,OAAO;AAClD,eAAO,EAAE,IAAI,MAAM,IAAI,IAAI,OAAO,OAAO,YAAY;AAAA,MACvD;AACA,UAAI;AACF,cAAM,OAAO,MAAM,KAAK,SAAS,MAAM,OAAO;AAC9C,eAAO,EAAE,IAAI,MAAM,IAAI,IAAI,MAAM,KAAK;AAAA,MACxC,SAAS,OAAO;AACd,eAAO,EAAE,IAAI,MAAM,IAAI,IAAI,OAAO,OAAO,KAAK,cAAc,KAAK,EAAE;AAAA,MACrE;AAAA,IACF,CAAC;AAAA,EACH;AACF;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
4
+ import { useWidgetData } from "@open-mercato/ui/backend/dashboard/widgetData";
5
5
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
6
  import { KpiCard } from "@open-mercato/ui/backend/charts";
7
7
  import {
@@ -11,7 +11,7 @@ import {
11
11
  } from "@open-mercato/ui/backend/date-range";
12
12
  import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
13
13
  import { formatCurrencyWithDecimals } from "../../../lib/formatters.js";
14
- async function fetchAovData(settings) {
14
+ async function fetchAovData(settings, fetchWidgetData) {
15
15
  const body = {
16
16
  entityType: "sales:orders",
17
17
  metric: {
@@ -24,16 +24,7 @@ async function fetchAovData(settings) {
24
24
  },
25
25
  comparison: settings.showComparison ? { type: "previous_period" } : void 0
26
26
  };
27
- const call = await apiCall("/api/dashboards/widgets/data", {
28
- method: "POST",
29
- headers: { "Content-Type": "application/json" },
30
- body: JSON.stringify(body)
31
- });
32
- if (!call.ok) {
33
- const errorMsg = call.result?.error;
34
- throw new Error(typeof errorMsg === "string" ? errorMsg : "Failed to fetch AOV data");
35
- }
36
- return call.result;
27
+ return fetchWidgetData(body);
37
28
  }
38
29
  const AovKpiWidget = ({
39
30
  mode,
@@ -48,12 +39,13 @@ const AovKpiWidget = ({
48
39
  const [trend, setTrend] = React.useState(void 0);
49
40
  const [loading, setLoading] = React.useState(true);
50
41
  const [error, setError] = React.useState(null);
42
+ const fetchWidgetData = useWidgetData();
51
43
  const refresh = React.useCallback(async () => {
52
44
  onRefreshStateChange?.(true);
53
45
  setLoading(true);
54
46
  setError(null);
55
47
  try {
56
- const data = await fetchAovData(hydrated);
48
+ const data = await fetchAovData(hydrated, fetchWidgetData);
57
49
  setValue(data.value);
58
50
  if (data.comparison) {
59
51
  setTrend({
@@ -70,7 +62,7 @@ const AovKpiWidget = ({
70
62
  setLoading(false);
71
63
  onRefreshStateChange?.(false);
72
64
  }
73
- }, [hydrated, onRefreshStateChange, t]);
65
+ }, [hydrated, fetchWidgetData, onRefreshStateChange, t]);
74
66
  React.useEffect(() => {
75
67
  refresh().catch(() => {
76
68
  });
@@ -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 { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n getComparisonLabelKey,\n} from '@open-mercato/ui/backend/date-range'\nimport { DEFAULT_SETTINGS, hydrateSettings, type AovKpiSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyWithDecimals } from '../../../lib/formatters'\n\nasync function fetchAovData(settings: AovKpiSettings): Promise<WidgetDataResponse> {\n const body = {\n entityType: 'sales:orders',\n metric: {\n field: 'grandTotalGrossAmount',\n aggregate: 'avg',\n },\n dateRange: {\n field: 'placedAt',\n preset: settings.dateRange,\n },\n comparison: settings.showComparison ? { type: 'previous_period' } : undefined,\n }\n\n const call = await apiCall<WidgetDataResponse>('/api/dashboards/widgets/data', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n\n if (!call.ok) {\n const errorMsg = (call.result as Record<string, unknown>)?.error\n throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Failed to fetch AOV data')\n }\n\n return call.result as WidgetDataResponse\n}\n\nconst AovKpiWidget: React.FC<DashboardWidgetComponentProps<AovKpiSettings>> = ({\n mode,\n settings = DEFAULT_SETTINGS,\n onSettingsChange,\n refreshToken,\n onRefreshStateChange,\n}) => {\n const t = useT()\n const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])\n const [value, setValue] = React.useState<number | null>(null)\n const [trend, setTrend] = React.useState<KpiTrend | undefined>(undefined)\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n const refresh = React.useCallback(async () => {\n onRefreshStateChange?.(true)\n setLoading(true)\n setError(null)\n try {\n const data = await fetchAovData(hydrated)\n setValue(data.value)\n if (data.comparison) {\n setTrend({\n value: data.comparison.change,\n direction: data.comparison.direction,\n })\n } else {\n setTrend(undefined)\n }\n } catch (err) {\n console.error('Failed to load AOV KPI data', err)\n setError(t('dashboards.analytics.widgets.aovKpi.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, onRefreshStateChange, t])\n\n React.useEffect(() => {\n refresh().catch(() => {})\n }, [refresh, refreshToken])\n\n if (mode === 'settings') {\n return (\n <div className=\"space-y-4 text-sm\">\n <DateRangeSelect\n id=\"aov-kpi-date-range\"\n label={t('dashboards.analytics.settings.dateRange', 'Date Range')}\n value={hydrated.dateRange}\n onChange={(dateRange: DateRangePreset) => onSettingsChange({ ...hydrated, dateRange })}\n />\n <div className=\"space-y-1.5\">\n <label className=\"flex items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={hydrated.showComparison}\n onChange={(e) => onSettingsChange({ ...hydrated, showComparison: e.target.checked })}\n className=\"h-4 w-4 rounded border focus-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 { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts'\nimport {\n DateRangeSelect,\n InlineDateRangeSelect,\n type DateRangePreset,\n getComparisonLabelKey,\n} from '@open-mercato/ui/backend/date-range'\nimport { DEFAULT_SETTINGS, hydrateSettings, type AovKpiSettings } from './config'\nimport type { WidgetDataResponse } from '../../../services/widgetDataService'\nimport { formatCurrencyWithDecimals } from '../../../lib/formatters'\n\nasync function fetchAovData(settings: AovKpiSettings, 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 console.error('Failed to load AOV KPI data', err)\n setError(t('dashboards.analytics.widgets.aovKpi.error', 'Failed to load data'))\n } finally {\n setLoading(false)\n onRefreshStateChange?.(false)\n }\n }, [hydrated, 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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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
4
+ import { useWidgetData } from "@open-mercato/ui/backend/dashboard/widgetData";
5
5
  import { useT } from "@open-mercato/shared/lib/i18n/context";
6
6
  import { KpiCard } from "@open-mercato/ui/backend/charts";
7
7
  import {
@@ -10,7 +10,7 @@ import {
10
10
  getComparisonLabelKey
11
11
  } from "@open-mercato/ui/backend/date-range";
12
12
  import { DEFAULT_SETTINGS, hydrateSettings } from "./config.js";
13
- async function fetchNewCustomersData(settings) {
13
+ async function fetchNewCustomersData(settings, fetchWidgetData) {
14
14
  const body = {
15
15
  entityType: "customers:entities",
16
16
  metric: {
@@ -23,16 +23,7 @@ async function fetchNewCustomersData(settings) {
23
23
  },
24
24
  comparison: settings.showComparison ? { type: "previous_period" } : void 0
25
25
  };
26
- const call = await apiCall("/api/dashboards/widgets/data", {
27
- method: "POST",
28
- headers: { "Content-Type": "application/json" },
29
- body: JSON.stringify(body)
30
- });
31
- if (!call.ok) {
32
- const errorMsg = call.result?.error;
33
- throw new Error(typeof errorMsg === "string" ? errorMsg : "Failed to fetch new customers data");
34
- }
35
- return call.result;
26
+ return fetchWidgetData(body);
36
27
  }
37
28
  const NewCustomersKpiWidget = ({
38
29
  mode,
@@ -47,12 +38,13 @@ const NewCustomersKpiWidget = ({
47
38
  const [trend, setTrend] = React.useState(void 0);
48
39
  const [loading, setLoading] = React.useState(true);
49
40
  const [error, setError] = React.useState(null);
41
+ const fetchWidgetData = useWidgetData();
50
42
  const refresh = React.useCallback(async () => {
51
43
  onRefreshStateChange?.(true);
52
44
  setLoading(true);
53
45
  setError(null);
54
46
  try {
55
- const data = await fetchNewCustomersData(hydrated);
47
+ const data = await fetchNewCustomersData(hydrated, fetchWidgetData);
56
48
  setValue(data.value);
57
49
  if (data.comparison) {
58
50
  setTrend({
@@ -69,7 +61,7 @@ const NewCustomersKpiWidget = ({
69
61
  setLoading(false);
70
62
  onRefreshStateChange?.(false);
71
63
  }
72
- }, [hydrated, onRefreshStateChange, t]);
64
+ }, [hydrated, fetchWidgetData, onRefreshStateChange, t]);
73
65
  React.useEffect(() => {
74
66
  refresh().catch(() => {
75
67
  });