@open-mercato/core 0.4.6-develop-9ff1d4a9a2 → 0.4.6-develop-219dae16c5

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,53 +1 @@
1
- import type { EntityManager } from '@mikro-orm/postgresql'
2
- import { createRequestContainer, type AppContainer } from '@open-mercato/shared/lib/di/container'
3
- import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
- import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
- import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
-
7
- export type WidgetScopeContext = {
8
- container: AppContainer
9
- em: EntityManager
10
- tenantId: string
11
- organizationIds: string[] | null
12
- }
13
-
14
- export async function resolveWidgetScope(
15
- req: Request,
16
- translate: (key: string, fallback?: string) => string,
17
- overrides?: { tenantId?: string | null; organizationId?: string | null }
18
- ): Promise<WidgetScopeContext> {
19
- const auth = await getAuthFromRequest(req)
20
- if (!auth) {
21
- throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })
22
- }
23
-
24
- const container = await createRequestContainer()
25
- const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
26
-
27
- const tenantId = overrides?.tenantId ?? auth.tenantId ?? null
28
- if (!tenantId) {
29
- throw new CrudHttpError(400, { error: translate('customers.errors.tenant_required', 'Tenant context is required') })
30
- }
31
-
32
- const organizationIds = (() => {
33
- if (overrides?.organizationId) return [overrides.organizationId]
34
- if (scope?.selectedId) return [scope.selectedId]
35
- if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds
36
- if (scope?.allowedIds === null) return null
37
- if (auth.orgId) return [auth.orgId]
38
- return [] as string[]
39
- })()
40
-
41
- if (organizationIds !== null && organizationIds.length === 0) {
42
- throw new CrudHttpError(400, { error: translate('customers.errors.organization_required', 'Organization context is required') })
43
- }
44
-
45
- const em = (container.resolve('em') as EntityManager)
46
-
47
- return {
48
- container,
49
- em,
50
- tenantId,
51
- organizationIds,
52
- }
53
- }
1
+ export { resolveWidgetScope, type WidgetScopeContext } from '@open-mercato/core/modules/dashboards/lib/widgetScope'
@@ -6,6 +6,7 @@ import {
6
6
  emitCrudSideEffects,
7
7
  emitCrudUndoSideEffects,
8
8
  requireId,
9
+ normalizeAuthorUserId,
9
10
  } from '@open-mercato/shared/lib/commands/helpers'
10
11
  import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
11
12
  import type { EntityManager } from '@mikro-orm/postgresql'
@@ -54,8 +55,6 @@ const activityCrudEvents: CrudEventsConfig = {
54
55
  }),
55
56
  }
56
57
 
