@open-mercato/core 0.8.1-develop.7452.1.5e211edd51 → 0.8.1-develop.7455.1.8f7e53badb
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/generated/entities/customer_dictionary_entry/index.js +2 -0
- package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
- package/dist/generated/entities/customer_interaction/index.js +2 -0
- package/dist/generated/entities/customer_interaction/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/customers/api/activity-types/route.js +151 -0
- package/dist/modules/customers/api/activity-types/route.js.map +7 -0
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -0
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
- package/dist/modules/customers/calendar-event-types.js +548 -0
- package/dist/modules/customers/calendar-event-types.js.map +7 -0
- package/dist/modules/customers/commands/dictionaries.js +70 -10
- package/dist/modules/customers/commands/dictionaries.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +187 -22
- package/dist/modules/customers/commands/interactions.js.map +3 -3
- package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
- package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
- package/dist/modules/customers/components/DictionarySettings.js +3 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
- package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
- package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
- package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
- package/dist/modules/customers/components/calendar/types.js +1 -0
- package/dist/modules/customers/components/calendar/types.js.map +2 -2
- package/dist/modules/customers/data/entities.js +6 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +39 -10
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/di.js +20 -1
- package/dist/modules/customers/di.js.map +2 -2
- package/dist/modules/customers/extension-points.js +13 -0
- package/dist/modules/customers/extension-points.js.map +2 -2
- package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
- package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
- package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
- package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
- package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
- package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
- package/dist/modules/customers/lib/calendar/timezone.js +111 -0
- package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
- package/dist/modules/customers/lib/interactionReadModel.js +1 -0
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +1 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
- package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
- package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
- package/dist/modules/planner/lib/availabilityMerge.js +101 -38
- package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
- package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/generated/entities/customer_dictionary_entry/index.ts +1 -0
- package/generated/entities/customer_interaction/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
- package/src/modules/customers/api/activity-types/route.ts +154 -0
- package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
- package/src/modules/customers/api/interactions/route.ts +4 -0
- package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
- package/src/modules/customers/calendar-event-types.ts +674 -0
- package/src/modules/customers/commands/dictionaries.ts +84 -8
- package/src/modules/customers/commands/interactions.ts +244 -21
- package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
- package/src/modules/customers/components/DictionarySettings.tsx +5 -3
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
- package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
- package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
- package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
- package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
- package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
- package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/components/detail/types.ts +1 -0
- package/src/modules/customers/data/entities.ts +8 -1
- package/src/modules/customers/data/validators.ts +42 -7
- package/src/modules/customers/di.ts +21 -0
- package/src/modules/customers/extension-points.ts +13 -0
- package/src/modules/customers/i18n/de.json +83 -0
- package/src/modules/customers/i18n/en.json +83 -0
- package/src/modules/customers/i18n/es.json +83 -0
- package/src/modules/customers/i18n/ko.json +83 -0
- package/src/modules/customers/i18n/pl.json +83 -0
- package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
- package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
- package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
- package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
- package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
- package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
- package/src/modules/customers/lib/calendar/timezone.ts +117 -0
- package/src/modules/customers/lib/interactionReadModel.ts +1 -0
- package/src/modules/customers/lib/todoCompatibility.ts +2 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
- package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
- package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
- package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
- package/src/modules/planner/lib/availabilityMerge.ts +106 -41
- package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
|
@@ -21,6 +21,8 @@ function DateTimeRow({
|
|
|
21
21
|
showTime,
|
|
22
22
|
onDateChange,
|
|
23
23
|
onTimeChange,
|
|
24
|
+
error,
|
|
25
|
+
disabled,
|
|
24
26
|
}: {
|
|
25
27
|
label: string
|
|
26
28
|
date: string
|
|
@@ -28,14 +30,17 @@ function DateTimeRow({
|
|
|
28
30
|
showTime: boolean
|
|
29
31
|
onDateChange(next: string): void
|
|
30
32
|
onTimeChange(next: string): void
|
|
33
|
+
error?: string | null
|
|
34
|
+
disabled?: boolean
|
|
31
35
|
}) {
|
|
32
36
|
return (
|
|
33
|
-
<div className="flex w-full items-end gap-2.5">
|
|
37
|
+
<div className="flex w-full flex-wrap items-end gap-x-2.5 gap-y-1">
|
|
34
38
|
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
35
39
|
<span className={LABEL_CLASS}>{label}</span>
|
|
36
|
-
<DateControl value={date} onChange={onDateChange} ariaLabel={label} />
|
|
40
|
+
<DateControl value={date} onChange={onDateChange} ariaLabel={label} disabled={disabled} />
|
|
37
41
|
</div>
|
|
38
|
-
{showTime ? <TimeControl value={time} onChange={onTimeChange} ariaLabel={label} /> : null}
|
|
42
|
+
{showTime ? <TimeControl value={time} onChange={onTimeChange} ariaLabel={label} disabled={disabled} /> : null}
|
|
43
|
+
{error ? <p className="w-full text-xs text-status-error-text">{error}</p> : null}
|
|
39
44
|
</div>
|
|
40
45
|
)
|
|
41
46
|
}
|
|
@@ -49,7 +54,9 @@ export function ScheduleSection({
|
|
|
49
54
|
startTime,
|
|
50
55
|
endDate,
|
|
51
56
|
endTime,
|
|
57
|
+
startError,
|
|
52
58
|
endsError,
|
|
59
|
+
disabled,
|
|
53
60
|
onAllDayChange,
|
|
54
61
|
onDateChange,
|
|
55
62
|
onStartTimeChange,
|
|
@@ -64,7 +71,9 @@ export function ScheduleSection({
|
|
|
64
71
|
startTime: string
|
|
65
72
|
endDate: string
|
|
66
73
|
endTime: string
|
|
74
|
+
startError?: string | null
|
|
67
75
|
endsError?: string | null
|
|
76
|
+
disabled?: boolean
|
|
68
77
|
onAllDayChange(next: boolean): void
|
|
69
78
|
onDateChange(next: string): void
|
|
70
79
|
onStartTimeChange(next: string): void
|
|
@@ -79,7 +88,7 @@ export function ScheduleSection({
|
|
|
79
88
|
{hasAllDay ? (
|
|
80
89
|
<div className="flex w-full items-center justify-between">
|
|
81
90
|
<span className={LABEL_CLASS}>{t('customers.calendar.editor.allDay', 'All day')}</span>
|
|
82
|
-
<AllDayToggle checked={allDay} onCheckedChange={onAllDayChange} label={t('customers.calendar.editor.allDay', 'All day')} />
|
|
91
|
+
<AllDayToggle checked={allDay} onCheckedChange={onAllDayChange} label={t('customers.calendar.editor.allDay', 'All day')} disabled={disabled} />
|
|
83
92
|
</div>
|
|
84
93
|
) : null}
|
|
85
94
|
<DateTimeRow
|
|
@@ -89,6 +98,8 @@ export function ScheduleSection({
|
|
|
89
98
|
showTime={showTime}
|
|
90
99
|
onDateChange={onDateChange}
|
|
91
100
|
onTimeChange={onStartTimeChange}
|
|
101
|
+
error={startError}
|
|
102
|
+
disabled={disabled}
|
|
92
103
|
/>
|
|
93
104
|
{hasEnd ? (
|
|
94
105
|
<DateTimeRow
|
|
@@ -98,6 +109,7 @@ export function ScheduleSection({
|
|
|
98
109
|
showTime={showTime}
|
|
99
110
|
onDateChange={onEndDateChange}
|
|
100
111
|
onTimeChange={onEndTimeChange}
|
|
112
|
+
disabled={disabled}
|
|
101
113
|
/>
|
|
102
114
|
) : null}
|
|
103
115
|
{endsError ? <p className="text-xs text-status-error-text">{endsError}</p> : null}
|
|
@@ -12,12 +12,14 @@ export function SegmentGroup<T extends string>({
|
|
|
12
12
|
onChange,
|
|
13
13
|
size = 'sm',
|
|
14
14
|
ariaLabel,
|
|
15
|
+
disabled = false,
|
|
15
16
|
}: {
|
|
16
17
|
options: Array<SegmentOption<T>>
|
|
17
18
|
value: T
|
|
18
19
|
onChange(next: T): void
|
|
19
20
|
size?: 'sm' | 'md'
|
|
20
21
|
ariaLabel: string
|
|
22
|
+
disabled?: boolean
|
|
21
23
|
}) {
|
|
22
24
|
return (
|
|
23
25
|
<div
|
|
@@ -34,6 +36,7 @@ export function SegmentGroup<T extends string>({
|
|
|
34
36
|
variant="ghost"
|
|
35
37
|
aria-pressed={isActive}
|
|
36
38
|
onClick={() => onChange(option.value)}
|
|
39
|
+
disabled={disabled}
|
|
37
40
|
title={option.label}
|
|
38
41
|
data-state={isActive ? 'active' : 'inactive'}
|
|
39
42
|
className={cn(
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { calendarDayEndInstant, calendarDayStartInstant, calendarWallTimeToInstant } from '../../../lib/calendar/timezone'
|
|
4
5
|
import { endOfDay } from 'date-fns/endOfDay'
|
|
5
6
|
import { startOfDay } from 'date-fns/startOfDay'
|
|
6
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
8
|
import { computeDurationMinutes, type EditorFormState, type EditorKindConfig } from '../../../lib/calendar/editorPayload'
|
|
8
|
-
import {
|
|
9
|
+
import { findEditorConflicts } from '../../../lib/calendar/conflicts'
|
|
9
10
|
import type { ConflictScope } from '../../../lib/calendar/preferences'
|
|
10
11
|
import { mapInteractionToCalendarItem } from '../../../lib/calendar/mapItem'
|
|
11
12
|
import { participantActorKey } from '../../../lib/calendar/participantIdentity'
|
|
@@ -55,12 +56,14 @@ export function useEditorLabelResolution(
|
|
|
55
56
|
}, [activeOrgId, open, form.relatedTo, form.dealId, form.dealLabel, form.assigneeUserId, form.assigneeName, update])
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
const NO_DRAFT_RESOURCES: ReadonlyArray<{ id: string; label: string }> = []
|
|
60
|
+
|
|
58
61
|
function formatClock(date: Date): string {
|
|
59
62
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
// Debounced save-time conflict probe. Detection uses the SAME `findConflicts`
|
|
63
|
-
// logic the grid uses (overlap + shared owner/participant) against a freshly
|
|
66
|
+
// logic the grid uses (overlap + shared owner/participant/resource) against a freshly
|
|
64
67
|
// fetched ±1-day window, so the editor warning is always consistent with the
|
|
65
68
|
// conflict badges/rings the user sees on the calendar. The warning is
|
|
66
69
|
// informational and never blocks saving.
|
|
@@ -72,9 +75,11 @@ export function useConflictProbe(
|
|
|
72
75
|
draftOwnerUserId: string | null,
|
|
73
76
|
scope: ConflictScope,
|
|
74
77
|
currentUserId: string | null,
|
|
78
|
+
draftResources: ReadonlyArray<{ id: string; label: string }> = NO_DRAFT_RESOURCES,
|
|
75
79
|
): string | null {
|
|
76
80
|
const t = useT()
|
|
77
81
|
const [conflict, setConflict] = React.useState<string | null>(null)
|
|
82
|
+
const resourcesKey = draftResources.map((resource) => resource.id).join(',')
|
|
78
83
|
// Keys on the canonical actor key, not the raw userId — an external guest has
|
|
79
84
|
// no userId, so a userId-only key never changes when guests are added or
|
|
80
85
|
// removed and the probe would keep reporting the previous attendee set.
|
|
@@ -93,23 +98,26 @@ export function useConflictProbe(
|
|
|
93
98
|
let start: Date
|
|
94
99
|
let end: Date
|
|
95
100
|
if (isAllDay) {
|
|
96
|
-
const dayDate = new Date(`${form.date}T00:00:00`)
|
|
97
|
-
if (Number.isNaN(dayDate.getTime())) {
|
|
101
|
+
const dayDate = form.timezone ? calendarDayStartInstant(form.date, form.timezone) : new Date(`${form.date}T00:00:00`)
|
|
102
|
+
if (!dayDate || Number.isNaN(dayDate.getTime())) {
|
|
98
103
|
setConflict(null)
|
|
99
104
|
return
|
|
100
105
|
}
|
|
101
|
-
start = startOfDay(dayDate)
|
|
102
|
-
|
|
106
|
+
start = form.timezone ? dayDate : startOfDay(dayDate)
|
|
107
|
+
const dayEnd = form.timezone ? calendarDayEndInstant(form.date, form.timezone) : endOfDay(dayDate)
|
|
108
|
+
if (!dayEnd) { setConflict(null); return }
|
|
109
|
+
end = dayEnd
|
|
103
110
|
} else {
|
|
104
111
|
if (!form.startTime) {
|
|
105
112
|
setConflict(null)
|
|
106
113
|
return
|
|
107
114
|
}
|
|
108
|
-
|
|
109
|
-
if (Number.isNaN(
|
|
115
|
+
const draftStart = form.timezone ? calendarWallTimeToInstant(form.date, form.startTime, form.timezone) : new Date(`${form.date}T${form.startTime}:00`)
|
|
116
|
+
if (!draftStart || Number.isNaN(draftStart.getTime())) {
|
|
110
117
|
setConflict(null)
|
|
111
118
|
return
|
|
112
119
|
}
|
|
120
|
+
start = draftStart
|
|
113
121
|
const durationMinutes = config.hasEnd ? computeDurationMinutes(form) ?? 30 : 30
|
|
114
122
|
end = new Date(start.getTime() + durationMinutes * 60_000)
|
|
115
123
|
}
|
|
@@ -134,25 +142,33 @@ export function useConflictProbe(
|
|
|
134
142
|
if (!item) continue
|
|
135
143
|
others.push(...expandOccurrences(item, fetchWindow))
|
|
136
144
|
}
|
|
137
|
-
const
|
|
145
|
+
const conflicts = findEditorConflicts(
|
|
138
146
|
{
|
|
139
147
|
start,
|
|
140
148
|
end,
|
|
141
149
|
ownerUserId: draftOwnerUserId,
|
|
142
150
|
participants: form.participants.map((participant) => ({ userId: participant.userId, name: participant.name })),
|
|
143
151
|
status: form.status,
|
|
152
|
+
resources: [...draftResources],
|
|
153
|
+
ownerLabel: draftOwnerUserId && draftOwnerUserId === form.assigneeUserId ? form.assigneeName : null,
|
|
144
154
|
},
|
|
145
155
|
others,
|
|
146
156
|
excludeId,
|
|
147
157
|
{ scope, currentUserId },
|
|
148
158
|
)
|
|
149
159
|
if (!active) return
|
|
150
|
-
if (
|
|
160
|
+
if (conflicts.length === 0) {
|
|
151
161
|
setConflict(null)
|
|
152
162
|
return
|
|
153
163
|
}
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
// Name who and what is double-booked: an overlap alone does not tell the
|
|
165
|
+
// user which attendee or room to change.
|
|
166
|
+
const summary = conflicts
|
|
167
|
+
.map(({ item, shared }) => {
|
|
168
|
+
const names = shared.map((subject) => subject.label).filter(Boolean)
|
|
169
|
+
const entry = `${formatClock(item.start)}–${formatClock(item.end)}: ${item.title || t('customers.calendar.grid.untitled', 'Untitled')}`
|
|
170
|
+
return names.length ? `${entry} (${names.join(', ')})` : entry
|
|
171
|
+
})
|
|
156
172
|
.join(', ')
|
|
157
173
|
setConflict(summary ? t('customers.calendar.editor.conflictWarning', 'Overlaps with: {items}', { items: summary }) : null)
|
|
158
174
|
} catch {
|
|
@@ -165,6 +181,6 @@ export function useConflictProbe(
|
|
|
165
181
|
controller.abort()
|
|
166
182
|
}
|
|
167
183
|
// Re-probe when schedule-relevant inputs change (time window, all-day, attendees, owner, status, scope).
|
|
168
|
-
}, [open, form.date, form.startTime, form.endDate, form.endTime, form.allDay, form.status, participantsKey, config.hasAllDay, config.hasEnd, excludeId, draftOwnerUserId, scope, currentUserId, t]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
184
|
+
}, [open, form.date, form.timezone, form.startTime, form.endDate, form.endTime, form.allDay, form.status, participantsKey, resourcesKey, config.hasAllDay, config.hasEnd, excludeId, draftOwnerUserId, scope, currentUserId, t]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
169
185
|
return conflict
|
|
170
186
|
}
|
|
@@ -69,12 +69,13 @@ export function UppercaseBadge({ style, className, children }: { style?: React.C
|
|
|
69
69
|
)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export function AllDayToggle({ checked, onCheckedChange, label }: { checked: boolean; onCheckedChange(next: boolean): void; label: string }) {
|
|
72
|
+
export function AllDayToggle({ checked, onCheckedChange, label, disabled }: { checked: boolean; onCheckedChange(next: boolean): void; label: string; disabled?: boolean }) {
|
|
73
73
|
return (
|
|
74
74
|
<Switch
|
|
75
75
|
checked={checked}
|
|
76
76
|
aria-label={label}
|
|
77
77
|
onCheckedChange={onCheckedChange}
|
|
78
|
+
disabled={disabled}
|
|
78
79
|
className="h-6 w-10"
|
|
79
80
|
/>
|
|
80
81
|
)
|
|
@@ -112,11 +113,13 @@ export function DateControl({
|
|
|
112
113
|
onChange,
|
|
113
114
|
ariaLabel,
|
|
114
115
|
className,
|
|
116
|
+
disabled,
|
|
115
117
|
}: {
|
|
116
118
|
value: string
|
|
117
119
|
onChange(next: string): void
|
|
118
120
|
ariaLabel: string
|
|
119
121
|
className?: string
|
|
122
|
+
disabled?: boolean
|
|
120
123
|
}) {
|
|
121
124
|
const [open, setOpen] = React.useState(false)
|
|
122
125
|
useCloseOnEditorScroll(setOpen)
|
|
@@ -128,6 +131,7 @@ export function DateControl({
|
|
|
128
131
|
open={open}
|
|
129
132
|
onOpenChange={setOpen}
|
|
130
133
|
aria-label={ariaLabel}
|
|
134
|
+
disabled={disabled}
|
|
131
135
|
className={cn('w-full', className)}
|
|
132
136
|
/>
|
|
133
137
|
)
|
|
@@ -140,13 +144,18 @@ const TIME_OPTIONS: string[] = Array.from({ length: 48 }, (_, index) => {
|
|
|
140
144
|
return `${String(hour).padStart(2, '0')}:${minute}`
|
|
141
145
|
})
|
|
142
146
|
|
|
143
|
-
export function TimeControl({ value, onChange, ariaLabel }: { value: string; onChange(next: string): void; ariaLabel: string }) {
|
|
147
|
+
export function TimeControl({ value, onChange, ariaLabel, disabled }: { value: string; onChange(next: string): void; ariaLabel: string; disabled?: boolean }) {
|
|
144
148
|
const [open, setOpen] = React.useState(false)
|
|
145
149
|
useCloseOnEditorScroll(setOpen)
|
|
146
|
-
//
|
|
147
|
-
|
|
150
|
+
// Radix renders every item even while the list is closed, so a closed picker
|
|
151
|
+
// mounts only the selected time and the full grid is built on open. An
|
|
152
|
+
// off-grid value (e.g. an imported 22:15) stays selectable.
|
|
153
|
+
const options = React.useMemo(() => {
|
|
154
|
+
if (!open) return value ? [value] : []
|
|
155
|
+
return value && !TIME_OPTIONS.includes(value) ? [value, ...TIME_OPTIONS] : TIME_OPTIONS
|
|
156
|
+
}, [open, value])
|
|
148
157
|
return (
|
|
149
|
-
<Select value={value} onValueChange={onChange} open={open} onOpenChange={setOpen}>
|
|
158
|
+
<Select value={value} onValueChange={onChange} open={open} onOpenChange={setOpen} disabled={disabled}>
|
|
150
159
|
<SelectTrigger aria-label={ariaLabel} className="h-9 w-32 shrink-0">
|
|
151
160
|
<SelectValue />
|
|
152
161
|
</SelectTrigger>
|
|
@@ -165,12 +174,14 @@ export function PersonChip({
|
|
|
165
174
|
compact,
|
|
166
175
|
onRemove,
|
|
167
176
|
removeLabel,
|
|
177
|
+
disabled,
|
|
168
178
|
}: {
|
|
169
179
|
name: string
|
|
170
180
|
badge?: React.ReactNode
|
|
171
181
|
compact?: boolean
|
|
172
182
|
onRemove?: () => void
|
|
173
183
|
removeLabel?: string
|
|
184
|
+
disabled?: boolean
|
|
174
185
|
}) {
|
|
175
186
|
return (
|
|
176
187
|
<span
|
|
@@ -188,6 +199,7 @@ export function PersonChip({
|
|
|
188
199
|
size="xs"
|
|
189
200
|
onClick={(event) => { event.stopPropagation(); onRemove() }}
|
|
190
201
|
aria-label={removeLabel}
|
|
202
|
+
disabled={disabled}
|
|
191
203
|
className="size-5 shrink-0"
|
|
192
204
|
>
|
|
193
205
|
<Plus aria-hidden className="size-3.5 rotate-45 opacity-50" />
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { buildFormFieldsFromCustomFields, fetchCustomFieldFormStructure } from '@open-mercato/ui/backend/utils/customFieldForms'
|
|
5
|
+
import { filterCustomFieldDefs, type CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
|
|
6
|
+
import { validateValuesAgainstDefs } from '@open-mercato/shared/modules/entities/validation'
|
|
7
|
+
|
|
8
|
+
export function selectCalendarCustomFields(definitions: CustomFieldDefDto[], fieldsetIds: readonly string[]) {
|
|
9
|
+
if (!fieldsetIds.length) return filterCustomFieldDefs(definitions, 'form')
|
|
10
|
+
const allowed = new Set(fieldsetIds)
|
|
11
|
+
return filterCustomFieldDefs(definitions, 'form').filter((definition) => {
|
|
12
|
+
const fieldsets = definition.fieldsets?.length ? definition.fieldsets : definition.fieldset ? [definition.fieldset] : []
|
|
13
|
+
return fieldsets.length ? fieldsets.some((fieldset) => allowed.has(fieldset)) : allowed.has('__general__')
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function projectCalendarCustomValues(values: Record<string, unknown>, definitions: CustomFieldDefDto[]) {
|
|
18
|
+
const activeIds = new Set(definitions.map((definition) => `cf_${definition.key}`))
|
|
19
|
+
return Object.fromEntries(Object.entries(values).filter(([key]) => !key.startsWith('cf_') || activeIds.has(key)))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateCalendarCustomValues(values: Record<string, unknown>, definitions: CustomFieldDefDto[]) {
|
|
23
|
+
const customValues = Object.fromEntries(definitions.map((definition) => [definition.key, values[`cf_${definition.key}`]]))
|
|
24
|
+
return validateValuesAgainstDefs(customValues, definitions.map((definition) => ({
|
|
25
|
+
key: definition.key,
|
|
26
|
+
kind: definition.kind,
|
|
27
|
+
configJson: { validation: definition.validation ?? [] },
|
|
28
|
+
})))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useCalendarCustomFields(open: boolean, entityId: string, fieldsetIds: readonly string[]) {
|
|
32
|
+
const [state, setState] = React.useState<{ definitions: CustomFieldDefDto[]; status: 'loading' | 'ready' | 'error' }>({
|
|
33
|
+
definitions: [], status: 'loading',
|
|
34
|
+
})
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
if (!open) return
|
|
37
|
+
let active = true
|
|
38
|
+
setState({ definitions: [], status: 'loading' })
|
|
39
|
+
void fetchCustomFieldFormStructure([entityId]).then(({ definitions }) => {
|
|
40
|
+
if (active) setState({ definitions, status: 'ready' })
|
|
41
|
+
}).catch(() => {
|
|
42
|
+
if (active) setState({ definitions: [], status: 'error' })
|
|
43
|
+
})
|
|
44
|
+
return () => { active = false }
|
|
45
|
+
}, [open, entityId])
|
|
46
|
+
const definitions = React.useMemo(() => selectCalendarCustomFields(state.definitions, fieldsetIds), [state.definitions, fieldsetIds])
|
|
47
|
+
const fields = React.useMemo(() => buildFormFieldsFromCustomFields(definitions), [definitions])
|
|
48
|
+
return { definitions, allDefinitions: state.definitions, fields, status: state.status }
|
|
49
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
5
|
+
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
|
+
import { calendarEventTypes, type EffectiveCalendarEventType } from '../../../calendar-event-types'
|
|
7
|
+
import type { CalendarEventTypeCatalog, ScopedCalendarEventType } from '../../../lib/calendar/eventTypeResolver'
|
|
8
|
+
import { KIND_CONFIG, type EditorKindConfig, type EditorTypeOption } from '../../../lib/calendar/editorPayload'
|
|
9
|
+
|
|
10
|
+
type CatalogState =
|
|
11
|
+
| { status: 'loading' | 'error'; items: readonly ScopedCalendarEventType[] }
|
|
12
|
+
| { status: 'ready'; items: readonly ScopedCalendarEventType[] }
|
|
13
|
+
|
|
14
|
+
export function useEventTypeCatalog(open: boolean) {
|
|
15
|
+
const [state, setState] = React.useState<CatalogState>({ status: 'loading', items: [] })
|
|
16
|
+
const [attempt, setAttempt] = React.useState(0)
|
|
17
|
+
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
if (!open) {
|
|
20
|
+
setState({ status: 'loading', items: [] })
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
let active = true
|
|
24
|
+
setState({ status: 'loading', items: [] })
|
|
25
|
+
void readApiResultOrThrow<CalendarEventTypeCatalog>('/api/customers/activity-types')
|
|
26
|
+
.then((catalog) => {
|
|
27
|
+
if (!Array.isArray(catalog.items)) throw new Error('[internal] Invalid activity type catalog')
|
|
28
|
+
if (active) setState({ status: 'ready', items: catalog.items })
|
|
29
|
+
})
|
|
30
|
+
.catch((error) => {
|
|
31
|
+
if (!active) return
|
|
32
|
+
getTelemetryRuntime()?.reportError(error, {
|
|
33
|
+
module: 'customers',
|
|
34
|
+
code: 'customers.activity_type_catalog_load_failed',
|
|
35
|
+
})
|
|
36
|
+
setState({ status: 'error', items: [] })
|
|
37
|
+
})
|
|
38
|
+
return () => { active = false }
|
|
39
|
+
}, [open, attempt])
|
|
40
|
+
|
|
41
|
+
const retry = React.useCallback(() => setAttempt((value) => value + 1), [])
|
|
42
|
+
|
|
43
|
+
return { ...state, retry }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function selectedEventType(
|
|
47
|
+
items: readonly ScopedCalendarEventType[],
|
|
48
|
+
key: string,
|
|
49
|
+
): EffectiveCalendarEventType {
|
|
50
|
+
const normalizedKey = key.trim().toLowerCase()
|
|
51
|
+
const selected = items.find((item) => item.key === normalizedKey)
|
|
52
|
+
if (selected) return selected
|
|
53
|
+
const baseline = calendarEventTypes.find((item) => item.key === normalizedKey) ?? calendarEventTypes[0]!
|
|
54
|
+
return {
|
|
55
|
+
...baseline,
|
|
56
|
+
key,
|
|
57
|
+
label: key,
|
|
58
|
+
provenance: {},
|
|
59
|
+
historical: true,
|
|
60
|
+
fallbackReason: 'unknown',
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isSelectableEventType(items: readonly ScopedCalendarEventType[], key: string): boolean {
|
|
65
|
+
const normalizedKey = key.trim().toLowerCase()
|
|
66
|
+
return items.some((item) => item.key === normalizedKey && item.selectable && !item.historical)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function eventTypeOptions(
|
|
70
|
+
items: readonly ScopedCalendarEventType[],
|
|
71
|
+
selectedKey: string,
|
|
72
|
+
translate: (key: string, fallback: string) => string,
|
|
73
|
+
): EditorTypeOption[] {
|
|
74
|
+
const displayLabel = (item: ScopedCalendarEventType) => item.labelKey
|
|
75
|
+
? translate(item.labelKey, item.label)
|
|
76
|
+
: item.label
|
|
77
|
+
const normalizedSelectedKey = selectedKey.trim().toLowerCase()
|
|
78
|
+
const options = items
|
|
79
|
+
.filter((item) => item.selectable && !item.historical)
|
|
80
|
+
.sort((left, right) => left.behavior.order - right.behavior.order)
|
|
81
|
+
.map((item) => ({ value: item.key === normalizedSelectedKey ? selectedKey : item.key, label: displayLabel(item), icon: item.icon ?? null }))
|
|
82
|
+
if (!options.some((option) => option.value === selectedKey)) {
|
|
83
|
+
const current = items.find((item) => item.key === normalizedSelectedKey)
|
|
84
|
+
options.unshift({ value: selectedKey, label: current ? displayLabel(current) : selectedKey, icon: current?.icon ?? null })
|
|
85
|
+
}
|
|
86
|
+
return options
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function eventTypeConfig(definition: EffectiveCalendarEventType): EditorKindConfig {
|
|
90
|
+
const { baseKind, fields } = definition.behavior
|
|
91
|
+
return {
|
|
92
|
+
dateLabel: KIND_CONFIG[baseKind].dateLabel,
|
|
93
|
+
hasEnd: fields.endTime,
|
|
94
|
+
hasAllDay: fields.allDay,
|
|
95
|
+
hasRepeat: fields.recurrence,
|
|
96
|
+
location: fields.location === 'none' ? null : fields.location,
|
|
97
|
+
people: fields.people === 'none' ? null : fields.people === 'recipients' ? 'to' : fields.people,
|
|
98
|
+
hasPriority: fields.priority,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -33,6 +33,7 @@ export const calendarInteractionPayloadSchema = z
|
|
|
33
33
|
scheduledAt: z.string().nullable().optional(),
|
|
34
34
|
occurredAt: z.string().nullable().optional(),
|
|
35
35
|
durationMinutes: z.number().nullable().optional(),
|
|
36
|
+
timezone: z.string().nullable().optional(),
|
|
36
37
|
allDay: z.boolean().nullable().optional(),
|
|
37
38
|
location: z.string().nullable().optional(),
|
|
38
39
|
participants: z.array(calendarParticipantSchema).nullable().optional(),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Collection, OptionalProps } from '@mikro-orm/core'
|
|
2
2
|
import { Entity, Index, ManyToOne, OneToMany, OneToOne, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
|
|
3
3
|
import type { DictionaryEntrySortMode } from '@open-mercato/core/modules/dictionaries/lib/entrySort'
|
|
4
|
+
import type { CalendarEventTypeBehavior } from '../calendar-event-types'
|
|
4
5
|
|
|
5
6
|
export type CustomerEntityKind = 'person' | 'company'
|
|
6
7
|
export type CustomerAddressFormat = 'line_first' | 'street_first'
|
|
@@ -571,7 +572,7 @@ export class CustomerActivity {
|
|
|
571
572
|
`create index "customer_interactions_email_channel_idx" on "customer_interactions" ("channel_id", "entity_id") where "interaction_type" = 'email' and "channel_id" is not null and "deleted_at" is null`,
|
|
572
573
|
})
|
|
573
574
|
export class CustomerInteraction {
|
|
574
|
-
[OptionalProps]?: 'status' | 'pinned' | 'createdAt' | 'updatedAt' | 'deletedAt' | 'durationMinutes' | 'location' | 'allDay' | 'recurrenceRule' | 'recurrenceEnd' | 'participants' | 'reminderMinutes' | 'visibility' | 'linkedEntities' | 'guestPermissions' | 'externalMessageId' | 'channelProviderKey' | 'channelId'
|
|
575
|
+
[OptionalProps]?: 'status' | 'pinned' | 'createdAt' | 'updatedAt' | 'deletedAt' | 'timezone' | 'durationMinutes' | 'location' | 'allDay' | 'recurrenceRule' | 'recurrenceEnd' | 'participants' | 'reminderMinutes' | 'visibility' | 'linkedEntities' | 'guestPermissions' | 'externalMessageId' | 'channelProviderKey' | 'channelId'
|
|
575
576
|
|
|
576
577
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
577
578
|
id!: string
|
|
@@ -661,6 +662,9 @@ export class CustomerInteraction {
|
|
|
661
662
|
@Property({ name: 'deal_id', type: 'uuid', nullable: true })
|
|
662
663
|
dealId?: string | null
|
|
663
664
|
|
|
665
|
+
@Property({ name: 'timezone', type: 'text', nullable: true })
|
|
666
|
+
timezone?: string | null
|
|
667
|
+
|
|
664
668
|
@Property({ name: 'duration_minutes', type: 'int', nullable: true })
|
|
665
669
|
durationMinutes?: number | null
|
|
666
670
|
|
|
@@ -953,6 +957,9 @@ export class CustomerDictionaryEntry {
|
|
|
953
957
|
@Property({ type: 'text', nullable: true })
|
|
954
958
|
icon?: string | null
|
|
955
959
|
|
|
960
|
+
@Property({ name: 'activity_type_behavior', type: 'jsonb', nullable: true })
|
|
961
|
+
activityTypeBehavior?: CalendarEventTypeBehavior | null
|
|
962
|
+
|
|
956
963
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
957
964
|
createdAt: Date = new Date()
|
|
958
965
|
|
|
@@ -2,6 +2,8 @@ import { z } from 'zod'
|
|
|
2
2
|
import { isValidPhoneNumber } from '@open-mercato/shared/lib/phone'
|
|
3
3
|
import { COORDINATE_RANGES } from '@open-mercato/shared/lib/location/coordinates'
|
|
4
4
|
import { dictionaryEntrySortModeSchema } from '@open-mercato/core/modules/dictionaries/lib/entrySort'
|
|
5
|
+
import { calendarEventTypeBehaviorSchema } from '../calendar-event-types'
|
|
6
|
+
import { calendarDayStartInstant, calendarWallTimeToInstant, isCalendarTimezone } from '../lib/calendar/timezone'
|
|
5
7
|
|
|
6
8
|
const uuid = () => z.string().uuid()
|
|
7
9
|
|
|
@@ -350,13 +352,27 @@ const dictionaryColorSchema = z
|
|
|
350
352
|
)
|
|
351
353
|
const dictionaryIconSchema = z.string().trim().max(48)
|
|
352
354
|
|
|
355
|
+
function validateDictionaryBehavior(
|
|
356
|
+
payload: { kind: string; behavior?: unknown },
|
|
357
|
+
ctx: z.RefinementCtx,
|
|
358
|
+
): void {
|
|
359
|
+
if (payload.behavior !== undefined && payload.kind !== 'activity_type') {
|
|
360
|
+
ctx.addIssue({
|
|
361
|
+
code: z.ZodIssueCode.custom,
|
|
362
|
+
path: ['behavior'],
|
|
363
|
+
message: 'Behavior is only supported for activity types',
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
353
368
|
export const customerDictionaryEntryCreateSchema = scopedSchema.extend({
|
|
354
369
|
kind: dictionaryKindEnum,
|
|
355
370
|
value: dictionaryValueSchema,
|
|
356
371
|
label: dictionaryLabelSchema.optional(),
|
|
357
372
|
color: dictionaryColorSchema.nullable().optional(),
|
|
358
373
|
icon: dictionaryIconSchema.nullable().optional(),
|
|
359
|
-
|
|
374
|
+
behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
|
|
375
|
+
}).superRefine(validateDictionaryBehavior)
|
|
360
376
|
|
|
361
377
|
export type CustomerDictionaryEntryCreateInput = z.infer<typeof customerDictionaryEntryCreateSchema>
|
|
362
378
|
|
|
@@ -368,18 +384,21 @@ export const customerDictionaryEntryUpdateSchema = scopedSchema
|
|
|
368
384
|
label: dictionaryLabelSchema.optional(),
|
|
369
385
|
color: dictionaryColorSchema.nullable().optional(),
|
|
370
386
|
icon: dictionaryIconSchema.nullable().optional(),
|
|
387
|
+
behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
|
|
371
388
|
})
|
|
372
389
|
.refine(
|
|
373
390
|
(payload) =>
|
|
374
391
|
payload.value !== undefined ||
|
|
375
392
|
payload.label !== undefined ||
|
|
376
393
|
payload.color !== undefined ||
|
|
377
|
-
payload.icon !== undefined
|
|
394
|
+
payload.icon !== undefined ||
|
|
395
|
+
payload.behavior !== undefined,
|
|
378
396
|
{
|
|
379
397
|
message: 'Provide at least one field to update.',
|
|
380
398
|
path: ['value'],
|
|
381
399
|
}
|
|
382
400
|
)
|
|
401
|
+
.superRefine(validateDictionaryBehavior)
|
|
383
402
|
|
|
384
403
|
export type CustomerDictionaryEntryUpdateInput = z.infer<typeof customerDictionaryEntryUpdateSchema>
|
|
385
404
|
|
|
@@ -477,7 +496,10 @@ const interactionGuestPermissionsSchema = z
|
|
|
477
496
|
})
|
|
478
497
|
.strict()
|
|
479
498
|
|
|
499
|
+
export const calendarTimezoneSchema = z.string().trim().min(1).max(120).refine(isCalendarTimezone, { message: 'customers.calendar.editor.validation.timezoneInvalid' })
|
|
500
|
+
|
|
480
501
|
const interactionExtendedFields = {
|
|
502
|
+
timezone: calendarTimezoneSchema.optional().nullable(),
|
|
481
503
|
durationMinutes: z.number().int().min(0).optional().nullable(),
|
|
482
504
|
location: z.string().trim().max(500).optional().nullable(),
|
|
483
505
|
allDay: z.boolean().optional().nullable(),
|
|
@@ -494,6 +516,7 @@ const interactionCreateBaseSchema = scopedSchema.extend({
|
|
|
494
516
|
id: z.string().uuid().optional(),
|
|
495
517
|
entityId: z.string().uuid(),
|
|
496
518
|
interactionType: z.string().trim().min(1).max(100),
|
|
519
|
+
enforceSelectableType: z.boolean().optional(),
|
|
497
520
|
title: z.string().trim().max(500).optional().nullable(),
|
|
498
521
|
body: z.string().trim().max(10000).optional().nullable(),
|
|
499
522
|
// Lenient like `deal_status` (status: z.string().max(50)). The `interaction-statuses`
|
|
@@ -518,11 +541,13 @@ const interactionCreateBaseSchema = scopedSchema.extend({
|
|
|
518
541
|
...interactionExtendedFields,
|
|
519
542
|
})
|
|
520
543
|
|
|
521
|
-
function deriveScheduledAtFromDateTime(date?: string | null, time?: string | null): Date | null {
|
|
544
|
+
function deriveScheduledAtFromDateTime(date?: string | null, time?: string | null, timezone?: string | null, allDay?: boolean | null): Date | null {
|
|
522
545
|
if (!date || typeof date !== 'string') return null
|
|
523
546
|
const trimmedDate = date.trim()
|
|
524
547
|
if (!trimmedDate) return null
|
|
525
548
|
const trimmedTime = typeof time === 'string' ? time.trim() : ''
|
|
549
|
+
if (timezone && allDay) return calendarDayStartInstant(trimmedDate, timezone)
|
|
550
|
+
if (timezone) return calendarWallTimeToInstant(trimmedDate, trimmedTime || '00:00', timezone)
|
|
526
551
|
const iso = trimmedTime ? `${trimmedDate}T${trimmedTime}:00` : `${trimmedDate}T00:00:00`
|
|
527
552
|
const parsed = new Date(iso)
|
|
528
553
|
return Number.isNaN(parsed.getTime()) ? null : parsed
|
|
@@ -551,9 +576,13 @@ export const interactionCreateSchema = interactionCreateBaseSchema
|
|
|
551
576
|
// external API consumers don't silently persist `scheduled_at: null` after
|
|
552
577
|
// the validator already enforced non-empty date/time. The form already
|
|
553
578
|
// computes `scheduledAt` itself, so this branch is a no-op for the form path.
|
|
554
|
-
.transform((value) => {
|
|
579
|
+
.transform((value, ctx) => {
|
|
555
580
|
if (value.scheduledAt) return value
|
|
556
|
-
const derived = deriveScheduledAtFromDateTime(value.date, value.time)
|
|
581
|
+
const derived = deriveScheduledAtFromDateTime(value.date, value.time, value.timezone, value.allDay)
|
|
582
|
+
if (value.timezone && value.date && !derived) {
|
|
583
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['time'], message: 'customers.calendar.editor.validation.timezoneGap' })
|
|
584
|
+
return z.NEVER
|
|
585
|
+
}
|
|
557
586
|
return derived ? { ...value, scheduledAt: derived } : value
|
|
558
587
|
})
|
|
559
588
|
|
|
@@ -572,6 +601,8 @@ const interactionUpdateBaseSchema = z
|
|
|
572
601
|
// state to express — that depends on #5935 making the column nullable first.
|
|
573
602
|
entityId: z.string().uuid().optional(),
|
|
574
603
|
interactionType: z.string().trim().min(1).max(100).optional(),
|
|
604
|
+
enforceSelectableType: z.boolean().optional(),
|
|
605
|
+
confirmDiscardInapplicableValues: z.boolean().optional(),
|
|
575
606
|
title: z.string().trim().max(500).optional().nullable(),
|
|
576
607
|
body: z.string().trim().max(10000).optional().nullable(),
|
|
577
608
|
status: z.string().max(50).optional(),
|
|
@@ -614,13 +645,17 @@ export const interactionUpdateSchema = interactionUpdateBaseSchema
|
|
|
614
645
|
// Mirror the create-schema derivation for partial updates: when an external
|
|
615
646
|
// caller supplies `date+time` without `scheduledAt`, derive the timestamp so
|
|
616
647
|
// the update doesn't silently leave `scheduled_at` stale.
|
|
617
|
-
.transform((value) => {
|
|
648
|
+
.transform((value, ctx) => {
|
|
618
649
|
if (value.scheduledAt !== undefined) return value
|
|
619
650
|
// An explicit `date: null` is how a caller drops the due date; mirror it
|
|
620
651
|
// onto `scheduledAt` rather than leaving the old timestamp behind (#5941).
|
|
621
652
|
if (value.date === null) return { ...value, scheduledAt: null }
|
|
622
653
|
if (!value.date && !value.time) return value
|
|
623
|
-
const derived = deriveScheduledAtFromDateTime(value.date, value.time)
|
|
654
|
+
const derived = deriveScheduledAtFromDateTime(value.date, value.time, value.timezone, value.allDay)
|
|
655
|
+
if (value.timezone && value.date && !derived) {
|
|
656
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['time'], message: 'customers.calendar.editor.validation.timezoneGap' })
|
|
657
|
+
return z.NEVER
|
|
658
|
+
}
|
|
624
659
|
return derived ? { ...value, scheduledAt: derived } : value
|
|
625
660
|
})
|
|
626
661
|
|