@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.
- 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,278 +1,34 @@
|
|
|
1
|
-
import {
|
|
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
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
232
|
-
export const
|
|
233
|
-
export const
|
|
234
|
-
export const
|
|
235
|
-
|
|
236
|
-
const
|
|
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
|
|
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
|
|
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
|
|
14
|
-
import {
|
|
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
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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 }
|