@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6245.1.2be3b151f8
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarToolbar.js +4 -6
- package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventBlock.js +2 -6
- package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
- package/dist/modules/customers/components/calendar/UpcomingCards.js +9 -7
- package/dist/modules/customers/components/calendar/UpcomingCards.js.map +2 -2
- package/dist/modules/customers/lib/calendar/format.js +27 -0
- package/dist/modules/customers/lib/calendar/format.js.map +7 -0
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +46 -13
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +43 -8
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/components/calendar/CalendarToolbar.tsx +4 -7
- package/src/modules/customers/components/calendar/EventBlock.tsx +2 -6
- package/src/modules/customers/components/calendar/UpcomingCards.tsx +9 -7
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/lib/calendar/format.ts +25 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +57 -13
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +57 -9
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -18,8 +18,14 @@ import { withScopedPayload } from '../utils'
|
|
|
18
18
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
19
19
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
20
20
|
import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
21
|
+
import {
|
|
22
|
+
runCrudMutationGuardAfterSuccess,
|
|
23
|
+
validateCrudMutationGuard,
|
|
24
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
21
25
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
22
26
|
|
|
27
|
+
const PIPELINE_RESOURCE_KIND = 'customers.pipeline'
|
|
28
|
+
|
|
23
29
|
export const metadata = {
|
|
24
30
|
GET: { requireAuth: true, requireFeatures: ['customers.pipelines.view'] },
|
|
25
31
|
POST: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
|
|
@@ -29,7 +35,7 @@ export const metadata = {
|
|
|
29
35
|
|
|
30
36
|
async function buildContext(
|
|
31
37
|
req: Request
|
|
32
|
-
): Promise<{ ctx: CommandRuntimeContext; organizationId: string | null; tenantId: string | null }> {
|
|
38
|
+
): Promise<{ ctx: CommandRuntimeContext; organizationId: string | null; tenantId: string | null; translate: (key: string, fallback?: string) => string }> {
|
|
33
39
|
const container = await createRequestContainer()
|
|
34
40
|
const auth = await getAuthFromRequest(req)
|
|
35
41
|
const { translate } = await resolveTranslations()
|
|
@@ -45,14 +51,14 @@ async function buildContext(
|
|
|
45
51
|
}
|
|
46
52
|
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
47
53
|
const tenantId = auth.tenantId ?? null
|
|
48
|
-
return { ctx, organizationId, tenantId }
|
|
54
|
+
return { ctx, organizationId, tenantId, translate }
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export async function GET(req: Request) {
|
|
52
58
|
try {
|
|
53
|
-
const { ctx, organizationId, tenantId } = await buildContext(req)
|
|
59
|
+
const { ctx, organizationId, tenantId, translate } = await buildContext(req)
|
|
54
60
|
if (!organizationId || !tenantId) {
|
|
55
|
-
return NextResponse.json({ error: 'Organization and tenant context required' }, { status: 400 })
|
|
61
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
56
62
|
}
|
|
57
63
|
const url = new URL(req.url)
|
|
58
64
|
const isDefaultParam = url.searchParams.get('isDefault')
|
|
@@ -84,16 +90,47 @@ export async function GET(req: Request) {
|
|
|
84
90
|
|
|
85
91
|
export async function POST(req: Request) {
|
|
86
92
|
try {
|
|
87
|
-
const { ctx } = await buildContext(req)
|
|
93
|
+
const { ctx, organizationId, tenantId, translate } = await buildContext(req)
|
|
94
|
+
if (!organizationId || !tenantId) {
|
|
95
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
96
|
+
}
|
|
88
97
|
const body = await req.json().catch(() => ({}))
|
|
89
|
-
const { translate } = await resolveTranslations()
|
|
90
98
|
const scoped = withScopedPayload(body, ctx, translate)
|
|
99
|
+
const input = pipelineCreateSchema.parse(scoped)
|
|
100
|
+
|
|
101
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
102
|
+
tenantId,
|
|
103
|
+
organizationId,
|
|
104
|
+
userId: ctx.auth!.sub,
|
|
105
|
+
resourceKind: PIPELINE_RESOURCE_KIND,
|
|
106
|
+
resourceId: organizationId,
|
|
107
|
+
operation: 'create',
|
|
108
|
+
requestMethod: req.method,
|
|
109
|
+
requestHeaders: req.headers,
|
|
110
|
+
mutationPayload: input,
|
|
111
|
+
})
|
|
112
|
+
if (guardResult && !guardResult.ok) {
|
|
113
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
114
|
+
}
|
|
91
115
|
|
|
92
116
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
93
117
|
const { result, logEntry } = await commandBus.execute<PipelineCreateInput, { pipelineId: string }>(
|
|
94
118
|
'customers.pipelines.create',
|
|
95
|
-
{ input
|
|
119
|
+
{ input, ctx },
|
|
96
120
|
)
|
|
121
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
122
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
123
|
+
tenantId,
|
|
124
|
+
organizationId,
|
|
125
|
+
userId: ctx.auth!.sub,
|
|
126
|
+
resourceKind: PIPELINE_RESOURCE_KIND,
|
|
127
|
+
resourceId: result?.pipelineId ?? organizationId,
|
|
128
|
+
operation: 'create',
|
|
129
|
+
requestMethod: req.method,
|
|
130
|
+
requestHeaders: req.headers,
|
|
131
|
+
metadata: guardResult.metadata ?? null,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
97
134
|
const response = NextResponse.json({ id: result?.pipelineId ?? null }, { status: 201 })
|
|
98
135
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
99
136
|
response.headers.set(
|
|
@@ -121,16 +158,47 @@ export async function POST(req: Request) {
|
|
|
121
158
|
|
|
122
159
|
export async function PUT(req: Request) {
|
|
123
160
|
try {
|
|
124
|
-
const { ctx } = await buildContext(req)
|
|
161
|
+
const { ctx, organizationId, tenantId, translate } = await buildContext(req)
|
|
162
|
+
if (!organizationId || !tenantId) {
|
|
163
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
164
|
+
}
|
|
125
165
|
const body = await req.json().catch(() => ({}))
|
|
126
|
-
const { translate } = await resolveTranslations()
|
|
127
166
|
const scoped = withScopedPayload(body, ctx, translate)
|
|
167
|
+
const input = pipelineUpdateSchema.parse(scoped)
|
|
168
|
+
|
|
169
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
170
|
+
tenantId,
|
|
171
|
+
organizationId,
|
|
172
|
+
userId: ctx.auth!.sub,
|
|
173
|
+
resourceKind: PIPELINE_RESOURCE_KIND,
|
|
174
|
+
resourceId: input.id,
|
|
175
|
+
operation: 'update',
|
|
176
|
+
requestMethod: req.method,
|
|
177
|
+
requestHeaders: req.headers,
|
|
178
|
+
mutationPayload: input,
|
|
179
|
+
})
|
|
180
|
+
if (guardResult && !guardResult.ok) {
|
|
181
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
182
|
+
}
|
|
128
183
|
|
|
129
184
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
130
185
|
const { logEntry } = await commandBus.execute<PipelineUpdateInput, void>(
|
|
131
186
|
'customers.pipelines.update',
|
|
132
|
-
{ input
|
|
187
|
+
{ input, ctx },
|
|
133
188
|
)
|
|
189
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
190
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
191
|
+
tenantId,
|
|
192
|
+
organizationId,
|
|
193
|
+
userId: ctx.auth!.sub,
|
|
194
|
+
resourceKind: PIPELINE_RESOURCE_KIND,
|
|
195
|
+
resourceId: input.id,
|
|
196
|
+
operation: 'update',
|
|
197
|
+
requestMethod: req.method,
|
|
198
|
+
requestHeaders: req.headers,
|
|
199
|
+
metadata: guardResult.metadata ?? null,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
134
202
|
const response = NextResponse.json({ ok: true })
|
|
135
203
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
136
204
|
response.headers.set(
|
|
@@ -158,16 +226,47 @@ export async function PUT(req: Request) {
|
|
|
158
226
|
|
|
159
227
|
export async function DELETE(req: Request) {
|
|
160
228
|
try {
|
|
161
|
-
const { ctx } = await buildContext(req)
|
|
229
|
+
const { ctx, organizationId, tenantId, translate } = await buildContext(req)
|
|
230
|
+
if (!organizationId || !tenantId) {
|
|
231
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
232
|
+
}
|
|
162
233
|
const body = await req.json().catch(() => ({}))
|
|
163
|
-
const { translate } = await resolveTranslations()
|
|
164
234
|
const scoped = withScopedPayload(body, ctx, translate)
|
|
235
|
+
const input = pipelineDeleteSchema.parse(scoped)
|
|
236
|
+
|
|
237
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
238
|
+
tenantId,
|
|
239
|
+
organizationId,
|
|
240
|
+
userId: ctx.auth!.sub,
|
|
241
|
+
resourceKind: PIPELINE_RESOURCE_KIND,
|
|
242
|
+
resourceId: input.id,
|
|
243
|
+
operation: 'delete',
|
|
244
|
+
requestMethod: req.method,
|
|
245
|
+
requestHeaders: req.headers,
|
|
246
|
+
mutationPayload: input,
|
|
247
|
+
})
|
|
248
|
+
if (guardResult && !guardResult.ok) {
|
|
249
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
250
|
+
}
|
|
165
251
|
|
|
166
252
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
167
253
|
await commandBus.execute<PipelineDeleteInput, void>(
|
|
168
254
|
'customers.pipelines.delete',
|
|
169
|
-
{ input
|
|
255
|
+
{ input, ctx },
|
|
170
256
|
)
|
|
257
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
258
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
259
|
+
tenantId,
|
|
260
|
+
organizationId,
|
|
261
|
+
userId: ctx.auth!.sub,
|
|
262
|
+
resourceKind: PIPELINE_RESOURCE_KIND,
|
|
263
|
+
resourceId: input.id,
|
|
264
|
+
operation: 'delete',
|
|
265
|
+
requestMethod: req.method,
|
|
266
|
+
requestHeaders: req.headers,
|
|
267
|
+
metadata: guardResult.metadata ?? null,
|
|
268
|
+
})
|
|
269
|
+
}
|
|
171
270
|
return NextResponse.json({ ok: true })
|
|
172
271
|
} catch (err) {
|
|
173
272
|
if (isCrudHttpError(err)) {
|
|
@@ -7,12 +7,18 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
|
|
|
7
7
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
8
8
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
9
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
|
+
import {
|
|
11
|
+
runCrudMutationGuardAfterSuccess,
|
|
12
|
+
validateCrudMutationGuard,
|
|
13
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
10
14
|
import { customerSettingsUpsertSchema, type CustomerSettingsUpsertInput } from '../../../data/validators'
|
|
11
15
|
import { loadCustomerSettings } from '../../../commands/settings'
|
|
12
16
|
import type { CustomerAddressFormat } from '../../../data/entities'
|
|
13
17
|
import { withScopedPayload } from '../../utils'
|
|
14
18
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
15
19
|
|
|
20
|
+
const SETTINGS_RESOURCE_KIND = 'customers.settings'
|
|
21
|
+
|
|
16
22
|
export const metadata = {
|
|
17
23
|
GET: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },
|
|
18
24
|
PUT: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },
|
|
@@ -78,17 +84,46 @@ export async function GET(req: Request) {
|
|
|
78
84
|
|
|
79
85
|
export async function PUT(req: Request) {
|
|
80
86
|
try {
|
|
81
|
-
const { ctx, translate } = await resolveSettingsContext(req)
|
|
87
|
+
const { ctx, tenantId, organizationId, translate } = await resolveSettingsContext(req)
|
|
82
88
|
const payload = await req.json().catch(() => ({}))
|
|
83
89
|
const scoped = withScopedPayload(payload, ctx, translate)
|
|
84
90
|
const input = customerSettingsUpsertSchema.parse(scoped)
|
|
85
91
|
|
|
92
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
93
|
+
tenantId,
|
|
94
|
+
organizationId,
|
|
95
|
+
userId: ctx.auth!.sub,
|
|
96
|
+
resourceKind: SETTINGS_RESOURCE_KIND,
|
|
97
|
+
resourceId: organizationId,
|
|
98
|
+
operation: 'update',
|
|
99
|
+
requestMethod: req.method,
|
|
100
|
+
requestHeaders: req.headers,
|
|
101
|
+
mutationPayload: input,
|
|
102
|
+
})
|
|
103
|
+
if (guardResult && !guardResult.ok) {
|
|
104
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
87
108
|
const { result } = await commandBus.execute<CustomerSettingsUpsertInput, { settingsId: string; addressFormat: CustomerAddressFormat }>(
|
|
88
109
|
'customers.settings.save',
|
|
89
110
|
{ input, ctx },
|
|
90
111
|
)
|
|
91
112
|
|
|
113
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
114
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
115
|
+
tenantId,
|
|
116
|
+
organizationId,
|
|
117
|
+
userId: ctx.auth!.sub,
|
|
118
|
+
resourceKind: SETTINGS_RESOURCE_KIND,
|
|
119
|
+
resourceId: organizationId,
|
|
120
|
+
operation: 'update',
|
|
121
|
+
requestMethod: req.method,
|
|
122
|
+
requestHeaders: req.headers,
|
|
123
|
+
metadata: guardResult.metadata ?? null,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
92
127
|
return NextResponse.json({
|
|
93
128
|
addressFormat: result?.addressFormat ?? input.addressFormat,
|
|
94
129
|
})
|
|
@@ -9,8 +9,14 @@ import { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/vali
|
|
|
9
9
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
10
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
11
|
import { withScopedPayload } from '../../utils'
|
|
12
|
+
import {
|
|
13
|
+
runCrudMutationGuardAfterSuccess,
|
|
14
|
+
validateCrudMutationGuard,
|
|
15
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
12
16
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
13
17
|
|
|
18
|
+
const TAG_ASSIGNMENT_RESOURCE_KIND = 'customers.tagAssignment'
|
|
19
|
+
|
|
14
20
|
export const metadata = {
|
|
15
21
|
POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },
|
|
16
22
|
}
|
|
@@ -37,15 +43,48 @@ async function buildContext(
|
|
|
37
43
|
export async function POST(req: Request) {
|
|
38
44
|
try {
|
|
39
45
|
const { ctx, auth, translate } = await buildContext(req)
|
|
46
|
+
const tenantId = auth!.tenantId
|
|
47
|
+
const organizationId = ctx.selectedOrganizationId
|
|
48
|
+
if (!tenantId || !organizationId) {
|
|
49
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
50
|
+
}
|
|
40
51
|
const body = await req.json().catch(() => ({}))
|
|
41
52
|
const scoped = withScopedPayload(body, ctx, translate)
|
|
42
53
|
const input = tagAssignmentSchema.parse(scoped)
|
|
43
54
|
|
|
55
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
56
|
+
tenantId,
|
|
57
|
+
organizationId,
|
|
58
|
+
userId: auth!.sub,
|
|
59
|
+
resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
|
|
60
|
+
resourceId: input.entityId,
|
|
61
|
+
operation: 'custom',
|
|
62
|
+
requestMethod: req.method,
|
|
63
|
+
requestHeaders: req.headers,
|
|
64
|
+
mutationPayload: input,
|
|
65
|
+
})
|
|
66
|
+
if (guardResult && !guardResult.ok) {
|
|
67
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
45
71
|
const { result, logEntry } = await commandBus.execute<TagAssignmentInput, { assignmentId: string }>(
|
|
46
72
|
'customers.tags.assign',
|
|
47
73
|
{ input, ctx },
|
|
48
74
|
)
|
|
75
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
76
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
77
|
+
tenantId,
|
|
78
|
+
organizationId,
|
|
79
|
+
userId: auth!.sub,
|
|
80
|
+
resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
|
|
81
|
+
resourceId: input.entityId,
|
|
82
|
+
operation: 'custom',
|
|
83
|
+
requestMethod: req.method,
|
|
84
|
+
requestHeaders: req.headers,
|
|
85
|
+
metadata: guardResult.metadata ?? null,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
49
88
|
const response = NextResponse.json({ id: result?.assignmentId ?? null }, { status: 201 })
|
|
50
89
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
51
90
|
response.headers.set(
|
|
@@ -9,8 +9,14 @@ import { tagAssignmentSchema, type TagAssignmentInput } from '../../../data/vali
|
|
|
9
9
|
import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
10
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
11
|
import { withScopedPayload } from '../../utils'
|
|
12
|
+
import {
|
|
13
|
+
runCrudMutationGuardAfterSuccess,
|
|
14
|
+
validateCrudMutationGuard,
|
|
15
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
12
16
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
13
17
|
|
|
18
|
+
const TAG_ASSIGNMENT_RESOURCE_KIND = 'customers.tagAssignment'
|
|
19
|
+
|
|
14
20
|
export const metadata = {
|
|
15
21
|
POST: { requireAuth: true, requireFeatures: ['customers.activities.manage'] },
|
|
16
22
|
}
|
|
@@ -37,15 +43,48 @@ async function buildContext(
|
|
|
37
43
|
export async function POST(req: Request) {
|
|
38
44
|
try {
|
|
39
45
|
const { ctx, auth, translate } = await buildContext(req)
|
|
46
|
+
const tenantId = auth!.tenantId
|
|
47
|
+
const organizationId = ctx.selectedOrganizationId
|
|
48
|
+
if (!tenantId || !organizationId) {
|
|
49
|
+
return NextResponse.json({ error: translate('customers.errors.context_required', 'Organization and tenant context required') }, { status: 400 })
|
|
50
|
+
}
|
|
40
51
|
const body = await req.json().catch(() => ({}))
|
|
41
52
|
const scoped = withScopedPayload(body, ctx, translate)
|
|
42
53
|
const input = tagAssignmentSchema.parse(scoped)
|
|
43
54
|
|
|
55
|
+
const guardResult = await validateCrudMutationGuard(ctx.container, {
|
|
56
|
+
tenantId,
|
|
57
|
+
organizationId,
|
|
58
|
+
userId: auth!.sub,
|
|
59
|
+
resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
|
|
60
|
+
resourceId: input.entityId,
|
|
61
|
+
operation: 'custom',
|
|
62
|
+
requestMethod: req.method,
|
|
63
|
+
requestHeaders: req.headers,
|
|
64
|
+
mutationPayload: input,
|
|
65
|
+
})
|
|
66
|
+
if (guardResult && !guardResult.ok) {
|
|
67
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
45
71
|
const { result, logEntry } = await commandBus.execute<TagAssignmentInput, { assignmentId: string | null }>(
|
|
46
72
|
'customers.tags.unassign',
|
|
47
73
|
{ input, ctx },
|
|
48
74
|
)
|
|
75
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
76
|
+
await runCrudMutationGuardAfterSuccess(ctx.container, {
|
|
77
|
+
tenantId,
|
|
78
|
+
organizationId,
|
|
79
|
+
userId: auth!.sub,
|
|
80
|
+
resourceKind: TAG_ASSIGNMENT_RESOURCE_KIND,
|
|
81
|
+
resourceId: input.entityId,
|
|
82
|
+
operation: 'custom',
|
|
83
|
+
requestMethod: req.method,
|
|
84
|
+
requestHeaders: req.headers,
|
|
85
|
+
metadata: guardResult.metadata ?? null,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
49
88
|
const response = NextResponse.json({ id: result?.assignmentId ?? null })
|
|
50
89
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
51
90
|
response.headers.set(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { format } from 'date-fns/format'
|
|
5
4
|
import { CalendarRange, ListFilter, Settings } from 'lucide-react'
|
|
6
5
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
7
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
@@ -18,7 +17,8 @@ import {
|
|
|
18
17
|
SelectTrigger,
|
|
19
18
|
SelectValue,
|
|
20
19
|
} from '@open-mercato/ui/primitives/select'
|
|
21
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
20
|
+
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
21
|
+
import { formatDateRangeLabel } from '../../lib/calendar/format'
|
|
22
22
|
import type {
|
|
23
23
|
CalendarFiltersValue,
|
|
24
24
|
CalendarRangePreset,
|
|
@@ -33,10 +33,6 @@ const ALL_OPTION = 'all'
|
|
|
33
33
|
|
|
34
34
|
const EMPTY_FILTERS: CalendarFiltersValue = { types: [], status: null, ownerUserId: null }
|
|
35
35
|
|
|
36
|
-
function formatRangeLabel(from: Date, to: Date): string {
|
|
37
|
-
return `${format(from, 'MMM dd')} – ${format(to, 'MMM dd, yyyy')}`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
36
|
export function CalendarToolbar(props: CalendarToolbarProps) {
|
|
41
37
|
const {
|
|
42
38
|
anchor,
|
|
@@ -54,6 +50,7 @@ export function CalendarToolbar(props: CalendarToolbarProps) {
|
|
|
54
50
|
onOpenSettings,
|
|
55
51
|
} = props
|
|
56
52
|
const t = useT()
|
|
53
|
+
const locale = useLocale()
|
|
57
54
|
const [rangeOpen, setRangeOpen] = React.useState(false)
|
|
58
55
|
const [filtersOpen, setFiltersOpen] = React.useState(false)
|
|
59
56
|
const [pendingFilters, setPendingFilters] = React.useState<CalendarFiltersValue>(filters)
|
|
@@ -128,7 +125,7 @@ export function CalendarToolbar(props: CalendarToolbarProps) {
|
|
|
128
125
|
className="min-w-0 text-muted-foreground sm:-ml-px sm:rounded-l-none"
|
|
129
126
|
>
|
|
130
127
|
<CalendarRange aria-hidden="true" />
|
|
131
|
-
<span className="truncate">{
|
|
128
|
+
<span className="truncate">{formatDateRangeLabel(locale, range.from, range.to)}</span>
|
|
132
129
|
</Button>
|
|
133
130
|
</PopoverTrigger>
|
|
134
131
|
<PopoverContent align="start" className="w-auto min-w-0 p-2">
|
|
@@ -6,6 +6,7 @@ import { cn } from '@open-mercato/shared/lib/utils'
|
|
|
6
6
|
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'
|
|
8
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
9
|
+
import { formatTimeRangeLabel } from '../../lib/calendar/format'
|
|
9
10
|
import type { CalendarItem, CalendarPlatform } from './types'
|
|
10
11
|
|
|
11
12
|
const SHOW_TIME_MIN_HEIGHT_PX = 44
|
|
@@ -63,12 +64,7 @@ export function resolveEventTone(item: CalendarItem, nowMs: number): EventTone {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export function formatTimeRange(locale: string, start: Date, end: Date): string {
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
return formatter.formatRange(start, end)
|
|
69
|
-
} catch {
|
|
70
|
-
return `${formatter.format(start)} - ${formatter.format(end)}`
|
|
71
|
-
}
|
|
67
|
+
return formatTimeRangeLabel(locale, start, end)
|
|
72
68
|
}
|
|
73
69
|
|
|
74
70
|
function participantLabel(participant: CalendarItem['participants'][number]): string {
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { differenceInCalendarDays } from 'date-fns/differenceInCalendarDays'
|
|
5
|
-
import { format } from 'date-fns/format'
|
|
6
5
|
import { AlertTriangle, ChevronDown, Clock } from 'lucide-react'
|
|
7
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
7
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
@@ -13,12 +12,13 @@ import {
|
|
|
13
12
|
PopoverContent,
|
|
14
13
|
PopoverTrigger,
|
|
15
14
|
} from '@open-mercato/ui/primitives/popover'
|
|
16
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
|
+
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
16
|
+
import { formatDateLabel, formatTimeRangeLabel } from '../../lib/calendar/format'
|
|
17
17
|
import type { CalendarItem, UpcomingCard, UpcomingCardsProps } from './types'
|
|
18
18
|
|
|
19
|
-
function formatTimeRange(item: CalendarItem, allDayLabel: string): string {
|
|
19
|
+
function formatTimeRange(locale: string, item: CalendarItem, allDayLabel: string): string {
|
|
20
20
|
if (item.allDay) return allDayLabel
|
|
21
|
-
return
|
|
21
|
+
return formatTimeRangeLabel(locale, item.start, item.end)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function canJoin(item: CalendarItem): boolean {
|
|
@@ -37,6 +37,7 @@ function UpcomingCardStatus({
|
|
|
37
37
|
onSeeConflict: UpcomingCardsProps['onSeeConflict']
|
|
38
38
|
}) {
|
|
39
39
|
const t = useT()
|
|
40
|
+
const locale = useLocale()
|
|
40
41
|
const { item, kind, conflictCount } = card
|
|
41
42
|
|
|
42
43
|
if (kind === 'today') {
|
|
@@ -91,7 +92,7 @@ function UpcomingCardStatus({
|
|
|
91
92
|
{t('customers.calendar.cards.cancelled', 'Cancelled')}
|
|
92
93
|
</span>
|
|
93
94
|
<span className="shrink-0 text-xs text-muted-foreground">
|
|
94
|
-
{
|
|
95
|
+
{formatDateLabel(locale, item.start)}
|
|
95
96
|
</span>
|
|
96
97
|
</div>
|
|
97
98
|
)
|
|
@@ -107,7 +108,7 @@ function UpcomingCardStatus({
|
|
|
107
108
|
: t('customers.calendar.cards.daysLater', '{days} days later', { days: daysLater })}
|
|
108
109
|
</span>
|
|
109
110
|
<span className="shrink-0 text-xs text-muted-foreground">
|
|
110
|
-
{
|
|
111
|
+
{formatDateLabel(locale, item.start)}
|
|
111
112
|
</span>
|
|
112
113
|
</div>
|
|
113
114
|
)
|
|
@@ -123,6 +124,7 @@ function UpcomingCardItem({
|
|
|
123
124
|
onCancel,
|
|
124
125
|
}: { card: UpcomingCard } & CardCallbacks) {
|
|
125
126
|
const t = useT()
|
|
127
|
+
const locale = useLocale()
|
|
126
128
|
const { item } = card
|
|
127
129
|
|
|
128
130
|
return (
|
|
@@ -131,7 +133,7 @@ function UpcomingCardItem({
|
|
|
131
133
|
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
132
134
|
<p className="truncate text-sm font-medium text-foreground">{item.title}</p>
|
|
133
135
|
<p className="truncate text-xs text-muted-foreground">
|
|
134
|
-
{formatTimeRange(item, t('customers.calendar.cards.allDay', 'All day'))}
|
|
136
|
+
{formatTimeRange(locale, item, t('customers.calendar.cards.allDay', 'All day'))}
|
|
135
137
|
</p>
|
|
136
138
|
</div>
|
|
137
139
|
<Popover>
|
|
@@ -1505,6 +1505,7 @@
|
|
|
1505
1505
|
"customers.errors.company_not_found": "Unternehmen nicht gefunden",
|
|
1506
1506
|
"customers.errors.company_people_load_failed": "Verknüpfte Personen konnten nicht geladen werden",
|
|
1507
1507
|
"customers.errors.company_required": "Unternehmens-ID ist erforderlich",
|
|
1508
|
+
"customers.errors.context_required": "Organisations- und Mandantenkontext erforderlich",
|
|
1508
1509
|
"customers.errors.customer_label_tables_missing": "Kundenlabel-Tabellen fehlen. Führen Sie yarn db:migrate aus.",
|
|
1509
1510
|
"customers.errors.customer_not_found": "Kunde nicht gefunden",
|
|
1510
1511
|
"customers.errors.deal_companies_load_failed": "Verknüpfte Unternehmen konnten nicht geladen werden",
|
|
@@ -1505,6 +1505,7 @@
|
|
|
1505
1505
|
"customers.errors.company_not_found": "Company not found",
|
|
1506
1506
|
"customers.errors.company_people_load_failed": "Failed to load linked people",
|
|
1507
1507
|
"customers.errors.company_required": "Company id is required",
|
|
1508
|
+
"customers.errors.context_required": "Organization and tenant context required",
|
|
1508
1509
|
"customers.errors.customer_label_tables_missing": "Customer label tables are missing. Run yarn db:migrate.",
|
|
1509
1510
|
"customers.errors.customer_not_found": "Customer not found",
|
|
1510
1511
|
"customers.errors.deal_companies_load_failed": "Failed to load linked companies",
|
|
@@ -1505,6 +1505,7 @@
|
|
|
1505
1505
|
"customers.errors.company_not_found": "Empresa no encontrada",
|
|
1506
1506
|
"customers.errors.company_people_load_failed": "No se pudieron cargar las personas vinculadas",
|
|
1507
1507
|
"customers.errors.company_required": "Se requiere el ID de la empresa",
|
|
1508
|
+
"customers.errors.context_required": "Se requiere contexto de organización e inquilino",
|
|
1508
1509
|
"customers.errors.customer_label_tables_missing": "Faltan las tablas de etiquetas de cliente. Ejecute yarn db:migrate.",
|
|
1509
1510
|
"customers.errors.customer_not_found": "Cliente no encontrado",
|
|
1510
1511
|
"customers.errors.deal_companies_load_failed": "No se pudieron cargar las empresas vinculadas",
|
|
@@ -1505,6 +1505,7 @@
|
|
|
1505
1505
|
"customers.errors.company_not_found": "Nie znaleziono firmy",
|
|
1506
1506
|
"customers.errors.company_people_load_failed": "Nie udało się załadować powiązanych osób",
|
|
1507
1507
|
"customers.errors.company_required": "Wymagany identyfikator firmy",
|
|
1508
|
+
"customers.errors.context_required": "Wymagany jest kontekst organizacji i najemcy",
|
|
1508
1509
|
"customers.errors.customer_label_tables_missing": "Brakuje tabel etykiet klientów. Uruchom yarn db:migrate.",
|
|
1509
1510
|
"customers.errors.customer_not_found": "Nie znaleziono klienta",
|
|
1510
1511
|
"customers.errors.deal_companies_load_failed": "Nie udało się załadować powiązanych firm",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const DATE_OPTIONS: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }
|
|
2
|
+
|
|
3
|
+
const TIME_OPTIONS: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: '2-digit' }
|
|
4
|
+
|
|
5
|
+
export function formatDateLabel(locale: string, date: Date): string {
|
|
6
|
+
return new Intl.DateTimeFormat(locale, DATE_OPTIONS).format(date)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatDateRangeLabel(locale: string, from: Date, to: Date): string {
|
|
10
|
+
const formatter = new Intl.DateTimeFormat(locale, DATE_OPTIONS)
|
|
11
|
+
try {
|
|
12
|
+
return formatter.formatRange(from, to)
|
|
13
|
+
} catch {
|
|
14
|
+
return `${formatter.format(from)} – ${formatter.format(to)}`
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatTimeRangeLabel(locale: string, start: Date, end: Date): string {
|
|
19
|
+
const formatter = new Intl.DateTimeFormat(locale, TIME_OPTIONS)
|
|
20
|
+
try {
|
|
21
|
+
return formatter.formatRange(start, end)
|
|
22
|
+
} catch {
|
|
23
|
+
return `${formatter.format(start)} – ${formatter.format(end)}`
|
|
24
|
+
}
|
|
25
|
+
}
|