@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +13 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/integrations/data/validators.js +9 -6
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +42 -2
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +42 -86
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/inboxFilters.js +133 -0
- package/dist/modules/messages/components/inboxFilters.js.map +7 -0
- package/dist/modules/messages/data/validators.js +2 -1
- package/dist/modules/messages/data/validators.js.map +2 -2
- package/dist/modules/sales/api/order-adjustments/route.js.map +2 -2
- package/dist/modules/sales/api/quote-adjustments/route.js.map +2 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/jest.config.cjs +3 -1
- package/package.json +15 -15
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +18 -7
- package/src/modules/integrations/data/validators.ts +5 -4
- package/src/modules/messages/commands/messages.ts +54 -2
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +46 -89
- package/src/modules/messages/components/inboxFilters.ts +162 -0
- package/src/modules/messages/data/validators.ts +1 -0
- package/src/modules/messages/i18n/de.json +5 -4
- package/src/modules/messages/i18n/en.json +5 -4
- package/src/modules/messages/i18n/es.json +5 -4
- package/src/modules/messages/i18n/pl.json +5 -4
- package/src/modules/sales/api/order-adjustments/route.ts +2 -1
- package/src/modules/sales/api/quote-adjustments/route.ts +2 -1
- package/src/modules/sales/lib/makeSalesLineRoute.ts +2 -1
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/workflows/lib/activity-executor.ts +4 -3
- 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(() =>
|
|
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
|
-
//
|
|
105
|
-
//
|
|
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(
|
|
112
|
-
setStartTime(
|
|
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(
|
|
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.
|
|
44
|
-
|
|
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
|
-
|
|
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 =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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-
|
|
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": "
|
|
135
|
-
"messages.filters.hasAttachments": "
|
|
136
|
-
"messages.filters.hasObjects": "
|
|
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-
|
|
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": "
|
|
135
|
-
"messages.filters.hasAttachments": "
|
|
136
|
-
"messages.filters.hasObjects": "
|
|
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-
|
|
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": "
|
|
135
|
-
"messages.filters.hasAttachments": "
|
|
136
|
-
"messages.filters.hasObjects": "
|
|
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": "
|
|
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...",
|