@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.
Files changed (137) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/commands/users.js +1 -0
  3. package/dist/modules/auth/commands/users.js.map +2 -2
  4. package/dist/modules/communication_channels/data/enrichers.js +9 -1
  5. package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
  6. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
  7. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
  8. package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
  9. package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
  10. package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
  11. package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
  12. package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
  13. package/dist/modules/customers/api/pipelines/route.js +107 -12
  14. package/dist/modules/customers/api/pipelines/route.js.map +2 -2
  15. package/dist/modules/customers/api/settings/address-format/route.js +33 -1
  16. package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
  17. package/dist/modules/customers/api/tags/assign/route.js +37 -0
  18. package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
  19. package/dist/modules/customers/api/tags/unassign/route.js +37 -0
  20. package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
  21. package/dist/modules/customers/components/calendar/CalendarToolbar.js +4 -6
  22. package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
  23. package/dist/modules/customers/components/calendar/EventBlock.js +2 -6
  24. package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
  25. package/dist/modules/customers/components/calendar/UpcomingCards.js +9 -7
  26. package/dist/modules/customers/components/calendar/UpcomingCards.js.map +2 -2
  27. package/dist/modules/customers/lib/calendar/format.js +27 -0
  28. package/dist/modules/customers/lib/calendar/format.js.map +7 -0
  29. package/dist/modules/entities/api/encryption.js +109 -38
  30. package/dist/modules/entities/api/encryption.js.map +2 -2
  31. package/dist/modules/entities/components/EncryptionManager.js +22 -9
  32. package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
  33. package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
  34. package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
  35. package/dist/modules/integrations/api/[id]/route.js +10 -3
  36. package/dist/modules/integrations/api/[id]/route.js.map +2 -2
  37. package/dist/modules/integrations/api/[id]/state/route.js +20 -5
  38. package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
  39. package/dist/modules/integrations/api/[id]/version/route.js +17 -1
  40. package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
  41. package/dist/modules/integrations/api/route.js +2 -0
  42. package/dist/modules/integrations/api/route.js.map +2 -2
  43. package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
  44. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  45. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
  46. package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
  47. package/dist/modules/integrations/backend/integrations/page.js +4 -3
  48. package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
  49. package/dist/modules/integrations/data/entities.js +2 -2
  50. package/dist/modules/integrations/data/entities.js.map +2 -2
  51. package/dist/modules/integrations/lib/credentials-service.js +32 -0
  52. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  53. package/dist/modules/integrations/lib/state-service.js +4 -2
  54. package/dist/modules/integrations/lib/state-service.js.map +2 -2
  55. package/dist/modules/notifications/lib/notificationService.js +42 -10
  56. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  57. package/dist/modules/payment_gateways/api/status/route.js +91 -6
  58. package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
  59. package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
  60. package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
  61. package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
  62. package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
  63. package/dist/modules/sales/backend/sales/channels/page.js +25 -7
  64. package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
  65. package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
  66. package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
  67. package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
  68. package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
  69. package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
  70. package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
  71. package/dist/modules/sales/components/StatusSettings.js +66 -31
  72. package/dist/modules/sales/components/StatusSettings.js.map +2 -2
  73. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
  74. package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
  75. package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
  76. package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
  77. package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
  78. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  79. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +46 -13
  80. package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
  81. package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
  82. package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
  83. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +43 -8
  84. package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
  85. package/dist/modules/workflows/backend/definitions/page.js +19 -10
  86. package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
  87. package/package.json +7 -7
  88. package/src/modules/auth/commands/users.ts +1 -0
  89. package/src/modules/communication_channels/data/enrichers.ts +25 -0
  90. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
  91. package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
  92. package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
  93. package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
  94. package/src/modules/customers/api/pipelines/route.ts +112 -13
  95. package/src/modules/customers/api/settings/address-format/route.ts +36 -1
  96. package/src/modules/customers/api/tags/assign/route.ts +39 -0
  97. package/src/modules/customers/api/tags/unassign/route.ts +39 -0
  98. package/src/modules/customers/components/calendar/CalendarToolbar.tsx +4 -7
  99. package/src/modules/customers/components/calendar/EventBlock.tsx +2 -6
  100. package/src/modules/customers/components/calendar/UpcomingCards.tsx +9 -7
  101. package/src/modules/customers/i18n/de.json +1 -0
  102. package/src/modules/customers/i18n/en.json +1 -0
  103. package/src/modules/customers/i18n/es.json +1 -0
  104. package/src/modules/customers/i18n/pl.json +1 -0
  105. package/src/modules/customers/lib/calendar/format.ts +25 -0
  106. package/src/modules/entities/api/encryption.ts +122 -39
  107. package/src/modules/entities/components/EncryptionManager.tsx +28 -9
  108. package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
  109. package/src/modules/integrations/api/[id]/route.ts +8 -1
  110. package/src/modules/integrations/api/[id]/state/route.ts +20 -4
  111. package/src/modules/integrations/api/[id]/version/route.ts +18 -1
  112. package/src/modules/integrations/api/route.ts +3 -0
  113. package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
  114. package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
  115. package/src/modules/integrations/backend/integrations/page.tsx +8 -5
  116. package/src/modules/integrations/data/entities.ts +2 -2
  117. package/src/modules/integrations/lib/credentials-service.ts +35 -0
  118. package/src/modules/integrations/lib/state-service.ts +3 -0
  119. package/src/modules/notifications/lib/notificationService.ts +74 -11
  120. package/src/modules/payment_gateways/api/status/route.ts +97 -5
  121. package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
  122. package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
  123. package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
  124. package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
  125. package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
  126. package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
  127. package/src/modules/sales/components/StatusSettings.tsx +74 -33
  128. package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
  129. package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
  130. package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
  131. package/src/modules/staff/i18n/de.json +5 -5
  132. package/src/modules/staff/i18n/es.json +5 -5
  133. package/src/modules/staff/i18n/pl.json +5 -5
  134. package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +57 -13
  135. package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
  136. package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +57 -9
  137. 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: pipelineCreateSchema.parse(scoped), ctx },
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: pipelineUpdateSchema.parse(scoped), ctx },
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: pipelineDeleteSchema.parse(scoped), ctx },
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">{formatRangeLabel(range.from, range.to)}</span>
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
- const formatter = new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit' })
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 `${format(item.start, 'h:mm a')} – ${format(item.end, 'h:mm a')}`
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
- {format(item.start, 'MMM dd, yyyy')}
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
- {format(item.start, 'MMM dd, yyyy')}
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
+ }