@open-mercato/core 0.6.2-develop.3467.1.2a1818709d → 0.6.3-develop.3484.1.1427e58147

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 (38) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +13 -5
  3. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  4. package/dist/modules/integrations/data/validators.js +9 -6
  5. package/dist/modules/integrations/data/validators.js.map +2 -2
  6. package/dist/modules/messages/commands/messages.js +42 -2
  7. package/dist/modules/messages/commands/messages.js.map +2 -2
  8. package/dist/modules/messages/components/MessagesInboxPageClient.js +42 -86
  9. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  10. package/dist/modules/messages/components/inboxFilters.js +133 -0
  11. package/dist/modules/messages/components/inboxFilters.js.map +7 -0
  12. package/dist/modules/messages/data/validators.js +2 -1
  13. package/dist/modules/messages/data/validators.js.map +2 -2
  14. package/dist/modules/sales/api/order-adjustments/route.js.map +2 -2
  15. package/dist/modules/sales/api/quote-adjustments/route.js.map +2 -2
  16. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
  17. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  18. package/jest.config.cjs +3 -1
  19. package/package.json +15 -15
  20. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +18 -7
  21. package/src/modules/integrations/data/validators.ts +5 -4
  22. package/src/modules/messages/commands/messages.ts +54 -2
  23. package/src/modules/messages/components/MessagesInboxPageClient.tsx +46 -89
  24. package/src/modules/messages/components/inboxFilters.ts +162 -0
  25. package/src/modules/messages/data/validators.ts +1 -0
  26. package/src/modules/messages/i18n/de.json +5 -4
  27. package/src/modules/messages/i18n/en.json +5 -4
  28. package/src/modules/messages/i18n/es.json +5 -4
  29. package/src/modules/messages/i18n/pl.json +5 -4
  30. package/src/modules/sales/api/order-adjustments/route.ts +2 -1
  31. package/src/modules/sales/api/quote-adjustments/route.ts +2 -1
  32. package/src/modules/sales/lib/makeSalesLineRoute.ts +2 -1
  33. package/src/modules/staff/i18n/de.json +1 -0
  34. package/src/modules/staff/i18n/en.json +1 -0
  35. package/src/modules/staff/i18n/es.json +1 -0
  36. package/src/modules/staff/i18n/pl.json +1 -0
  37. package/src/modules/workflows/lib/activity-executor.ts +4 -3
  38. package/tsconfig.json +1 -0
@@ -1,5 +1,4 @@
1
1
  import * as React from 'react'
2
- import { format } from 'date-fns'
3
2
  import type { ActivityType } from './fieldConfig'
4
3
 
5
4
  export type RsvpStatus = 'pending' | 'accepted' | 'declined' | 'tentative'
@@ -63,6 +62,18 @@ const DEFAULT_REMINDER_MINUTES: Record<ActivityType, number> = {
63
62
  email: 15,
64
63
  }
65
64
 
