@open-mercato/core 0.4.6-develop-f7d3079656 → 0.4.6-develop-0861f05ea9

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 (107) hide show
  1. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +17 -154
  2. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +3 -3
  3. package/dist/modules/currencies/backend/exchange-rates/create/page.js +14 -152
  4. package/dist/modules/currencies/backend/exchange-rates/create/page.js.map +2 -2
  5. package/dist/modules/currencies/lib/exchangeRateFormConfig.js +167 -0
  6. package/dist/modules/currencies/lib/exchangeRateFormConfig.js.map +7 -0
  7. package/dist/modules/customers/api/dashboard/widgets/utils.js +1 -34
  8. package/dist/modules/customers/api/dashboard/widgets/utils.js.map +2 -2
  9. package/dist/modules/customers/commands/activities.js +3 -8
  10. package/dist/modules/customers/commands/activities.js.map +2 -2
  11. package/dist/modules/customers/commands/comments.js +2 -8
  12. package/dist/modules/customers/commands/comments.js.map +2 -2
  13. package/dist/modules/dashboards/lib/widgetScope.js +38 -0
  14. package/dist/modules/dashboards/lib/widgetScope.js.map +7 -0
  15. package/dist/modules/entities/lib/makeActivityRoute.js +265 -0
  16. package/dist/modules/entities/lib/makeActivityRoute.js.map +7 -0
  17. package/dist/modules/resources/api/activities.js +24 -232
  18. package/dist/modules/resources/api/activities.js.map +2 -2
  19. package/dist/modules/resources/commands/activities.js +3 -8
  20. package/dist/modules/resources/commands/activities.js.map +2 -2
  21. package/dist/modules/resources/commands/comments.js +2 -8
  22. package/dist/modules/resources/commands/comments.js.map +2 -2
  23. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js +27 -182
  24. package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js.map +2 -2
  25. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js +28 -183
  26. package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js.map +2 -2
  27. package/dist/modules/sales/api/order-line-statuses/route.js +15 -194
  28. package/dist/modules/sales/api/order-line-statuses/route.js.map +2 -2
  29. package/dist/modules/sales/api/order-lines/route.js +15 -281
  30. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  31. package/dist/modules/sales/api/order-statuses/route.js +15 -194
  32. package/dist/modules/sales/api/order-statuses/route.js.map +2 -2
  33. package/dist/modules/sales/api/payment-statuses/route.js +15 -194
  34. package/dist/modules/sales/api/payment-statuses/route.js.map +2 -2
  35. package/dist/modules/sales/api/quote-lines/route.js +15 -279
  36. package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
  37. package/dist/modules/sales/api/shipment-statuses/route.js +15 -194
  38. package/dist/modules/sales/api/shipment-statuses/route.js.map +2 -2
  39. package/dist/modules/sales/components/PaymentMethodsSettings.js +3 -84
  40. package/dist/modules/sales/components/PaymentMethodsSettings.js.map +2 -2
  41. package/dist/modules/sales/components/ProviderFieldInput.js +86 -0
  42. package/dist/modules/sales/components/ProviderFieldInput.js.map +7 -0
  43. package/dist/modules/sales/components/ShippingMethodsSettings.js +3 -82
  44. package/dist/modules/sales/components/ShippingMethodsSettings.js.map +2 -2
  45. package/dist/modules/sales/lib/makeSalesLineRoute.js +308 -0
  46. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +7 -0
  47. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js +206 -0
  48. package/dist/modules/sales/lib/makeStatusDictionaryRoute.js.map +7 -0
  49. package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js +178 -0
  50. package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js.map +7 -0
  51. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +1 -39
  52. package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +2 -2
  53. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +1 -39
  54. package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +2 -2
  55. package/dist/modules/sales/widgets/dashboard/shared.js +46 -0
  56. package/dist/modules/sales/widgets/dashboard/shared.js.map +7 -0
  57. package/dist/modules/staff/api/activities.js +24 -232
  58. package/dist/modules/staff/api/activities.js.map +2 -2
  59. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +14 -34
  60. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  61. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +15 -34
  62. package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
  63. package/dist/modules/staff/commands/activities.js +3 -8
  64. package/dist/modules/staff/commands/activities.js.map +2 -2
  65. package/dist/modules/staff/commands/comments.js +2 -8
  66. package/dist/modules/staff/commands/comments.js.map +2 -2
  67. package/dist/modules/staff/lib/leaveRequestHelpers.js +41 -0
  68. package/dist/modules/staff/lib/leaveRequestHelpers.js.map +7 -0
  69. package/package.json +2 -2
  70. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +20 -180
  71. package/src/modules/currencies/backend/exchange-rates/create/page.tsx +16 -175
  72. package/src/modules/currencies/lib/exchangeRateFormConfig.ts +200 -0
  73. package/src/modules/customers/api/dashboard/widgets/utils.ts +1 -53
  74. package/src/modules/customers/commands/activities.ts +2 -8
  75. package/src/modules/customers/commands/comments.ts +2 -8
  76. package/src/modules/dashboards/i18n/de.json +3 -0
  77. package/src/modules/dashboards/i18n/en.json +3 -0
  78. package/src/modules/dashboards/i18n/es.json +3 -0
  79. package/src/modules/dashboards/i18n/pl.json +3 -0
  80. package/src/modules/dashboards/lib/widgetScope.ts +53 -0
  81. package/src/modules/entities/lib/makeActivityRoute.ts +327 -0
  82. package/src/modules/resources/api/activities.ts +25 -269
  83. package/src/modules/resources/commands/activities.ts +2 -7
  84. package/src/modules/resources/commands/comments.ts +2 -8
  85. package/src/modules/sales/api/dashboard/widgets/new-orders/route.ts +29 -244
  86. package/src/modules/sales/api/dashboard/widgets/new-quotes/route.ts +30 -245
  87. package/src/modules/sales/api/order-line-statuses/route.ts +16 -209
  88. package/src/modules/sales/api/order-lines/route.ts +16 -300
  89. package/src/modules/sales/api/order-statuses/route.ts +16 -209
  90. package/src/modules/sales/api/payment-statuses/route.ts +16 -209
  91. package/src/modules/sales/api/quote-lines/route.ts +16 -298
  92. package/src/modules/sales/api/shipment-statuses/route.ts +16 -209
  93. package/src/modules/sales/components/PaymentMethodsSettings.tsx +3 -88
  94. package/src/modules/sales/components/ProviderFieldInput.tsx +85 -0
  95. package/src/modules/sales/components/ShippingMethodsSettings.tsx +3 -86
  96. package/src/modules/sales/lib/makeSalesLineRoute.ts +345 -0
  97. package/src/modules/sales/lib/makeStatusDictionaryRoute.ts +229 -0
  98. package/src/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.ts +247 -0
  99. package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +7 -50
  100. package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +7 -49
  101. package/src/modules/sales/widgets/dashboard/shared.ts +44 -0
  102. package/src/modules/staff/api/activities.ts +25 -269
  103. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +15 -69
  104. package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +16 -65
  105. package/src/modules/staff/commands/activities.ts +2 -7
  106. package/src/modules/staff/commands/comments.ts +2 -8
  107. package/src/modules/staff/lib/leaveRequestHelpers.ts +78 -0