57
- const UUID_REGEX = /^[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}$/
58
-
59
58
  type ActivitySnapshot = {
60
59
  activity: {
61
60
  id: string
@@ -149,12 +148,7 @@ const createActivityCommand: CommandHandler<ActivityCreateInput, { activityId: s
149
148
  ensureSameScope(entity, parsed.organizationId, parsed.tenantId)
150
149
  const deal = await requireDealInScope(em, parsed.dealId, parsed.tenantId, parsed.organizationId)
151
150
 
152
- const authSub = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null
153
- const normalizedAuthor = (() => {
154
- if (parsed.authorUserId) return parsed.authorUserId
155
- if (!authSub) return null
156
- return UUID_REGEX.test(authSub) ? authSub : null
157
- })()
151
+ const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId, ctx.auth)
158
152
 
159
153
  const dictionaryEntry = await ensureDictionaryEntry(em, {
160
154
  tenantId: parsed.tenantId,
@@ -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 { CustomerComment } from '../data/entities'
@@ -79,13 +79,7 @@ const createCommentCommand: CommandHandler<CommentCreateInput, { commentId: stri
79
79
  const parsed = commentCreateSchema.parse(rawInput)
80
80
  ensureTenantScope(ctx, parsed.tenantId)
81
81
  ensureOrganizationScope(ctx, parsed.organizationId)
82
- const authSub = ctx.auth?.isApiKey ? null : ctx.auth?.sub ?? null
83
- const normalizedAuthor = (() => {
84
- if (parsed.authorUserId) return parsed.authorUserId
85
- if (!authSub) return null
86
- 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}$/
87
- return uuidRegex.test(authSub) ? authSub : null
88
- })()
82
+ const normalizedAuthor = normalizeAuthorUserId(parsed.authorUserId, ctx.auth)
89
83
 
90
84
  const em = (ctx.container.resolve('em') as EntityManager).fork()
91
85
  const entity = await requireCustomerEntity(em, parsed.entityId, undefined, 'Customer not found')
@@ -96,6 +96,9 @@
96
96
  "dashboards.analytics.widgets.topProducts.empty": "Keine Produktverkaufsdaten für diesen Zeitraum",
97
97
  "dashboards.analytics.widgets.topProducts.error": "Produktdaten konnten nicht geladen werden",
98
98
  "dashboards.analytics.widgets.topProducts.title": "Top-Produkte nach Umsatz",
99
+ "dashboards.errors.organization_required": "Organisationskontext ist erforderlich",
100
+ "dashboards.errors.tenant_required": "Mandantenkontext ist erforderlich",
101
+ "dashboards.errors.unauthorized": "Nicht autorisiert",
99
102
  "dashboards.widgets.effective": "Aktive Widgets:",
100
103
  "dashboards.widgets.error.load": "Widget-Konfiguration konnte nicht geladen werden.",
101
104
  "dashboards.widgets.error.save": "Dashboard-Widget-Einstellungen konnten nicht gespeichert werden.",
@@ -96,6 +96,9 @@
96
96
  "dashboards.analytics.widgets.topProducts.empty": "No product sales data for this period",
97
97
  "dashboards.analytics.widgets.topProducts.error": "Failed to load top products data",
98
98
  "dashboards.analytics.widgets.topProducts.title": "Top Products by Revenue",
99
+ "dashboards.errors.organization_required": "Organization context is required",
100
+ "dashboards.errors.tenant_required": "Tenant context is required",
101
+ "dashboards.errors.unauthorized": "Unauthorized",
99
102
  "dashboards.widgets.effective": "Effective widgets:",
100
103
  "dashboards.widgets.error.load": "Unable to load widget configuration.",
101
104
  "dashboards.widgets.error.save": "Unable to save dashboard widget preferences.",
@@ -96,6 +96,9 @@
96
96
  "dashboards.analytics.widgets.topProducts.empty": "No hay datos de ventas de productos para este período",
97
97
  "dashboards.analytics.widgets.topProducts.error": "No se pudieron cargar los datos de productos",
98
98
  "dashboards.analytics.widgets.topProducts.title": "Productos principales por ingresos",
99
+ "dashboards.errors.organization_required": "Se requiere contexto de organización",
100
+ "dashboards.errors.tenant_required": "Se requiere contexto de inquilino",
101
+ "dashboards.errors.unauthorized": "No autorizado",
99
102
  "dashboards.widgets.effective": "Widgets efectivos:",
100
103
  "dashboards.widgets.error.load": "No se pudo cargar la configuración de widgets.",
101
104
  "dashboards.widgets.error.save": "No se pudieron guardar las preferencias de los widgets del panel.",
@@ -96,6 +96,9 @@
96
96
  "dashboards.analytics.widgets.topProducts.empty": "Brak danych o sprzedaży produktów dla tego okresu",
97
97
  "dashboards.analytics.widgets.topProducts.error": "Nie udało się załadować danych o produktach",
98
98
  "dashboards.analytics.widgets.topProducts.title": "Najlepsze produkty wg przychodu",
99
+ "dashboards.errors.organization_required": "Wymagany kontekst organizacji",
100
+ "dashboards.errors.tenant_required": "Wymagany kontekst najemcy",
101
+ "dashboards.errors.unauthorized": "Brak autoryzacji",
99
102
  "dashboards.widgets.effective": "Aktywne widżety:",
100
103
  "dashboards.widgets.error.load": "Nie udało się wczytać konfiguracji widżetów.",
101
104
  "dashboards.widgets.error.save": "Nie udało się zapisać preferencji widżetów pulpitu.",
@@ -0,0 +1,53 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import { createRequestContainer, type AppContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+
7
+ export type WidgetScopeContext = {
8
+ container: AppContainer
9
+ em: EntityManager
10
+ tenantId: string
11
+ organizationIds: string[] | null
12
+ }
13
+
14
+ export async function resolveWidgetScope(
15
+ req: Request,
16
+ translate: (key: string, fallback?: string) => string,
17
+ overrides?: { tenantId?: string | null; organizationId?: string | null }
18
+ ): Promise<WidgetScopeContext> {
19
+ const auth = await getAuthFromRequest(req)
20
+ if (!auth) {
21
+ throw new CrudHttpError(401, { error: translate('dashboards.errors.unauthorized', 'Unauthorized') })
22
+ }
23
+
24
+ const container = await createRequestContainer()
25
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
26
+
27
+ const tenantId = overrides?.tenantId ?? auth.tenantId ?? null
28
+ if (!tenantId) {
29
+ throw new CrudHttpError(400, { error: translate('dashboards.errors.tenant_required', 'Tenant context is required') })
30
+ }
31
+
32
+ const organizationIds = (() => {
33
+ if (overrides?.organizationId) return [overrides.organizationId]
34
+ if (scope?.selectedId) return [scope.selectedId]
35
+ if (Array.isArray(scope?.filterIds) && scope.filterIds.length > 0) return scope.filterIds
36
+ if (scope?.allowedIds === null) return null
37
+ if (auth.orgId) return [auth.orgId]
38
+ return [] as string[]
39
+ })()
40
+
41
+ if (organizationIds !== null && organizationIds.length === 0) {
42
+ throw new CrudHttpError(400, { error: translate('dashboards.errors.organization_required', 'Organization context is required') })
43
+ }
44
+
45
+ const em = (container.resolve('em') as EntityManager)
46
+
47
+ return {
48
+ container,
49
+ em,
50
+ tenantId,
51
+ organizationIds,
52
+ }
53
+ }
@@ -0,0 +1,327 @@
1
+ import { z, type ZodTypeAny } 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'
7
+ import { User } from '@open-mercato/core/modules/auth/data/entities'
8
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
9
+ import type { CrudOpenApiOptions } from '@open-mercato/shared/lib/openapi/crud'
10
+ import {
11
+ createPagedListResponseSchema as createSharedPagedListResponseSchema,
12
+ defaultOkResponseSchema as sharedDefaultOkResponseSchema,
13
+ } from '@open-mercato/shared/lib/openapi/crud'
14
+
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- MikroORM entity class constructor
16
+ type EntityClass = new (...args: any[]) => unknown
17
+
18
+ interface ActivityRouteConfig {
19
+ entity: EntityClass
20
+ entityId: string
21
+ parentFkColumn: string
22
+ parentFkParam: string
23
+ features: { view: string; manage: string }
24
+ createSchema: ZodTypeAny
25
+ updateSchema: ZodTypeAny
26
+ commandPrefix: string
27
+ logPrefix: string
28
+ openApiFactory: (options: CrudOpenApiOptions) => OpenApiRouteDoc
29
+ openApi: {
30
+ resourceName: string
31
+ createDescription: string
32
+ updateDescription: string
33
+ deleteDescription: string
34
+ }
35
+ }
36
+
37
+ function createPagedListResponseSchema(itemSchema: ZodTypeAny) {
38
+ return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true })
39
+ }
40
+
41
+ const defaultOkResponseSchema = sharedDefaultOkResponseSchema
42
+
43
+ const rawBodySchema = z.object({}).passthrough()
44
+
45
+ const listSchema = z
46
+ .object({
47
+ page: z.coerce.number().min(1).default(1),
48
+ pageSize: z.coerce.number().min(1).max(100).default(50),
49
+ entityId: z.string().uuid().optional(),
50
+ sortField: z.string().optional(),
51
+ sortDir: z.enum(['asc', 'desc']).optional(),
52
+ })
53
+ .passthrough()
54
+
55
+ const sortFieldMap = {
56
+ occurredAt: 'occurred_at',
57
+ createdAt: 'created_at',
58
+ updatedAt: 'updated_at',
59
+ }
60
+
61
+ const activityCreateResponseSchema = z.object({
62
+ id: z.string().uuid().nullable(),
63
+ authorUserId: z.string().uuid().nullable(),
64
+ })
65
+
66
+ export function makeActivityRoute(config: ActivityRouteConfig) {
67
+ const {
68
+ entity,
69
+ entityId,
70
+ parentFkColumn,
71
+ parentFkParam,
72
+ features,
73
+ createSchema,
74
+ updateSchema,
75
+ commandPrefix,
76
+ logPrefix,
77
+ openApiFactory,
78
+ openApi: openApiConfig,
79
+ } = config
80
+
81
+ const routeMetadata = {
82
+ GET: { requireAuth: true, requireFeatures: [features.view] },
83
+ POST: { requireAuth: true, requireFeatures: [features.manage] },
84
+ PUT: { requireAuth: true, requireFeatures: [features.manage] },
85
+ DELETE: { requireAuth: true, requireFeatures: [features.manage] },
86
+ }
87
+
88
+ const fields = [
89
+ 'id',
90
+ parentFkColumn,
91
+ 'activity_type',
92
+ 'subject',
93
+ 'body',
94
+ 'occurred_at',
95
+ 'author_user_id',
96
+ 'appearance_icon',
97
+ 'appearance_color',
98
+ 'organization_id',
99
+ 'tenant_id',
100
+ 'created_at',
101
+ 'updated_at',
102
+ ]
103
+
104
+ const crud = makeCrudRoute({
105
+ metadata: routeMetadata,
106
+ orm: {
107
+ entity,
108
+ idField: 'id',
109
+ orgField: 'organizationId',
110
+ tenantField: 'tenantId',
111
+ },
112
+ indexer: {
113
+ entityType: entityId,
114
+ },
115
+ list: {
116
+ schema: listSchema,
117
+ entityId,
118
+ fields,
119
+ decorateCustomFields: {
120
+ entityIds: entityId,
121
+ },
122
+ sortFieldMap,
123
+ buildFilters: async (query) => {
124
+ const filters: Record<string, unknown> = {}
125
+ if (query.entityId) filters[parentFkColumn] = { $eq: query.entityId }
126
+ return filters
127
+ },
128
+ transformItem: (item: Record<string, unknown>) => {
129
+ const record = (item ?? {}) as Record<string, unknown>
130
+ const toIsoString = (value: unknown): string | null => {
131
+ if (value == null) return null
132
+ if (value instanceof Date) return value.toISOString()
133
+ if (typeof value === 'string') {
134
+ const trimmed = value.trim()
135
+ if (!trimmed.length) return null
136
+ const date = new Date(trimmed)
137
+ return Number.isNaN(date.getTime()) ? trimmed : date.toISOString()
138
+ }
139
+ return null
140
+ }
141
+ const readString = (value: unknown): string | null => (typeof value === 'string' ? value : null)
142
+ const idValue = readString(record.id) ?? (record.id != null ? String(record.id) : '')
143
+ const parentId = readString(record[parentFkColumn]) ?? readString(record[parentFkParam]) ?? null
144
+ const activityType =
145
+ readString(record['activity_type']) ??
146
+ readString(record['activityType']) ??
147
+ ''
148
+ const subject =
149
+ readString(record.subject) ??
150
+ (record.subject == null ? null : String(record.subject))
151
+ const body =
152
+ readString(record.body) ??
153
+ (record.body == null ? null : String(record.body))
154
+ const authorUserId =
155
+ readString(record['author_user_id']) ?? readString(record['authorUserId']) ?? null
156
+ const appearanceIconRaw =
157
+ readString(record['appearance_icon']) ?? readString(record['appearanceIcon'])
158
+ const appearanceColorRaw =
159
+ readString(record['appearance_color']) ?? readString(record['appearanceColor'])
160
+ const organizationId =
161
+ readString(record['organization_id']) ?? readString(record['organizationId'])
162
+ const tenantId =
163
+ readString(record['tenant_id']) ?? readString(record['tenantId'])
164
+ const output: Record<string, unknown> = {
165
+ id: idValue,
166
+ entityId: parentId,
167
+ [parentFkParam]: parentId,
168
+ activityType,
169
+ subject,
170
+ body,
171
+ occurredAt: toIsoString(record['occurred_at'] ?? record['occurredAt']),
172
+ createdAt: toIsoString(record['created_at'] ?? record['createdAt']),
173
+ authorUserId,
174
+ organizationId,
175
+ tenantId,
176
+ appearanceIcon: appearanceIconRaw && appearanceIconRaw.trim().length ? appearanceIconRaw : null,
177
+ appearanceColor: appearanceColorRaw && appearanceColorRaw.trim().length ? appearanceColorRaw : null,
178
+ customFields: Array.isArray(record.customFields) ? record.customFields : undefined,
179
+ customValues: record.customValues ?? undefined,
180
+ }
181
+ for (const [key, value] of Object.entries(record)) {
182
+ if (key.startsWith('cf_') || key.startsWith('cf:')) {
183
+ output[key] = value
184
+ }
185
+ }
186
+ return output
187
+ },
188
+ },
189
+ actions: {
190
+ create: {
191
+ commandId: `${commandPrefix}.create`,
192
+ schema: rawBodySchema,
193
+ mapInput: async ({ raw, ctx }) => {
194
+ const { translate } = await resolveTranslations()
195
+ return parseScopedCommandInput(createSchema, raw ?? {}, ctx, translate)
196
+ },
197
+ response: ({ result }) => ({
198
+ id: result?.activityId ?? result?.id ?? null,
199
+ authorUserId: result?.authorUserId ?? null,
200
+ }),
201
+ status: 201,
202
+ },
203
+ update: {
204
+ commandId: `${commandPrefix}.update`,
205
+ schema: rawBodySchema,
206
+ mapInput: async ({ raw, ctx }) => {
207
+ const { translate } = await resolveTranslations()
208
+ return parseScopedCommandInput(updateSchema, raw ?? {}, ctx, translate)
209
+ },
210
+ response: () => ({ ok: true }),
211
+ },
212
+ delete: {
213
+ commandId: `${commandPrefix}.delete`,
214
+ schema: rawBodySchema,
215
+ mapInput: async ({ parsed, ctx }) => {
216
+ const { translate } = await resolveTranslations()
217
+ const id = resolveCrudRecordId(parsed, ctx, translate)
218
+ return { id }
219
+ },
220
+ response: () => ({ ok: true }),
221
+ },
222
+ },
223
+ hooks: {
224
+ afterList: async (payload, ctx) => {
225
+ const items = Array.isArray(payload.items) ? payload.items : []
226
+ if (!items.length) return
227
+ const userIds = new Set<string>()
228
+ items.forEach((item: unknown) => {
229
+ if (!item || typeof item !== 'object') return
230
+ const record = item as Record<string, unknown>
231
+ const userId =
232
+ typeof record.author_user_id === 'string'
233
+ ? record.author_user_id
234
+ : typeof record.authorUserId === 'string'
235
+ ? record.authorUserId
236
+ : null
237
+ if (userId) userIds.add(userId)
238
+ })
239
+ if (!userIds.size) return
240
+ try {
241
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
242
+ const users = await findWithDecryption(
243
+ em,
244
+ User,
245
+ { id: { $in: Array.from(userIds) } },
246
+ undefined,
247
+ { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.selectedOrganizationId ?? null },
248
+ )
249
+ const map = new Map<string, { name: string | null; email: string | null }>()
250
+ users.forEach((user) => {
251
+ const name = typeof user.name === 'string' && user.name.trim().length
252
+ ? user.name.trim()
253
+ : null
254
+ map.set(user.id, { name, email: user.email ?? null })
255
+ })
256
+ items.forEach((item: unknown) => {
257
+ if (!item || typeof item !== 'object') return
258
+ const record = item as Record<string, unknown>
259
+ const userId =
260
+ typeof record.author_user_id === 'string'
261
+ ? record.author_user_id
262
+ : typeof record.authorUserId === 'string'
263
+ ? record.authorUserId
264
+ : null
265
+ if (!userId) return
266
+ const meta = map.get(userId)
267
+ if (!meta) return
268
+ record.authorName = meta.name
269
+ record.authorEmail = meta.email
270
+ if (!('author_name' in record)) record.author_name = meta.name
271
+ if (!('author_email' in record)) record.author_email = meta.email
272
+ })
273
+ } catch (err) {
274
+ console.warn(`${logPrefix} failed to enrich author metadata`, err)
275
+ }
276
+ },
277
+ },
278
+ })
279
+
280
+ const activityListItemSchema = z
281
+ .object({
282
+ id: z.string().uuid(),
283
+ [parentFkColumn]: z.string().uuid().nullable().optional(),
284
+ activity_type: z.string().nullable().optional(),
285
+ subject: z.string().nullable().optional(),
286
+ body: z.string().nullable().optional(),
287
+ occurred_at: z.string().nullable().optional(),
288
+ author_user_id: z.string().uuid().nullable(),
289
+ appearance_icon: z.string().nullable().optional(),
290
+ appearance_color: z.string().nullable().optional(),
291
+ organization_id: z.string().uuid().nullable().optional(),
292
+ tenant_id: z.string().uuid().nullable().optional(),
293
+ created_at: z.string().nullable(),
294
+ updated_at: z.string().nullable().optional(),
295
+ })
296
+ .passthrough()
297
+
298
+ const openApi = openApiFactory({
299
+ resourceName: openApiConfig.resourceName,
300
+ querySchema: listSchema,
301
+ listResponseSchema: createPagedListResponseSchema(activityListItemSchema),
302
+ create: {
303
+ schema: createSchema,
304
+ responseSchema: activityCreateResponseSchema,
305
+ description: openApiConfig.createDescription,
306
+ },
307
+ update: {
308
+ schema: updateSchema,
309
+ responseSchema: defaultOkResponseSchema,
310
+ description: openApiConfig.updateDescription,
311
+ },
312
+ del: {
313
+ schema: z.object({ id: z.string().uuid() }),
314
+ responseSchema: defaultOkResponseSchema,
315
+ description: openApiConfig.deleteDescription,
316
+ },
317
+ })
318
+
319
+ return {
320
+ metadata: routeMetadata,
321
+ openApi,
322
+ GET: crud.GET,
323
+ POST: crud.POST,
324
+ PUT: crud.PUT,
325
+ DELETE: crud.DELETE,
326
+ }
327
+ }