65
+ function padDatePart(value: number): string {
66
+ return String(value).padStart(2, '0')
67
+ }
68
+
69
+ function formatLocalDateInput(date: Date): string {
70
+ return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`
71
+ }
72
+
73
+ function formatLocalTimeInput(date: Date): string {
74
+ return `${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`
75
+ }
76
+
66
77
  interface UseScheduleFormStateParams {
67
78
  open: boolean
68
79
  editData: ScheduleActivityEditData | null | undefined
@@ -71,7 +82,7 @@ interface UseScheduleFormStateParams {
71
82
  export function useScheduleFormState({ open, editData }: UseScheduleFormStateParams) {
72
83
  const [activityType, setActivityType] = React.useState<ActivityType>('meeting')
73
84
  const [title, setTitle] = React.useState('')
74
- const [date, setDate] = React.useState(() => format(new Date(), 'yyyy-MM-dd'))
85
+ const [date, setDate] = React.useState(() => formatLocalDateInput(new Date()))
75
86
  const [startTime, setStartTime] = React.useState('10:00')
76
87
  const [duration, setDuration] = React.useState(30)
77
88
  const [allDay, setAllDay] = React.useState(false)
@@ -101,15 +112,15 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
101
112
  // For historical activities the canonical timestamp is `occurredAt`; for
102
113
  // planned/future ones it's `scheduledAt`. Without this fallback editing a
103
114
  // past activity prefilled to "today" instead of its actual moment (#1807).
104
- // Use `date-fns` `format(...)` so the seed values are in the user's local
105
- // timezone (matches the cluster-E local-day convention).
115
+ // Keep seed values in the user's local timezone, matching the cluster-E
116
+ // local-day convention.
106
117
  const sourceTimestamp = editData.occurredAt ?? editData.scheduledAt ?? null
107
118
  const seedDate = sourceTimestamp ? new Date(sourceTimestamp) : new Date()
108
119
  const seedDateValid = !Number.isNaN(seedDate.getTime())
109
120
  const fallbackNow = new Date()
110
121
  const dateForForm = seedDateValid ? seedDate : fallbackNow
111
- setDate(format(dateForForm, 'yyyy-MM-dd'))
112
- setStartTime(format(dateForForm, 'HH:mm'))
122
+ setDate(formatLocalDateInput(dateForForm))
123
+ setStartTime(formatLocalTimeInput(dateForForm))
113
124
  setDuration(editData.durationMinutes ?? 30)
114
125
  setAllDay(editData.allDay ?? false)
115
126
  setDescription(editData.body ?? '')
@@ -170,7 +181,7 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
170
181
  // Create mode: reset all fields
171
182
  setActivityType('meeting')
172
183
  setTitle('')
173
- setDate(format(new Date(), 'yyyy-MM-dd'))
184
+ setDate(formatLocalDateInput(new Date()))
174
185
  setStartTime('10:00')
175
186
  setDuration(30)
176
187
  setAllDay(false)
@@ -40,14 +40,15 @@ export const listIntegrationLogsQuerySchema = z.object({
40
40
 
41
41
  export type ListIntegrationLogsQuery = z.infer<typeof listIntegrationLogsQuerySchema>
42
42
 
43
- const optionalBooleanQuery = z.union([z.boolean(), z.string(), z.null(), z.undefined()])
44
- .transform((value) => {
43
+ const optionalBooleanQuery = z.preprocess(
44
+ (value) => {
45
45
  if (value === undefined || value === '' || value === null) return undefined
46
46
  if (value === true || value === 'true' || value === '1') return true
47
47
  if (value === false || value === 'false' || value === '0') return false
48
48
  return value
49
- })
50
- .pipe(z.boolean().optional())
49
+ },
50
+ z.boolean().optional(),
51
+ )
51
52
 
52
53
  export const integrationMarketplaceHealthStatusSchema = z.enum(['healthy', 'degraded', 'unhealthy', 'unconfigured'])
53
54
 
@@ -2,7 +2,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { z } from 'zod'
3
3
  import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
4
4
  import { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'
5
- import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
6
6
  import { Message, MessageObject, MessageRecipient, type MessageActionData } from '../data/entities'
7
7
  import { emitMessagesEvent } from '../events'
8
8
  import {
@@ -368,6 +368,20 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
368
368
  if (message.senderUserId !== input.userId) throw new Error('Access denied')
369
369
  if (!message.isDraft) throw new Error('Only draft messages can be edited')
370
370
 
371
+ const isSending = input.isDraft === false
372
+ const preloadedRecipients = isSending && !input.recipients
373
+ ? await findWithDecryption(
374
+ em,
375
+ MessageRecipient,
376
+ { messageId: message.id, deletedAt: null },
377
+ undefined,
378
+ {
379
+ tenantId: input.tenantId,
380
+ organizationId: input.organizationId,
381
+ },
382
+ )
383
+ : null
384
+
371
385
  const nextMessageType = input.type ?? message.type
372
386
  if (input.objects) {
373
387
  const objectValidationError = validateMessageObjectsForType(nextMessageType, input.objects)
@@ -454,12 +468,50 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
454
468
  )
455
469
  }
456
470
 
471
+ if (isSending) {
472
+ const finalVisibility = input.visibility ?? message.visibility
473
+ const finalSubject = input.subject ?? message.subject
474
+ const finalBody = input.body ?? message.body
475
+ const finalRecipientCount = input.recipients
476
+ ? input.recipients.length
477
+ : (preloadedRecipients?.length ?? 0)
478
+
479
+ if (finalVisibility !== 'public' && finalRecipientCount === 0) {
480
+ throw new Error('at least one recipient is required')
481
+ }
482
+ if (!finalSubject?.trim()) throw new Error('subject is required')
483
+ if (!finalBody?.trim()) throw new Error('body is required')
484
+
485
+ message.isDraft = false
486
+ message.status = 'sent'
487
+ message.sentAt = new Date()
488
+ if (!message.threadId) message.threadId = message.id
489
+ }
490
+
457
491
  await em.flush()
458
492
  await emitMessageIndexUpsert(ctx.container, {
459
493
  messageId: message.id,
460
494
  tenantId: input.tenantId,
461
495
  organizationId: input.organizationId,
462
496
  })
497
+
498
+ if (isSending) {
499
+ const recipientUserIds = input.recipients
500
+ ? input.recipients.map((r) => r.userId)
501
+ : (preloadedRecipients ?? []).map((r) => r.recipientUserId)
502
+ const resolvedVisibility = input.visibility ?? message.visibility
503
+ const resolvedSendViaEmail = input.sendViaEmail ?? message.sendViaEmail
504
+ await emitMessageSentEvent(ctx.container, {
505
+ messageId: message.id,
506
+ senderUserId: input.userId,
507
+ recipientUserIds,
508
+ sendViaEmail: resolvedVisibility === 'public' ? true : resolvedSendViaEmail,
509
+ externalEmail: message.externalEmail ?? null,
510
+ tenantId: input.tenantId,
511
+ organizationId: input.organizationId,
512
+ })
513
+ }
514
+
463
515
  return { ok: true, id: message.id }
464
516
  },
465
517
  async captureAfter(rawInput, _result, ctx) {
@@ -473,7 +525,7 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
473
525
  buildLog: async ({ input, snapshots }) => {
474
526
  const parsed = updateDraftCommandSchema.parse(input)
475
527
  return {
476
- actionLabel: 'Update draft message',
528
+ actionLabel: parsed.isDraft === false ? 'Send draft message' : 'Update draft message',
477
529
  resourceKind: 'messages.message',
478
530
  resourceId: parsed.messageId,
479
531
  tenantId: parsed.tenantId,
@@ -19,6 +19,11 @@ import { DefaultMessageListItem } from './defaults/DefaultMessageListItem'
19
19
  import { getMessageListParticipantLabel } from './messageListLabels'
20
20
  import { toErrorMessage } from './message-detail/utils'
21
21
  import { useMessagesInboxBulkActions, type MessageFolder } from './useMessagesInboxBulkActions'
22
+ import {
23
+ buildMessagesInboxFilters,
24
+ buildMessagesListParams,
25
+ type SenderOption,
26
+ } from './inboxFilters'
22
27
 
23
28
  type MessageListItem = {
24
29
  id: string
@@ -106,30 +111,13 @@ export function MessagesInboxPageClient() {
106
111
  scopeVersion,
107
112
  ],
108
113
  queryFn: async () => {
109
- const params = new URLSearchParams()
110
- params.set('folder', folder)
111
- params.set('page', String(page))
112
- params.set('pageSize', String(pageSize))
113
-
114
- if (search.trim()) {
115
- params.set('search', search.trim())
116
- }
117
-
118
- const status = typeof filterValues.status === 'string' ? filterValues.status.trim() : ''
119
- const type = typeof filterValues.type === 'string' ? filterValues.type.trim() : ''
120
- const hasObjects = typeof filterValues.hasObjects === 'string' ? filterValues.hasObjects.trim() : ''
121
- const hasAttachments = typeof filterValues.hasAttachments === 'string' ? filterValues.hasAttachments.trim() : ''
122
- const hasActions = typeof filterValues.hasActions === 'string' ? filterValues.hasActions.trim() : ''
123
- const senderId = typeof filterValues.senderId === 'string' ? filterValues.senderId.trim() : ''
124
- const since = typeof filterValues.since === 'string' ? filterValues.since.trim() : ''
125
-
126
- if (status) params.set('status', status)
127
- if (type) params.set('type', type)
128
- if (hasObjects) params.set('hasObjects', hasObjects)
129
- if (hasAttachments) params.set('hasAttachments', hasAttachments)
130
- if (hasActions) params.set('hasActions', hasActions)
131
- if (senderId) params.set('senderId', senderId)
132
- if (since) params.set('since', since)
114
+ const params = buildMessagesListParams({
115
+ folder,
116
+ page,
117
+ pageSize,
118
+ search,
119
+ filterValues,
120
+ })
133
121
 
134
122
  const call = await apiCall<MessageListResponse>(`/api/messages?${params.toString()}`)
135
123
  if (!call.ok) {
@@ -191,6 +179,19 @@ export function MessagesInboxPageClient() {
191
179
  return map
192
180
  }, [messageTypesQuery.data, t])
193
181
 
182
+ const [senderOptions, setSenderOptions] = React.useState<SenderOption[]>([])
183
+ const senderOptionsScopeRef = React.useRef(scopeVersion)
184
+
185
+ const mergeSenderOptions = React.useCallback((incoming: SenderOption[]) => {
186
+ if (incoming.length === 0) return
187
+ setSenderOptions((prev) => {
188
+ const map = new Map<string, SenderOption>()
189
+ for (const opt of prev) map.set(opt.value, opt)
190
+ for (const opt of incoming) map.set(opt.value, opt)
191
+ return Array.from(map.values())
192
+ })
193
+ }, [])
194
+
194
195
  const loadSenderOptions = React.useCallback(async (query?: string) => {
195
196
  const params = new URLSearchParams()
196
197
  params.set('page', '1')
@@ -203,7 +204,7 @@ export function MessagesInboxPageClient() {
203
204
  if (!call.ok) return []
204
205
 
205
206
  const items = Array.isArray(call.result?.items) ? call.result?.items ?? [] : []
206
- return items.flatMap((item) => {
207
+ const next: SenderOption[] = items.flatMap((item) => {
207
208
  if (!item || typeof item.id !== 'string' || item.id.trim().length === 0) return []
208
209
  const name = typeof item.name === 'string' && item.name.trim().length > 0 ? item.name.trim() : null
209
210
  const email = typeof item.email === 'string' && item.email.trim().length > 0 ? item.email.trim() : null
@@ -214,7 +215,19 @@ export function MessagesInboxPageClient() {
214
215
  description: email && email !== label ? email : null,
215
216
  }]
216
217
  })
217
- }, [])
218
+ if (senderOptionsScopeRef.current === scopeVersion) {
219
+ mergeSenderOptions(next)
220
+ }
221
+ return next
222
+ }, [mergeSenderOptions, scopeVersion])
223
+
224
+ React.useEffect(() => {
225
+ senderOptionsScopeRef.current = scopeVersion
226
+ setSenderOptions([])
227
+ loadSenderOptions().catch((error: unknown) => {
228
+ console.warn('[messages] Failed to load sender filter options', error)
229
+ })
230
+ }, [loadSenderOptions, scopeVersion])
218
231
 
219
232
  const listItemComponentKeyByType = React.useMemo(() => {
220
233
  const map: Record<string, string | null> = {}
@@ -230,69 +243,13 @@ export function MessagesInboxPageClient() {
230
243
  label: t(item.labelKey, item.type),
231
244
  }))
232
245
 
233
- return [
234
- {
235
- id: 'status',
236
- label: t('messages.filters.status', 'Status'),
237
- type: 'select',
238
- options: [
239
- { value: '', label: t('messages.filters.all', 'All') },
240
- { value: 'unread', label: t('messages.status.unread', 'Unread') },
241
- { value: 'read', label: t('messages.status.read', 'Read') },
242
- { value: 'archived', label: t('messages.status.archived', 'Archived') },
243
- ],
244
- },
245
- {
246
- id: 'type',
247
- label: t('messages.filters.type', 'Type'),
248
- type: 'select',
249
- options: [{ value: '', label: t('messages.filters.all', 'All') }, ...typeOptions],
250
- },
251
- {
252
- id: 'hasObjects',
253
- label: t('messages.filters.hasObjects', 'Objects'),
254
- type: 'select',
255
- options: [
256
- { value: '', label: t('messages.filters.all', 'All') },
257
- { value: 'true', label: t('common.yes', 'Yes') },
258
- { value: 'false', label: t('common.no', 'No') },
259
- ],
260
- },
261
- {
262
- id: 'hasAttachments',
263
- label: t('messages.filters.hasAttachments', 'Attachments'),
264
- type: 'select',
265
- options: [
266
- { value: '', label: t('messages.filters.all', 'All') },
267
- { value: 'true', label: t('common.yes', 'Yes') },
268
- { value: 'false', label: t('common.no', 'No') },
269
- ],
270
- },
271
- {
272
- id: 'hasActions',
273
- label: t('messages.filters.hasActions', 'Actions'),
274
- type: 'select',
275
- options: [
276
- { value: '', label: t('messages.filters.all', 'All') },
277
- { value: 'true', label: t('common.yes', 'Yes') },
278
- { value: 'false', label: t('common.no', 'No') },
279
- ],
280
- },
281
- {
282
- id: 'senderId',
283
- label: t('messages.filters.sender', 'Sender'),
284
- type: 'select',
285
- options: [{ value: '', label: t('messages.filters.all', 'All') }],
286
- loadOptions: loadSenderOptions,
287
- },
288
- {
289
- id: 'since',
290
- label: t('messages.filters.since', 'Sent after'),
291
- type: 'text',
292
- placeholder: t('messages.filters.sincePlaceholder', 'YYYY-MM-DDTHH:mm:ssZ'),
293
- },
294
- ]
295
- }, [loadSenderOptions, messageTypesQuery.data, t])
246
+ return buildMessagesInboxFilters({
247
+ t,
248
+ typeOptions,
249
+ senderOptions,
250
+ loadSenderOptions,
251
+ })
252
+ }, [loadSenderOptions, messageTypesQuery.data, senderOptions, t])
296
253
 
297
254
  const columns = React.useMemo<ColumnDef<MessageListItem>[]>(() => [
298
255
  {
@@ -0,0 +1,162 @@
1
+ import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
2
+
3
+ type Translator = (key: string, fallback?: string) => string
4
+
5
+ export type SenderOption = {
6
+ value: string
7
+ label: string
8
+ description?: string | null
9
+ }
10
+
11
+ export type MessagesInboxFilterContext = {
12
+ t: Translator
13
+ typeOptions: { value: string; label: string }[]
14
+ senderOptions: SenderOption[]
15
+ loadSenderOptions: (query?: string) => Promise<SenderOption[]>
16
+ }
17
+
18
+ export function buildMessagesInboxFilters({
19
+ t,
20
+ typeOptions,
21
+ senderOptions,
22
+ loadSenderOptions,
23
+ }: MessagesInboxFilterContext): FilterDef[] {
24
+ const senderLabel = (val: string): string => {
25
+ const match = senderOptions.find((opt) => opt.value === val)
26
+ return match?.label ?? val
27
+ }
28
+
29
+ return [
30
+ {
31
+ id: 'status',
32
+ label: t('messages.filters.status', 'Status'),
33
+ type: 'select',
34
+ options: [
35
+ { value: '', label: t('messages.filters.all', 'All') },
36
+ { value: 'unread', label: t('messages.status.unread', 'Unread') },
37
+ { value: 'read', label: t('messages.status.read', 'Read') },
38
+ { value: 'archived', label: t('messages.status.archived', 'Archived') },
39
+ ],
40
+ },
41
+ {
42
+ id: 'type',
43
+ label: t('messages.filters.type', 'Type'),
44
+ type: 'select',
45
+ options: [{ value: '', label: t('messages.filters.all', 'All') }, ...typeOptions],
46
+ },
47
+ {
48
+ id: 'hasObjects',
49
+ label: t('messages.filters.hasObjects', 'Has objects'),
50
+ type: 'select',
51
+ options: [
52
+ { value: '', label: t('messages.filters.all', 'All') },
53
+ { value: 'true', label: t('common.yes', 'Yes') },
54
+ { value: 'false', label: t('common.no', 'No') },
55
+ ],
56
+ },
57
+ {
58
+ id: 'hasAttachments',
59
+ label: t('messages.filters.hasAttachments', 'Has attachments'),
60
+ type: 'select',
61
+ options: [
62
+ { value: '', label: t('messages.filters.all', 'All') },
63
+ { value: 'true', label: t('common.yes', 'Yes') },
64
+ { value: 'false', label: t('common.no', 'No') },
65
+ ],
66
+ },
67
+ {
68
+ id: 'hasActions',
69
+ label: t('messages.filters.hasActions', 'Has actions'),
70
+ type: 'select',
71
+ options: [
72
+ { value: '', label: t('messages.filters.all', 'All') },
73
+ { value: 'true', label: t('common.yes', 'Yes') },
74
+ { value: 'false', label: t('common.no', 'No') },
75
+ ],
76
+ },
77
+ {
78
+ id: 'senderId',
79
+ label: t('messages.filters.sender', 'Sender'),
80
+ type: 'combobox',
81
+ placeholder: t('messages.filters.senderEmpty', 'Type to search users'),
82
+ options: senderOptions,
83
+ loadOptions: loadSenderOptions,
84
+ formatValue: senderLabel,
85
+ formatDescription: (val: string) =>
86
+ senderOptions.find((opt) => opt.value === val)?.description ?? null,
87
+ },
88
+ {
89
+ id: 'since',
90
+ label: t('messages.filters.since', 'Sent after'),
91
+ type: 'text',
92
+ placeholder: t('messages.filters.sincePlaceholder', 'YYYY-MM-DD'),
93
+ },
94
+ ]
95
+ }
96
+
97
+ const DATE_ONLY_PATTERN = /^\d{4}-\d{2}-\d{2}$/
98
+
99
+ export function normalizeMessagesSinceValue(input: string): string | null {
100
+ const trimmed = input.trim()
101
+ if (!trimmed) return null
102
+ if (DATE_ONLY_PATTERN.test(trimmed)) {
103
+ const parsed = new Date(`${trimmed}T00:00:00.000Z`)
104
+ if (Number.isNaN(parsed.getTime())) return null
105
+ if (parsed.toISOString().slice(0, 10) !== trimmed) return null
106
+ return parsed.toISOString()
107
+ }
108
+ const parsed = new Date(trimmed)
109
+ if (Number.isNaN(parsed.getTime())) return null
110
+ return parsed.toISOString()
111
+ }
112
+
113
+ export type BuildMessagesListParamsOptions = {
114
+ folder: string
115
+ page: number
116
+ pageSize: number
117
+ search: string
118
+ filterValues: FilterValues
119
+ }
120
+
121
+ export function buildMessagesListParams({
122
+ folder,
123
+ page,
124
+ pageSize,
125
+ search,
126
+ filterValues,
127
+ }: BuildMessagesListParamsOptions): URLSearchParams {
128
+ const params = new URLSearchParams()
129
+ params.set('folder', folder)
130
+ params.set('page', String(page))
131
+ params.set('pageSize', String(pageSize))
132
+
133
+ if (search.trim()) {
134
+ params.set('search', search.trim())
135
+ }
136
+
137
+ const stringValue = (id: string): string => {
138
+ const raw = filterValues[id]
139
+ return typeof raw === 'string' ? raw.trim() : ''
140
+ }
141
+
142
+ const status = stringValue('status')
143
+ const type = stringValue('type')
144
+ const hasObjects = stringValue('hasObjects')
145
+ const hasAttachments = stringValue('hasAttachments')
146
+ const hasActions = stringValue('hasActions')
147
+ const senderId = stringValue('senderId')
148
+ const sinceRaw = stringValue('since')
149
+
150
+ if (status) params.set('status', status)
151
+ if (type) params.set('type', type)
152
+ if (hasObjects) params.set('hasObjects', hasObjects)
153
+ if (hasAttachments) params.set('hasAttachments', hasAttachments)
154
+ if (hasActions) params.set('hasActions', hasActions)
155
+ if (senderId) params.set('senderId', senderId)
156
+ if (sinceRaw) {
157
+ const normalized = normalizeMessagesSinceValue(sinceRaw)
158
+ if (normalized) params.set('since', normalized)
159
+ }
160
+
161
+ return params
162
+ }
@@ -186,6 +186,7 @@ export const updateDraftSchema = z.object({
186
186
  attachmentIds: z.array(z.string().uuid()).optional(),
187
187
  actionData: messageActionDataSchema.optional(),
188
188
  sendViaEmail: z.boolean().optional(),
189
+ isDraft: z.literal(false).optional(),
189
190
  }).superRefine((value, ctx) => {
190
191
  if (value.recipients) {
191
192
  const duplicateRecipientIds = collectDuplicateRecipientIds(value.recipients)
@@ -131,12 +131,13 @@
131
131
  "messages.externalName": "Externer Name",
132
132
  "messages.fields.type": "Nachrichtentyp",
133
133
  "messages.filters.all": "Alle",
134
- "messages.filters.hasActions": "Aktionen",
135
- "messages.filters.hasAttachments": "Anhänge",
136
- "messages.filters.hasObjects": "Objekte",
134
+ "messages.filters.hasActions": "Hat Aktionen",
135
+ "messages.filters.hasAttachments": "Hat Anhänge",
136
+ "messages.filters.hasObjects": "Hat Objekte",
137
137
  "messages.filters.sender": "Absender",
138
+ "messages.filters.senderEmpty": "Tippe, um Benutzer zu suchen",
138
139
  "messages.filters.since": "Gesendet nach",
139
- "messages.filters.sincePlaceholder": "YYYY-MM-DDTHH:mm:ssZ",
140
+ "messages.filters.sincePlaceholder": "YYYY-MM-DD",
140
141
  "messages.filters.status": "Status",
141
142
  "messages.filters.type": "Typ",
142
143
  "messages.flash.actionSuccess": "Aktion abgeschlossen.",
@@ -131,12 +131,13 @@
131
131
  "messages.externalName": "External name",
132
132
  "messages.fields.type": "Message type",
133
133
  "messages.filters.all": "All",
134
- "messages.filters.hasActions": "Actions",
135
- "messages.filters.hasAttachments": "Attachments",
136
- "messages.filters.hasObjects": "Objects",
134
+ "messages.filters.hasActions": "Has actions",
135
+ "messages.filters.hasAttachments": "Has attachments",
136
+ "messages.filters.hasObjects": "Has objects",
137
137
  "messages.filters.sender": "Sender",
138
+ "messages.filters.senderEmpty": "Type to search users",
138
139
  "messages.filters.since": "Sent after",
139
- "messages.filters.sincePlaceholder": "YYYY-MM-DDTHH:mm:ssZ",
140
+ "messages.filters.sincePlaceholder": "YYYY-MM-DD",
140
141
  "messages.filters.status": "Status",
141
142
  "messages.filters.type": "Type",
142
143
  "messages.flash.actionSuccess": "Action completed.",
@@ -131,12 +131,13 @@
131
131
  "messages.externalName": "Nombre externo",
132
132
  "messages.fields.type": "Tipo de mensaje",
133
133
  "messages.filters.all": "Todos",
134
- "messages.filters.hasActions": "Acciones",
135
- "messages.filters.hasAttachments": "Adjuntos",
136
- "messages.filters.hasObjects": "Objetos",
134
+ "messages.filters.hasActions": "Tiene acciones",
135
+ "messages.filters.hasAttachments": "Tiene adjuntos",
136
+ "messages.filters.hasObjects": "Tiene objetos",
137
137
  "messages.filters.sender": "Remitente",
138
+ "messages.filters.senderEmpty": "Escribe para buscar usuarios",
138
139
  "messages.filters.since": "Enviado después de",
139
- "messages.filters.sincePlaceholder": "YYYY-MM-DDTHH:mm:ssZ",
140
+ "messages.filters.sincePlaceholder": "YYYY-MM-DD",
140
141
  "messages.filters.status": "Estado",
141
142
  "messages.filters.type": "Tipo",
142
143
  "messages.flash.actionSuccess": "Acción completada.",
@@ -131,12 +131,13 @@
131
131
  "messages.externalName": "Nazwa zewnętrzna",
132
132
  "messages.fields.type": "Typ wiadomości",
133
133
  "messages.filters.all": "Wszystkie",
134
- "messages.filters.hasActions": "Akcje",
135
- "messages.filters.hasAttachments": "Załączniki",
136
- "messages.filters.hasObjects": "Obiekty",
134
+ "messages.filters.hasActions": "Ma akcje",
135
+ "messages.filters.hasAttachments": "Ma załączniki",
136
+ "messages.filters.hasObjects": "Ma obiekty",
137
137
  "messages.filters.sender": "Nadawca",
138
+ "messages.filters.senderEmpty": "Wpisz, aby wyszukać użytkowników",
138
139
  "messages.filters.since": "Wysłane po",
139
- "messages.filters.sincePlaceholder": "YYYY-MM-DDTHH:mm:ssZ",
140
+ "messages.filters.sincePlaceholder": "RRRR-MM-DD",
140
141
  "messages.filters.status": "Status",
141
142
  "messages.filters.type": "Typ",
142
143
  "messages.flash.actionSuccess": "Akcja wykonana.",
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/core'
2
3
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
3
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
5
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
@@ -85,7 +86,7 @@ const crud = makeCrudRoute({
85
86
  const filters: Record<string, unknown> = {}
86
87
  if (query.orderId) filters.order_id = { $eq: query.orderId }
87
88
  try {
88
- const em = ctx.container.resolve('em')
89
+ const em = ctx.container.resolve<EntityManager>('em')
89
90
  const cfFilters = await buildCustomFieldFiltersFromQuery({
90
91
  entityId: E.sales.sales_order_adjustment,
91
92
  query,
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/core'
2
3
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
3
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
5
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
@@ -82,7 +83,7 @@ const crud = makeCrudRoute({
82
83
  const filters: Record<string, unknown> = {}
83
84
  if (query.quoteId) filters.quote_id = { $eq: query.quoteId }
84
85
  try {
85
- const em = ctx.container.resolve('em')
86
+ const em = ctx.container.resolve<EntityManager>('em')
86
87
  const cfFilters = await buildCustomFieldFiltersFromQuery({
87
88
  entityId: E.sales.sales_quote_adjustment,
88
89
  query,
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import type { EntityManager } from '@mikro-orm/core'
2
3
  import { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'
3
4
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
5
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
@@ -210,7 +211,7 @@ export function makeSalesLineRoute(config: SalesLineRouteConfig) {
210
211
  if (query.id) filters.id = { $eq: query.id }
211
212
  if (query[parentFkParam]) filters[parentFkColumn] = { $eq: query[parentFkParam] }
212
213
  try {
213
- const em = ctx.container.resolve('em')
214
+ const em = ctx.container.resolve<EntityManager>('em')
214
215
  const cfFilters = await buildCustomFieldFiltersFromQuery({
215
216
  entityId,
216
217
  query,
@@ -280,6 +280,7 @@
280
280
  "staff.messageObjects.notFound": "Nicht gefunden",
281
281
  "staff.messageObjects.team.title": "Team",
282
282
  "staff.messageObjects.teamMember.title": "Teammitglied",
283
+ "staff.messages.leaveRequestApproval": "Urlaubsantrag – Genehmigung",
283
284
  "staff.myAvailability.empty.profileRequired": "Erstelle dein Teamprofil, um Verfügbarkeit zu verwalten.",
284
285
  "staff.myAvailability.errors.load": "Verfügbarkeit konnte nicht geladen werden.",
285
286
  "staff.myAvailability.loading": "Verfügbarkeit wird geladen...",
@@ -280,6 +280,7 @@
280
280
  "staff.messageObjects.notFound": "Not found",
281
281
  "staff.messageObjects.team.title": "Team",
282
282
  "staff.messageObjects.teamMember.title": "Team member",
283
+ "staff.messages.leaveRequestApproval": "Leave request approval",
283
284
  "staff.myAvailability.empty.profileRequired": "Create your team member profile to manage availability.",
284
285
  "staff.myAvailability.errors.load": "Failed to load availability.",
285
286
  "staff.myAvailability.loading": "Loading availability...",