@@ -1,278 +1,34 @@
1
- import { z } from 'zod'
2
- import type { EntityManager } from '@mikro-orm/postgresql'
3
- import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
4
- import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
5
- import { resolveCrudRecordId, parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
6
- import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
1
+ import { makeActivityRoute } from '@open-mercato/core/modules/entities/lib/makeActivityRoute'
7
2
  import { ResourcesResourceActivity } from '../data/entities'
8
3
  import {
9
4
  resourcesResourceActivityCreateSchema,
10
5
  resourcesResourceActivityUpdateSchema,
11
6
  } from '../data/validators'
12
- import { User } from '@open-mercato/core/modules/auth/data/entities'
13
7
  import { E } from '#generated/entities.ids.generated'
14
- import { createResourcesCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from './openapi'
15
-
16
- const rawBodySchema = z.object({}).passthrough()
17
-
18
- const listSchema = z
19
- .object({
20
- page: z.coerce.number().min(1).default(1),
21
- pageSize: z.coerce.number().min(1).max(100).default(50),
22
- entityId: z.string().uuid().optional(),
23
- sortField: z.string().optional(),
24
- sortDir: z.enum(['asc', 'desc']).optional(),
25
- })
26
- .passthrough()
27
-
28
- const routeMetadata = {
29
- GET: { requireAuth: true, requireFeatures: ['resources.view'] },
30
- POST: { requireAuth: true, requireFeatures: ['resources.manage_resources'] },
31
- PUT: { requireAuth: true, requireFeatures: ['resources.manage_resources'] },
32
- DELETE: { requireAuth: true, requireFeatures: ['resources.manage_resources'] },
33
- }
34
-
35
- export const metadata = routeMetadata
36
-
37
- const crud = makeCrudRoute({
38
- metadata: routeMetadata,
39
- orm: {
40
- entity: ResourcesResourceActivity,
41
- idField: 'id',
42
- orgField: 'organizationId',
43
- tenantField: 'tenantId',
44
- },
45
- indexer: {
46
- entityType: E.resources.resources_resource_activity,
47
- },
48
- list: {
49
- schema: listSchema,
50
- entityId: E.resources.resources_resource_activity,
51
- fields: [
52
- 'id',
53
- 'resource_id',
54
- 'activity_type',
55
- 'subject',
56
- 'body',
57
- 'occurred_at',
58
- 'author_user_id',
59
- 'appearance_icon',
60
- 'appearance_color',
61
- 'organization_id',
62
- 'tenant_id',
63
- 'created_at',
64
- 'updated_at',
65
- ],
66
- decorateCustomFields: {
67
- entityIds: E.resources.resources_resource_activity,
68
- },
69
- sortFieldMap: {
70
- occurredAt: 'occurred_at',
71
- createdAt: 'created_at',
72
- updatedAt: 'updated_at',
73
- },
74
- buildFilters: async (query) => {
75
- const filters: Record<string, unknown> = {}
76
- if (query.entityId) filters.resource_id = { $eq: query.entityId }
77
- return filters
78
- },
79
- transformItem: (item: Record<string, unknown>) => {
80
- const record = (item ?? {}) as Record<string, unknown>
81
- const toIsoString = (value: unknown): string | null => {
82
- if (value == null) return null
83
- if (value instanceof Date) return value.toISOString()
84
- if (typeof value === 'string') {
85
- const trimmed = value.trim()
86
- if (!trimmed.length) return null
87
- const date = new Date(trimmed)
88
- return Number.isNaN(date.getTime()) ? trimmed : date.toISOString()
89
- }
90
- return null
91
- }
92
- const readString = (value: unknown): string | null => (typeof value === 'string' ? value : null)
93
- const idValue = readString(record.id) ?? (record.id != null ? String(record.id) : '')
94
- const resourceId = readString(record['resource_id']) ?? readString(record['resourceId']) ?? null
95
- const activityType =
96
- readString(record['activity_type']) ??
97
- readString(record['activityType']) ??
98
- ''
99
- const subject =
100
- readString(record.subject) ??
101
- (record.subject == null ? null : String(record.subject))
102
- const body =
103
- readString(record.body) ??
104
- (record.body == null ? null : String(record.body))
105
- const authorUserId =
106
- readString(record['author_user_id']) ?? readString(record['authorUserId']) ?? null
107
- const appearanceIconRaw =
108
- readString(record['appearance_icon']) ?? readString(record['appearanceIcon'])
109
- const appearanceColorRaw =
110
- readString(record['appearance_color']) ?? readString(record['appearanceColor'])
111
- const organizationId =
112
- readString(record['organization_id']) ?? readString(record['organizationId'])
113
- const tenantId =
114
- readString(record['tenant_id']) ?? readString(record['tenantId'])
115
- const output: Record<string, unknown> = {
116
- id: idValue,
117
- entityId: resourceId,
118
- resourceId,
119
- activityType,
120
- subject,
121
- body,
122
- occurredAt: toIsoString(record['occurred_at'] ?? record['occurredAt']),
123
- createdAt: toIsoString(record['created_at'] ?? record['createdAt']),
124
- authorUserId,
125
- organizationId,
126
- tenantId,
127
- appearanceIcon: appearanceIconRaw && appearanceIconRaw.trim().length ? appearanceIconRaw : null,
128
- appearanceColor: appearanceColorRaw && appearanceColorRaw.trim().length ? appearanceColorRaw : null,
129
- customFields: Array.isArray(record.customFields) ? record.customFields : undefined,
130
- customValues: record.customValues ?? undefined,
131
- }
132
- for (const [key, value] of Object.entries(record)) {
133
- if (key.startsWith('cf_') || key.startsWith('cf:')) {
134
- output[key] = value
135
- }
136
- }
137
- return output
138
- },
139
- },
140
- actions: {
141
- create: {
142
- commandId: 'resources.resource-activities.create',
143
- schema: rawBodySchema,
144
- mapInput: async ({ raw, ctx }) => {
145
- const { translate } = await resolveTranslations()
146
- return parseScopedCommandInput(resourcesResourceActivityCreateSchema, raw ?? {}, ctx, translate)
147
- },
148
- response: ({ result }) => ({
149
- id: result?.activityId ?? result?.id ?? null,
150
- authorUserId: result?.authorUserId ?? null,
151
- }),
152
- status: 201,
153
- },
154
- update: {
155
- commandId: 'resources.resource-activities.update',
156
- schema: rawBodySchema,
157
- mapInput: async ({ raw, ctx }) => {
158
- const { translate } = await resolveTranslations()
159
- return parseScopedCommandInput(resourcesResourceActivityUpdateSchema, raw ?? {}, ctx, translate)
160
- },
161
- response: () => ({ ok: true }),
162
- },
163
- delete: {
164
- commandId: 'resources.resource-activities.delete',
165
- schema: rawBodySchema,
166
- mapInput: async ({ parsed, ctx }) => {
167
- const { translate } = await resolveTranslations()
168
- const id = resolveCrudRecordId(parsed, ctx, translate)
169
- return { id }
170
- },
171
- response: () => ({ ok: true }),
172
- },
173
- },
174
- hooks: {
175
- afterList: async (payload, ctx) => {
176
- const items = Array.isArray(payload.items) ? payload.items : []
177
- if (!items.length) return
178
- const userIds = new Set<string>()
179
- items.forEach((item: unknown) => {
180
- if (!item || typeof item !== 'object') return
181
- const record = item as Record<string, unknown>
182
- const userId =
183
- typeof record.author_user_id === 'string'
184
- ? record.author_user_id
185
- : typeof record.authorUserId === 'string'
186
- ? record.authorUserId
187
- : null
188
- if (userId) userIds.add(userId)
189
- })
190
- if (!userIds.size) return
191
- try {
192
- const em = (ctx.container.resolve('em') as EntityManager).fork()
193
- const users = await findWithDecryption(
194
- em,
195
- User,
196
- { id: { $in: Array.from(userIds) } },
197
- undefined,
198
- { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.selectedOrganizationId ?? null },
199
- )
200
- const map = new Map<string, { name: string | null; email: string | null }>()
201
- users.forEach((user) => {
202
- const name = typeof user.name === 'string' && user.name.trim().length
203
- ? user.name.trim()
204
- : null
205
- map.set(user.id, { name, email: user.email ?? null })
206
- })
207
- items.forEach((item: unknown) => {
208
- if (!item || typeof item !== 'object') return
209
- const record = item as Record<string, unknown>
210
- const userId =
211
- typeof record.author_user_id === 'string'
212
- ? record.author_user_id
213
- : typeof record.authorUserId === 'string'
214
- ? record.authorUserId
215
- : null
216
- if (!userId) return
217
- const meta = map.get(userId)
218
- if (!meta) return
219
- record.authorName = meta.name
220
- record.authorEmail = meta.email
221
- if (!('author_name' in record)) record.author_name = meta.name
222
- if (!('author_email' in record)) record.author_email = meta.email
223
- })
224
- } catch (err) {
225
- console.warn('[resources.activities] failed to enrich author metadata', err)
226
- }
227
- },
8
+ import { createResourcesCrudOpenApi } from './openapi'
9
+
10
+ const route = makeActivityRoute({
11
+ entity: ResourcesResourceActivity,
12
+ entityId: E.resources.resources_resource_activity,
13
+ parentFkColumn: 'resource_id',
14
+ parentFkParam: 'resourceId',
15
+ features: { view: 'resources.view', manage: 'resources.manage_resources' },
16
+ createSchema: resourcesResourceActivityCreateSchema,
17
+ updateSchema: resourcesResourceActivityUpdateSchema,
18
+ commandPrefix: 'resources.resource-activities',
19
+ logPrefix: '[resources.activities]',
20
+ openApiFactory: createResourcesCrudOpenApi,
21
+ openApi: {
22
+ resourceName: 'ResourceActivity',
23
+ createDescription: 'Adds an activity to a resource timeline.',
24
+ updateDescription: 'Updates a resource activity.',
25
+ deleteDescription: 'Deletes a resource activity.',
228
26
  },
229
27
  })
230
28
 
231
- export const GET = crud.GET
232
- export const POST = crud.POST
233
- export const PUT = crud.PUT
234
- export const DELETE = crud.DELETE
235
-
236
- const activityListItemSchema = z
237
- .object({
238
- id: z.string().uuid(),
239
- resource_id: z.string().uuid().nullable().optional(),
240
- activity_type: z.string().nullable().optional(),
241
- subject: z.string().nullable().optional(),
242
- body: z.string().nullable().optional(),
243
- occurred_at: z.string().nullable().optional(),
244
- author_user_id: z.string().uuid().nullable(),
245
- appearance_icon: z.string().nullable().optional(),
246
- appearance_color: z.string().nullable().optional(),
247
- organization_id: z.string().uuid().nullable().optional(),
248
- tenant_id: z.string().uuid().nullable().optional(),
249
- created_at: z.string().nullable(),
250
- updated_at: z.string().nullable().optional(),
251
- })
252
- .passthrough()
253
-
254
- const activityCreateResponseSchema = z.object({
255
- id: z.string().uuid().nullable(),
256
- authorUserId: z.string().uuid().nullable(),
257
- })
258
-
259
- export const openApi = createResourcesCrudOpenApi({
260
- resourceName: 'ResourceActivity',
261
- querySchema: listSchema,
262
- listResponseSchema: createPagedListResponseSchema(activityListItemSchema),
263
- create: {
264
- schema: resourcesResourceActivityCreateSchema,
265
- responseSchema: activityCreateResponseSchema,
266
- description: 'Adds an activity to a resource timeline.',
267
- },
268
- update: {
269
- schema: resourcesResourceActivityUpdateSchema,
270
- responseSchema: defaultOkResponseSchema,
271
- description: 'Updates a resource activity.',
272
- },
273
- del: {
274
- schema: z.object({ id: z.string().uuid() }),
275
- responseSchema: defaultOkResponseSchema,
276
- description: 'Deletes a resource activity.',
277
- },
278
- })
29
+ export const metadata = route.metadata
30
+ export const openApi = route.openApi
31
+ export const GET = route.GET
32
+ export const POST = route.POST
33
+ export const PUT = route.PUT
34
+ export const DELETE = route.DELETE
@@ -7,6 +7,7 @@ import {
7
7
  emitCrudUndoSideEffects,
8
8
  buildChanges,
9
9
  requireId,
10
+ normalizeAuthorUserId,
10
11
  } from '@open-mercato/shared/lib/commands/helpers'
11
12
  import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
12
13
  import type { EntityManager } from '@mikro-orm/postgresql'
@@ -114,13 +115,7 @@ const createActivityCommand: CommandHandler<ResourcesResourceActivityCreateInput
114
115
  const { parsed, custom } = parseWithCustomFields(resourcesResourceActivityCreateSchema, rawInput)
115
116
  ensureTenantScope(ctx, parsed.tenantId)
116
117
  ensureOrganizationScope(ctx, parsed.organizationId)
117
- const authSub = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null
118
- const normalizedAuthor = (() => {
119
- if (parsed.authorUserId) return parsed.authorUserId
120
- if (!authSub) return null
121
- const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
122
- return uuidRegex.test(authSub) ? authSub : null
123
- })()
118
+ const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId, ctx.auth)
124
119
 
125
120
  const em = (ctx.container.resolve('em') as EntityManager).fork()
126
121
  const resource = await requireResource(em, parsed.entityId, 'Resource not found')
@@ -1,6 +1,6 @@
1
1
  import { registerCommand } from '@open-mercato/shared/lib/commands'
2
2
  import type { CommandHandler } from '@open-mercato/shared/lib/commands'
3
- import { emitCrudSideEffects, emitCrudUndoSideEffects, buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'
3
+ import { emitCrudSideEffects, emitCrudUndoSideEffects, buildChanges, requireId, normalizeAuthorUserId } from '@open-mercato/shared/lib/commands/helpers'
4
4
  import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
5
5
  import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
@@ -60,13 +60,7 @@ const createCommentCommand: CommandHandler<
60
60
  const parsed = resourcesResourceCommentCreateSchema.parse(rawInput)
61
61
  ensureTenantScope(ctx, parsed.tenantId)
62
62
  ensureOrganizationScope(ctx, parsed.organizationId)
63
- const authSub = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null
64
- const normalizedAuthor = (() => {
65
- if (parsed.authorUserId) return parsed.authorUserId
66
- if (!authSub) return null
67
- const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
68
- return uuidRegex.test(authSub) ? authSub : null
69
- })()
63
+ const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId, ctx.auth)
70
64
 
71
65
  const em = (ctx.container.resolve('em') as EntityManager).fork()
72
66
  const resource = await requireResource(em, parsed.entityId, 'Resource not found')
@@ -1,223 +1,7 @@
1
- import { createHash } from 'node:crypto'
2
- import { NextResponse } from 'next/server'
3
1
  import { z } from 'zod'
4
- import type { FilterQuery } from '@mikro-orm/core'
5
- import type { CacheStrategy } from '@open-mercato/cache'
6
- import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
7
- import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
8
- import { runWithCacheTenant } from '@open-mercato/cache'
9
- import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
- import { findAndCountWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
- import { resolveDateRange } from '@open-mercato/ui/backend/date-range'
12
2
  import { SalesOrder } from '../../../../data/entities'
13
- import { extractCustomerName, type DatePeriodOption } from '../helpers'
14
- import { resolveWidgetScope, type WidgetScopeContext } from '../../../../../customers/api/dashboard/widgets/utils'
15
-
16
- const WIDGET_CACHE_TTL = 120_000
17
- const WIDGET_CACHE_SEGMENT_TTL = 86_400_000
18
- const WIDGET_CACHE_SEGMENT_KEY = 'widget-data:__segment__'
19
- const WIDGET_CACHE_TAGS = ['widget-data', 'widget-data:sales:orders']
20
- const WIDGET_CACHE_ID = 'sales:new-orders'
21
-
22
- const querySchema = z.object({
23
- limit: z.coerce.number().min(1).max(20).default(5),
24
- datePeriod: z.enum(['last24h', 'last7d', 'last30d', 'custom']).default('last24h'),
25
- customFrom: z.string().optional(),
26
- customTo: z.string().optional(),
27
- tenantId: z.string().uuid().optional(),
28
- organizationId: z.string().uuid().optional(),
29
- })
30
-
31
- export const metadata = {
32
- GET: { requireAuth: true, requireFeatures: ['dashboards.view', 'sales.widgets.new-orders'] },
33
- }
34
-
35
- type WidgetContext = WidgetScopeContext & {
36
- limit: number
37
- datePeriod: DatePeriodOption
38
- customFrom?: string
39
- customTo?: string
40
- }
41
-
42
- type NewOrdersWidgetResponse = {
43
- items: Array<{
44
- id: string
45
- orderNumber: string
46
- status: string | null
47
- fulfillmentStatus: string | null
48
- paymentStatus: string | null
49
- customerName: string | null
50
- customerEntityId: string | null
51
- netAmount: string
52
- grossAmount: string
53
- currency: string | null
54
- createdAt: string
55
- }>
56
- total: number
57
- dateRange: {
58
- from: string
59
- to: string
60
- }
61
- }
62
-
63
- function normalizeOrganizationIds(organizationIds: string[] | null): string[] | null {
64
- if (organizationIds === null) return null
65
- const set = new Set(organizationIds)
66
- return Array.from(set).sort((a, b) => a.localeCompare(b))
67
- }
68
-
69
- function buildCacheKey(params: {
70
- tenantId: string
71
- organizationIds: string[] | null
72
- limit: number
73
- datePeriod: DatePeriodOption
74
- customFrom?: string
75
- customTo?: string
76
- }): string {
77
- const hash = createHash('sha256')
78
- hash.update(
79
- JSON.stringify({
80
- widget: WIDGET_CACHE_ID,
81
- ...params,
82
- organizationIds: normalizeOrganizationIds(params.organizationIds),
83
- })
84
- )
85
- return `widget-data:${hash.digest('hex').slice(0, 16)}`
86
- }
87
-
88
- async function resolveContext(req: Request, translate: (key: string, fallback?: string) => string): Promise<WidgetContext> {
89
- const url = new URL(req.url)
90
- const rawQuery: Record<string, string> = {}
91
- for (const [key, value] of url.searchParams.entries()) rawQuery[key] = value
92
- const parsed = querySchema.safeParse(rawQuery)
93
- if (!parsed.success) {
94
- throw new CrudHttpError(400, { error: translate('sales.errors.invalid_query', 'Invalid query parameters') })
95
- }
96
-
97
- const { container, em, tenantId, organizationIds } = await resolveWidgetScope(req, translate, {
98
- tenantId: parsed.data.tenantId ?? null,
99
- organizationId: parsed.data.organizationId ?? null,
100
- })
101
-
102
- return {
103
- container,
104
- em,
105
- tenantId,
106
- organizationIds,
107
- limit: parsed.data.limit,
108
- datePeriod: parsed.data.datePeriod,
109
- customFrom: parsed.data.customFrom,
110
- customTo: parsed.data.customTo,
111
- }
112
- }
113
-
114
- export async function GET(req: Request) {
115
- const { translate } = await resolveTranslations()
116
- try {
117
- const { container, em, tenantId, organizationIds, limit, datePeriod, customFrom, customTo } = await resolveContext(
118
- req,
119
- translate
120
- )
121
- const range = (() => {
122
- if (datePeriod === 'custom') {
123
- const from = customFrom ? new Date(customFrom) : new Date(0)
124
- const to = customTo ? new Date(customTo) : new Date()
125
- return { start: from, end: to }
126
- }
127
- const preset = datePeriod === 'last7d' ? 'last_7_days' : datePeriod === 'last30d' ? 'last_30_days' : 'today'
128
- return resolveDateRange(preset)
129
- })()
130
-
131
- let cache: CacheStrategy | null = null
132
- try {
133
- cache = container.resolve<CacheStrategy>('cache')
134
- } catch {
135
- cache = null
136
- }
137
-
138
-
139
-
140
- // Use shared WidgetDataService to fetch aggregated widget data
141
- const cacheKey = buildCacheKey({ tenantId, organizationIds, limit, datePeriod, customFrom, customTo })
142
- const tenantScope = tenantId ?? null
143
-
144
- if (cache) {
145
- try {
146
- const cached = await runWithCacheTenant(tenantScope, () => cache!.get(cacheKey))
147
- if (cached && typeof cached === 'object' && 'items' in (cached as object)) {
148
- return NextResponse.json(cached)
149
- }
150
- } catch {
151
- }
152
- }
153
-
154
- const where: FilterQuery<SalesOrder> = {
155
- tenantId,
156
- deletedAt: null,
157
- createdAt: { $gte: range.start, $lte: range.end },
158
- }
159
-
160
- if (Array.isArray(organizationIds)) {
161
- const unique = Array.from(new Set(organizationIds))
162
- where.organizationId = unique.length === 1 ? unique[0] : { $in: unique }
163
- }
164
-
165
- const organizationIdScope = Array.isArray(organizationIds) && organizationIds.length === 1 ? organizationIds[0] : null
166
- const [orders, total] = await findAndCountWithDecryption(
167
- em,
168
- SalesOrder,
169
- where,
170
- {
171
- limit,
172
- orderBy: { createdAt: 'desc' as const },
173
- },
174
- { tenantId, organizationId: organizationIdScope },
175
- )
176
-
177
- const items = orders.map((order) => ({
178
- id: order.id,
179
- orderNumber: order.orderNumber,
180
- status: order.status ?? null,
181
- fulfillmentStatus: order.fulfillmentStatus ?? null,
182
- paymentStatus: order.paymentStatus ?? null,
183
- customerName: extractCustomerName(order.customerSnapshot) ?? null,
184
- customerEntityId: order.customerEntityId ?? null,
185
- netAmount: order.grandTotalNetAmount ?? '0',
186
- grossAmount: order.grandTotalGrossAmount ?? '0',
187
- currency: order.currencyCode ?? null,
188
- createdAt: order.createdAt.toISOString(),
189
- }))
190
-
191
- const response: NewOrdersWidgetResponse = {
192
- items,
193
- total,
194
- dateRange: { from: range.start.toISOString(), to: range.end.toISOString() },
195
- }
196
-
197
- if (cache) {
198
- try {
199
- await runWithCacheTenant(tenantScope, () => cache!.set(cacheKey, response, { ttl: WIDGET_CACHE_TTL, tags: WIDGET_CACHE_TAGS }))
200
- await runWithCacheTenant(tenantScope, () => cache!.set(
201
- WIDGET_CACHE_SEGMENT_KEY,
202
- { updatedAt: response.dateRange.to },
203
- { ttl: WIDGET_CACHE_SEGMENT_TTL, tags: ['widget-data'] },
204
- ))
205
- } catch {
206
- }
207
- }
208
-
209
- return NextResponse.json(response)
210
- } catch (err) {
211
- if (err instanceof CrudHttpError) {
212
- return NextResponse.json(err.body, { status: err.status })
213
- }
214
- console.error('sales.widgets.newOrders failed', err)
215
- return NextResponse.json(
216
- { error: translate('sales.widgets.newOrders.error', 'Failed to load orders') },
217
- { status: 500 },
218
- )
219
- }
220
- }
3
+ import { extractCustomerName } from '../helpers'
4
+ import { makeDashboardWidgetRoute } from '../../../../widgets/dashboard/makeDashboardWidgetRoute'
221
5
 
222
6
  const orderItemSchema = z.object({
223
7
  id: z.string().uuid(),
@@ -233,32 +17,33 @@ const orderItemSchema = z.object({
233
17
  createdAt: z.string(),
234
18
  })
235
19
 
236
- const responseSchema = z.object({
237
- items: z.array(orderItemSchema),
238
- total: z.number(),
239
- dateRange: z.object({
240
- from: z.string(),
241
- to: z.string(),
20
+ const { GET, metadata, openApi } = makeDashboardWidgetRoute({
21
+ entity: SalesOrder,
22
+ cacheId: 'sales:new-orders',
23
+ cacheTags: ['widget-data:sales:orders'],
24
+ feature: 'sales.widgets.new-orders',
25
+ itemSchema: orderItemSchema,
26
+ errorPrefix: 'sales.widgets.newOrders',
27
+ openApi: {
28
+ summary: 'New orders dashboard widget',
29
+ description: 'Fetches recently created sales orders for the dashboard widget with a configurable date period.',
30
+ getSummary: 'Fetch recently created sales orders',
31
+ itemDescription: 'List of recent orders',
32
+ errorFallback: 'Failed to load orders',
33
+ },
34
+ mapItem: (order) => ({
35
+ id: order.id as string,
36
+ orderNumber: order.orderNumber as string,
37
+ status: (order.status as string) ?? null,
38
+ fulfillmentStatus: (order.fulfillmentStatus as string) ?? null,
39
+ paymentStatus: (order.paymentStatus as string) ?? null,
40
+ customerName: extractCustomerName(order.customerSnapshot) ?? null,
41
+ customerEntityId: (order.customerEntityId as string) ?? null,
42
+ netAmount: (order.grandTotalNetAmount as string) ?? '0',
43
+ grossAmount: (order.grandTotalGrossAmount as string) ?? '0',
44
+ currency: (order.currencyCode as string) ?? null,
45
+ createdAt: order.createdAt ? (order.createdAt as Date).toISOString() : new Date().toISOString(),
242
46
  }),
243
47
  })
244
48
 
245
- const widgetErrorSchema = z.object({ error: z.string() })
246
-
247
- export const openApi: OpenApiRouteDoc = {
248
- tag: 'Sales',
249
- summary: 'New orders dashboard widget',
250
- description: 'Fetches recently created sales orders for the dashboard widget with a configurable date period.',
251
- methods: {
252
- GET: {
253
- summary: 'Fetch recently created sales orders',
254
- query: querySchema,
255
- responses: [{ status: 200, description: 'List of recent orders', schema: responseSchema }],
256
- errors: [
257
- { status: 400, description: 'Invalid query parameters', schema: widgetErrorSchema },
258
- { status: 401, description: 'Unauthorized', schema: widgetErrorSchema },
259
- { status: 403, description: 'Forbidden', schema: widgetErrorSchema },
260
- { status: 500, description: 'Widget failed to load', schema: widgetErrorSchema },
261
- ],
262
- },
263
- },
264
- }
49
+ export { GET, metadata, openApi }