@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.
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +17 -154
- package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +3 -3
- package/dist/modules/currencies/backend/exchange-rates/create/page.js +14 -152
- package/dist/modules/currencies/backend/exchange-rates/create/page.js.map +2 -2
- package/dist/modules/currencies/lib/exchangeRateFormConfig.js +167 -0
- package/dist/modules/currencies/lib/exchangeRateFormConfig.js.map +7 -0
- package/dist/modules/customers/api/dashboard/widgets/utils.js +1 -34
- package/dist/modules/customers/api/dashboard/widgets/utils.js.map +2 -2
- package/dist/modules/customers/commands/activities.js +3 -8
- package/dist/modules/customers/commands/activities.js.map +2 -2
- package/dist/modules/customers/commands/comments.js +2 -8
- package/dist/modules/customers/commands/comments.js.map +2 -2
- package/dist/modules/dashboards/lib/widgetScope.js +38 -0
- package/dist/modules/dashboards/lib/widgetScope.js.map +7 -0
- package/dist/modules/entities/lib/makeActivityRoute.js +265 -0
- package/dist/modules/entities/lib/makeActivityRoute.js.map +7 -0
- package/dist/modules/resources/api/activities.js +24 -232
- package/dist/modules/resources/api/activities.js.map +2 -2
- package/dist/modules/resources/commands/activities.js +3 -8
- package/dist/modules/resources/commands/activities.js.map +2 -2
- package/dist/modules/resources/commands/comments.js +2 -8
- package/dist/modules/resources/commands/comments.js.map +2 -2
- package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js +27 -182
- package/dist/modules/sales/api/dashboard/widgets/new-orders/route.js.map +2 -2
- package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js +28 -183
- package/dist/modules/sales/api/dashboard/widgets/new-quotes/route.js.map +2 -2
- package/dist/modules/sales/api/order-line-statuses/route.js +15 -194
- package/dist/modules/sales/api/order-line-statuses/route.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +15 -281
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/api/order-statuses/route.js +15 -194
- package/dist/modules/sales/api/order-statuses/route.js.map +2 -2
- package/dist/modules/sales/api/payment-statuses/route.js +15 -194
- package/dist/modules/sales/api/payment-statuses/route.js.map +2 -2
- package/dist/modules/sales/api/quote-lines/route.js +15 -279
- package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
- package/dist/modules/sales/api/shipment-statuses/route.js +15 -194
- package/dist/modules/sales/api/shipment-statuses/route.js.map +2 -2
- package/dist/modules/sales/components/PaymentMethodsSettings.js +3 -84
- package/dist/modules/sales/components/PaymentMethodsSettings.js.map +2 -2
- package/dist/modules/sales/components/ProviderFieldInput.js +86 -0
- package/dist/modules/sales/components/ProviderFieldInput.js.map +7 -0
- package/dist/modules/sales/components/ShippingMethodsSettings.js +3 -82
- package/dist/modules/sales/components/ShippingMethodsSettings.js.map +2 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js +308 -0
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +7 -0
- package/dist/modules/sales/lib/makeStatusDictionaryRoute.js +206 -0
- package/dist/modules/sales/lib/makeStatusDictionaryRoute.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js +178 -0
- package/dist/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.js.map +7 -0
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +1 -39
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +1 -39
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/shared.js +46 -0
- package/dist/modules/sales/widgets/dashboard/shared.js.map +7 -0
- package/dist/modules/staff/api/activities.js +24 -232
- package/dist/modules/staff/api/activities.js.map +2 -2
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +14 -34
- package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js +15 -34
- package/dist/modules/staff/backend/staff/my-leave-requests/[id]/page.js.map +2 -2
- package/dist/modules/staff/commands/activities.js +3 -8
- package/dist/modules/staff/commands/activities.js.map +2 -2
- package/dist/modules/staff/commands/comments.js +2 -8
- package/dist/modules/staff/commands/comments.js.map +2 -2
- package/dist/modules/staff/lib/leaveRequestHelpers.js +41 -0
- package/dist/modules/staff/lib/leaveRequestHelpers.js.map +7 -0
- package/package.json +2 -2
- package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +20 -180
- package/src/modules/currencies/backend/exchange-rates/create/page.tsx +16 -175
- package/src/modules/currencies/lib/exchangeRateFormConfig.ts +200 -0
- package/src/modules/customers/api/dashboard/widgets/utils.ts +1 -53
- package/src/modules/customers/commands/activities.ts +2 -8
- package/src/modules/customers/commands/comments.ts +2 -8
- package/src/modules/dashboards/i18n/de.json +3 -0
- package/src/modules/dashboards/i18n/en.json +3 -0
- package/src/modules/dashboards/i18n/es.json +3 -0
- package/src/modules/dashboards/i18n/pl.json +3 -0
- package/src/modules/dashboards/lib/widgetScope.ts +53 -0
- package/src/modules/entities/lib/makeActivityRoute.ts +327 -0
- package/src/modules/resources/api/activities.ts +25 -269
- package/src/modules/resources/commands/activities.ts +2 -7
- package/src/modules/resources/commands/comments.ts +2 -8
- package/src/modules/sales/api/dashboard/widgets/new-orders/route.ts +29 -244
- package/src/modules/sales/api/dashboard/widgets/new-quotes/route.ts +30 -245
- package/src/modules/sales/api/order-line-statuses/route.ts +16 -209
- package/src/modules/sales/api/order-lines/route.ts +16 -300
- package/src/modules/sales/api/order-statuses/route.ts +16 -209
- package/src/modules/sales/api/payment-statuses/route.ts +16 -209
- package/src/modules/sales/api/quote-lines/route.ts +16 -298
- package/src/modules/sales/api/shipment-statuses/route.ts +16 -209
- package/src/modules/sales/components/PaymentMethodsSettings.tsx +3 -88
- package/src/modules/sales/components/ProviderFieldInput.tsx +85 -0
- package/src/modules/sales/components/ShippingMethodsSettings.tsx +3 -86
- package/src/modules/sales/lib/makeSalesLineRoute.ts +345 -0
- package/src/modules/sales/lib/makeStatusDictionaryRoute.ts +229 -0
- package/src/modules/sales/widgets/dashboard/makeDashboardWidgetRoute.ts +247 -0
- package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +7 -50
- package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +7 -49
- package/src/modules/sales/widgets/dashboard/shared.ts +44 -0
- package/src/modules/staff/api/activities.ts +25 -269
- package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +15 -69
- package/src/modules/staff/backend/staff/my-leave-requests/[id]/page.tsx +16 -65
- package/src/modules/staff/commands/activities.ts +2 -7
- package/src/modules/staff/commands/comments.ts +2 -8
- package/src/modules/staff/lib/leaveRequestHelpers.ts +78 -0
|
@@ -1,53 +1 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|