@open-mercato/core 0.6.6-develop.6519.1.bdc91fb755 → 0.6.6-develop.6520.1.55df6df792
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/backend/calendar/page.js +9 -1
- package/dist/modules/customers/backend/calendar/page.js.map +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js +0 -11
- package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -235
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarScreen.js +5 -5
- package/dist/modules/customers/components/calendar/CalendarScreen.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js +1 -11
- package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +162 -176
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/LocationField.js +61 -14
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +10 -5
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PriorityField.js +85 -0
- package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js +8 -3
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js +225 -0
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +12 -7
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/inputs.js +68 -57
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/lookups.js +82 -2
- package/dist/modules/customers/components/calendar/editor/lookups.js.map +2 -2
- package/dist/modules/customers/components/calendar/useCalendarItems.js +6 -1
- package/dist/modules/customers/components/calendar/useCalendarItems.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +53 -22
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js +1 -1
- package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/backend/calendar/page.tsx +8 -1
- package/src/modules/customers/components/calendar/AgendaList.tsx +0 -10
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +444 -261
- package/src/modules/customers/components/calendar/CalendarScreen.tsx +12 -5
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +6 -6
- package/src/modules/customers/components/calendar/MonthGrid.tsx +0 -11
- package/src/modules/customers/components/calendar/TimeGrid.tsx +1 -24
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +64 -2
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +11 -4
- package/src/modules/customers/components/calendar/editor/PriorityField.tsx +90 -0
- package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +8 -3
- package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +264 -0
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +14 -6
- package/src/modules/customers/components/calendar/editor/inputs.tsx +91 -50
- package/src/modules/customers/components/calendar/editor/lookups.ts +115 -2
- package/src/modules/customers/components/calendar/useCalendarItems.ts +8 -1
- package/src/modules/customers/data/validators.ts +3 -1
- package/src/modules/customers/i18n/de.json +12 -2
- package/src/modules/customers/i18n/en.json +12 -2
- package/src/modules/customers/i18n/es.json +12 -2
- package/src/modules/customers/i18n/pl.json +12 -2
- package/src/modules/customers/lib/calendar/editorPayload.ts +94 -37
- package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +1 -1
- package/src/modules/entities/i18n/de.json +120 -0
- package/src/modules/entities/i18n/en.json +120 -0
- package/src/modules/entities/i18n/es.json +120 -0
- package/src/modules/entities/i18n/pl.json +120 -0
- package/dist/modules/customers/components/calendar/editor/CategoryField.js +0 -75
- package/dist/modules/customers/components/calendar/editor/CategoryField.js.map +0 -7
- package/src/modules/customers/components/calendar/editor/CategoryField.tsx +0 -79
|
@@ -4,11 +4,13 @@ import * as React from 'react'
|
|
|
4
4
|
import { Calendar, X } from 'lucide-react'
|
|
5
5
|
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
|
|
6
6
|
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
-
import { apiCallOrThrow
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
8
|
+
import { extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
9
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
10
|
+
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
10
11
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
11
|
-
import {
|
|
12
|
+
import { CrudForm, type CrudFormGroup, type CrudFormGroupComponentProps } from '@open-mercato/ui/backend/CrudForm'
|
|
13
|
+
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
12
14
|
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
13
15
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
14
16
|
import { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
@@ -16,13 +18,14 @@ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
|
16
18
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
17
19
|
import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
18
20
|
import { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'
|
|
21
|
+
import { E } from '#generated/entities.ids.generated'
|
|
19
22
|
import {
|
|
20
|
-
|
|
23
|
+
buildEditorTypeOptions,
|
|
21
24
|
buildInteractionPayload,
|
|
22
25
|
computeDurationMinutes,
|
|
23
26
|
createDefaultFormState,
|
|
24
27
|
defaultRepeatDaysForDateInput,
|
|
25
|
-
|
|
28
|
+
editorKindOfInteractionType,
|
|
26
29
|
KIND_CONFIG,
|
|
27
30
|
parseItemToFormState,
|
|
28
31
|
resolveSavedOwnerUserId,
|
|
@@ -31,13 +34,16 @@ import {
|
|
|
31
34
|
type EditorPriority,
|
|
32
35
|
} from '../../lib/calendar/editorPayload'
|
|
33
36
|
import type { ConflictScope } from '../../lib/calendar/preferences'
|
|
37
|
+
import { renderDictionaryIcon } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
|
|
38
|
+
import { normalizeCustomFieldSubmitValue } from '../detail/customFieldUtils'
|
|
34
39
|
import type { CalendarItem } from './types'
|
|
35
|
-
import { Field } from './editor/inputs'
|
|
40
|
+
import { EDITOR_SCROLL_EVENT, Field } from './editor/inputs'
|
|
36
41
|
import { SegmentGroup } from './editor/SegmentGroup'
|
|
42
|
+
import { PriorityField } from './editor/PriorityField'
|
|
37
43
|
import { RelatedToField } from './editor/RelatedToField'
|
|
38
|
-
import { CategoryField } from './editor/CategoryField'
|
|
39
44
|
import { RepeatField } from './editor/RepeatField'
|
|
40
45
|
import { PeopleField } from './editor/PeopleField'
|
|
46
|
+
import { ResourcesField } from './editor/ResourcesField'
|
|
41
47
|
import { ScheduleSection } from './editor/ScheduleSection'
|
|
42
48
|
import { LocationField } from './editor/LocationField'
|
|
43
49
|
import { useConflictProbe, useEditorLabelResolution } from './editor/hooks'
|
|
@@ -49,16 +55,17 @@ export interface CalendarEventEditorProps {
|
|
|
49
55
|
defaultDate?: Date
|
|
50
56
|
defaultRange?: { start: Date; end: Date } | null
|
|
51
57
|
typeLabels: Record<string, string>
|
|
52
|
-
|
|
53
|
-
surfacedTypes?: string[]
|
|
54
|
-
eventCategories?: string[]
|
|
58
|
+
typeIcons?: Record<string, string | null>
|
|
55
59
|
conflictScope?: ConflictScope
|
|
56
60
|
currentUserId?: string | null
|
|
61
|
+
resourcesEnabled?: boolean
|
|
62
|
+
staffEnabled?: boolean
|
|
57
63
|
onOpenChange(open: boolean): void
|
|
58
64
|
onSaved(): void
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
const FORM_ID = 'customers-calendar-event-editor'
|
|
68
|
+
const INTERACTION_ENTITY_IDS = [E.customers.customer_interaction]
|
|
62
69
|
|
|
63
70
|
const PEOPLE_FIELD_TEXT = {
|
|
64
71
|
attendees: { labelKey: 'customers.calendar.editor.attendees', label: 'Attendees', placeholderKey: 'customers.calendar.editor.addPeoplePlaceholder', placeholder: 'Add staff or customer…' },
|
|
@@ -66,35 +73,57 @@ const PEOPLE_FIELD_TEXT = {
|
|
|
66
73
|
to: { labelKey: 'customers.calendar.editor.to', label: 'To', placeholderKey: 'customers.calendar.editor.addRecipientPlaceholder', placeholder: 'Add recipient…' },
|
|
67
74
|
} as const
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
// CrudForm values carry the flattened EditorFormState keys (seeded via
|
|
77
|
+
// initialValues) plus cf_* custom-field keys managed by CrudForm itself.
|
|
78
|
+
function formStateOfValues(values: Record<string, unknown>): EditorFormState {
|
|
79
|
+
return values as unknown as EditorFormState
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function customFieldInitialValues(item: CalendarItem): Record<string, unknown> {
|
|
83
|
+
const customValues = (item.raw as Record<string, unknown>).customValues
|
|
84
|
+
if (!customValues || typeof customValues !== 'object' || Array.isArray(customValues)) return {}
|
|
85
|
+
return Object.fromEntries(
|
|
86
|
+
Object.entries(customValues as Record<string, unknown>).map(([key, value]) => [`cf_${key}`, value]),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type EditorBodyProps = {
|
|
91
|
+
ctx: CrudFormGroupComponentProps
|
|
92
|
+
open: boolean
|
|
93
|
+
isEdit: boolean
|
|
94
|
+
item?: CalendarItem | null
|
|
95
|
+
typeLabels: Record<string, string>
|
|
96
|
+
typeIcons: Record<string, string | null>
|
|
97
|
+
conflictScope: ConflictScope
|
|
98
|
+
currentUserId: string | null
|
|
99
|
+
resourcesEnabled: boolean
|
|
100
|
+
staffEnabled: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function EditorBody({
|
|
104
|
+
ctx,
|
|
105
|
+
open,
|
|
106
|
+
isEdit,
|
|
107
|
+
item,
|
|
108
|
+
typeLabels,
|
|
109
|
+
typeIcons,
|
|
110
|
+
conflictScope,
|
|
111
|
+
currentUserId,
|
|
112
|
+
resourcesEnabled,
|
|
113
|
+
staffEnabled,
|
|
114
|
+
}: EditorBodyProps) {
|
|
70
115
|
const t = useT()
|
|
71
116
|
const locale = useLocale()
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const [saving, setSaving] = React.useState(false)
|
|
75
|
-
const openKeyRef = React.useRef<string | null>(null)
|
|
76
|
-
const isEdit = mode === 'edit' && Boolean(item?.id)
|
|
117
|
+
const { setValue, errors } = ctx
|
|
118
|
+
const form = formStateOfValues(ctx.values)
|
|
77
119
|
const config = KIND_CONFIG[form.kind]
|
|
78
120
|
|
|
79
|
-
const update = React.useCallback(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
openKeyRef.current = null
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
const key = `${mode}:${item?.id ?? 'new'}`
|
|
89
|
-
if (openKeyRef.current === key) return
|
|
90
|
-
openKeyRef.current = key
|
|
91
|
-
setForm(
|
|
92
|
-
mode === 'edit' && item
|
|
93
|
-
? parseItemToFormState(item)
|
|
94
|
-
: createDefaultFormState(defaultDate ?? null, undefined, defaultRange ?? null),
|
|
95
|
-
)
|
|
96
|
-
setFieldErrors({})
|
|
97
|
-
}, [open, mode, item, defaultDate, defaultRange])
|
|
121
|
+
const update = React.useCallback(
|
|
122
|
+
(patch: Partial<EditorFormState>) => {
|
|
123
|
+
for (const [key, value] of Object.entries(patch)) setValue(key, value)
|
|
124
|
+
},
|
|
125
|
+
[setValue],
|
|
126
|
+
)
|
|
98
127
|
|
|
99
128
|
useEditorLabelResolution(open, form, update)
|
|
100
129
|
// Probe against the owner the interaction will actually be SAVED with so the
|
|
@@ -104,101 +133,378 @@ export function CalendarEventEditor({ open, mode, item, defaultDate, defaultRang
|
|
|
104
133
|
const draftOwnerUserId = resolveSavedOwnerUserId(config, form, isEdit, item?.ownerUserId ?? null)
|
|
105
134
|
// Self-exclude by the underlying interaction id (raw.id): findEditorConflictItems
|
|
106
135
|
// drops candidates by raw.id, and every expanded occurrence of a recurring series
|
|
107
|
-
// shares it — so the edited record never conflicts with itself.
|
|
108
|
-
|
|
109
|
-
// raw.id keeps it correct even if a future edit path skips that normalization.)
|
|
110
|
-
const conflict = useConflictProbe(open, form, config, isEdit && item ? item.raw.id : null, draftOwnerUserId, conflictScope ?? 'all', currentUserId ?? null)
|
|
111
|
-
|
|
112
|
-
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
113
|
-
formId: string
|
|
114
|
-
resourceKind: string
|
|
115
|
-
resourceId: string
|
|
116
|
-
retryLastMutation: () => Promise<boolean>
|
|
117
|
-
}>({
|
|
118
|
-
contextId: 'customers-calendar-event-editor',
|
|
119
|
-
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const handleSave = React.useCallback(async () => {
|
|
123
|
-
const errors: FieldErrors = {}
|
|
124
|
-
if (!form.title.trim()) errors.title = t('customers.calendar.editor.validation.titleRequired', 'Title is required')
|
|
125
|
-
if (!form.relatedTo) errors.relatedTo = t('customers.calendar.editor.validation.relatedToRequired', 'Select a person or company to link this event')
|
|
126
|
-
if (config.hasEnd && !form.allDay && computeDurationMinutes(form) === null) {
|
|
127
|
-
errors.ends = t('customers.calendar.editor.validation.endsBeforeStarts', 'End must be after start')
|
|
128
|
-
}
|
|
129
|
-
setFieldErrors(errors)
|
|
130
|
-
if (Object.keys(errors).length > 0) return
|
|
131
|
-
setSaving(true)
|
|
132
|
-
try {
|
|
133
|
-
const payload = buildInteractionPayload(form, { mode, id: item?.id })
|
|
134
|
-
await runMutation({
|
|
135
|
-
operation: () => {
|
|
136
|
-
const call = () =>
|
|
137
|
-
apiCallOrThrow('/api/customers/interactions', {
|
|
138
|
-
method: isEdit ? 'PUT' : 'POST',
|
|
139
|
-
headers: { 'content-type': 'application/json' },
|
|
140
|
-
body: JSON.stringify(payload),
|
|
141
|
-
})
|
|
142
|
-
return isEdit ? withScopedApiRequestHeaders(buildOptimisticLockHeader(item?.updatedAt), call) : call()
|
|
143
|
-
},
|
|
144
|
-
mutationPayload: {
|
|
145
|
-
operation: isEdit ? 'updateCalendarEvent' : 'createCalendarEvent',
|
|
146
|
-
interactionId: item?.id ?? null,
|
|
147
|
-
interactionType: form.category ?? form.kind,
|
|
148
|
-
},
|
|
149
|
-
context: {
|
|
150
|
-
formId: 'customers-calendar-event-editor',
|
|
151
|
-
resourceKind: 'customers.interaction',
|
|
152
|
-
resourceId: item?.id ?? 'new',
|
|
153
|
-
retryLastMutation,
|
|
154
|
-
},
|
|
155
|
-
})
|
|
156
|
-
flash(t('customers.calendar.editor.saved', 'Event saved'), 'success')
|
|
157
|
-
onOpenChange(false)
|
|
158
|
-
requestAnimationFrame(() => { onSaved() })
|
|
159
|
-
} catch (err) {
|
|
160
|
-
// An optimistic-lock 409 is surfaced as the persistent conflict bar by
|
|
161
|
-
// useGuardedMutation (surfaceRecordConflict) — close without re-flashing.
|
|
162
|
-
if (extractOptimisticLockConflict(err)) {
|
|
163
|
-
onOpenChange(false)
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
const { message } = mapCrudServerErrorToFormErrors(err)
|
|
167
|
-
const key = typeof message === 'string' ? message.trim() : ''
|
|
168
|
-
flash(key ? t(key, key) : t('customers.calendar.editor.error', 'Failed to save event'), 'error')
|
|
169
|
-
} finally {
|
|
170
|
-
setSaving(false)
|
|
171
|
-
}
|
|
172
|
-
}, [config.hasEnd, form, isEdit, item?.id, mode, onOpenChange, onSaved, retryLastMutation, runMutation, t])
|
|
136
|
+
// shares it — so the edited record never conflicts with itself.
|
|
137
|
+
const conflict = useConflictProbe(open, form, config, isEdit && item ? item.raw.id : null, draftOwnerUserId, conflictScope, currentUserId)
|
|
173
138
|
|
|
174
|
-
const
|
|
139
|
+
const kindLabels = React.useMemo(
|
|
140
|
+
() => ({
|
|
141
|
+
meeting: t('customers.calendar.editor.types.meeting', 'meeting'),
|
|
142
|
+
call: t('customers.calendar.editor.types.call', 'call'),
|
|
143
|
+
email: t('customers.calendar.editor.types.email', 'email'),
|
|
144
|
+
note: t('customers.calendar.editor.types.note', 'note'),
|
|
145
|
+
event: t('customers.calendar.editor.types.event', 'event'),
|
|
146
|
+
task: t('customers.calendar.editor.types.task', 'task'),
|
|
147
|
+
}),
|
|
148
|
+
[t],
|
|
149
|
+
) satisfies Record<EditorKind, string>
|
|
175
150
|
|
|
176
|
-
const
|
|
151
|
+
const selectedType = form.category ?? form.kind
|
|
152
|
+
const typeOptions = React.useMemo(
|
|
153
|
+
() => buildEditorTypeOptions({ typeLabels, typeIcons, selectedValue: selectedType, kindLabels }),
|
|
154
|
+
[typeLabels, typeIcons, selectedType, kindLabels],
|
|
155
|
+
)
|
|
156
|
+
const typeSwitcherOptions = React.useMemo(
|
|
177
157
|
() =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}),
|
|
185
|
-
[typeLabels, surfacedTypes, eventCategories, form.category, form.kind, t],
|
|
158
|
+
typeOptions.map((option) => ({
|
|
159
|
+
value: option.value,
|
|
160
|
+
label: option.label,
|
|
161
|
+
icon: renderDictionaryIcon(option.icon, 'h-4 w-4'),
|
|
162
|
+
})),
|
|
163
|
+
[typeOptions],
|
|
186
164
|
)
|
|
187
165
|
|
|
166
|
+
// Candidates for the Call "insert phone from contact" button: the linked
|
|
167
|
+
// person (companies have no phone) + customer attendees. Their userId is the
|
|
168
|
+
// person id, so the picker can resolve primary_phone from the people API.
|
|
169
|
+
const phoneContactIds = React.useMemo(() => {
|
|
170
|
+
const ids: string[] = []
|
|
171
|
+
if (form.relatedTo && form.relatedTo.kind !== 'company') ids.push(form.relatedTo.id)
|
|
172
|
+
for (const participant of form.participants) {
|
|
173
|
+
if (participant.isCustomer) ids.push(participant.userId)
|
|
174
|
+
}
|
|
175
|
+
return Array.from(new Set(ids))
|
|
176
|
+
}, [form.relatedTo, form.participants])
|
|
177
|
+
|
|
188
178
|
const titleLabel = form.kind === 'email'
|
|
189
179
|
? t('customers.calendar.editor.titleLabel.email', 'Subject')
|
|
190
180
|
: form.kind === 'note'
|
|
191
181
|
? t('customers.calendar.editor.titleLabel.note', 'Note')
|
|
192
182
|
: t('customers.calendar.editor.titleLabel.generic', 'Title')
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
// Single column on phones (full-screen sheet). On lg+ the dialog widens and
|
|
186
|
+
// the fields group into two thematic columns: WHEN (schedule + repeat) on
|
|
187
|
+
// the left, CONTEXT (related record, category, location) on the right;
|
|
188
|
+
// people/resources and the task fields pair up below; title, description
|
|
189
|
+
// and the type switcher span both columns.
|
|
190
|
+
<div className="grid w-full grid-cols-1 items-start gap-4 lg:grid-cols-2 lg:gap-x-6">
|
|
191
|
+
{conflict ? (
|
|
192
|
+
<Alert variant="warning" className="rounded-lg lg:col-span-2">
|
|
193
|
+
<AlertTitle>{t('customers.calendar.editor.conflictTitle', 'Calendar conflict')}</AlertTitle>
|
|
194
|
+
<AlertDescription>{conflict}</AlertDescription>
|
|
195
|
+
</Alert>
|
|
196
|
+
) : null}
|
|
197
|
+
<div className="w-full lg:col-span-2">
|
|
198
|
+
<SegmentGroup<string>
|
|
199
|
+
ariaLabel={t('customers.calendar.editor.typeSwitcher', 'Event type')}
|
|
200
|
+
value={selectedType}
|
|
201
|
+
onChange={(type) => update({ kind: editorKindOfInteractionType(type), category: type })}
|
|
202
|
+
options={typeSwitcherOptions}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
<Field label={titleLabel} error={errors.title} className="lg:col-span-2">
|
|
206
|
+
<Input
|
|
207
|
+
type="text"
|
|
208
|
+
value={form.title}
|
|
209
|
+
onChange={(event) => update({ title: event.target.value })}
|
|
210
|
+
placeholder={t('customers.calendar.editor.titlePlaceholder', 'Add a title…')}
|
|
211
|
+
aria-label={titleLabel}
|
|
212
|
+
autoFocus
|
|
213
|
+
size="lg"
|
|
214
|
+
/>
|
|
215
|
+
</Field>
|
|
216
|
+
<div className="flex w-full flex-col gap-4">
|
|
217
|
+
<ScheduleSection
|
|
218
|
+
dateLabel={config.dateLabel}
|
|
219
|
+
hasAllDay={config.hasAllDay}
|
|
220
|
+
hasEnd={config.hasEnd}
|
|
221
|
+
allDay={form.allDay}
|
|
222
|
+
date={form.date}
|
|
223
|
+
startTime={form.startTime}
|
|
224
|
+
endDate={form.endDate}
|
|
225
|
+
endTime={form.endTime}
|
|
226
|
+
locale={locale}
|
|
227
|
+
endsError={errors.ends}
|
|
228
|
+
onAllDayChange={(allDay) => update({ allDay })}
|
|
229
|
+
onDateChange={(date) => {
|
|
230
|
+
const untouchedDefault =
|
|
231
|
+
JSON.stringify(form.repeatDays) === JSON.stringify(defaultRepeatDaysForDateInput(form.date))
|
|
232
|
+
update(
|
|
233
|
+
untouchedDefault
|
|
234
|
+
? { date, repeatDays: defaultRepeatDaysForDateInput(date) }
|
|
235
|
+
: { date },
|
|
236
|
+
)
|
|
237
|
+
}}
|
|
238
|
+
onStartTimeChange={(startTime) => update({ startTime })}
|
|
239
|
+
onEndDateChange={(endDate) => update({ endDate })}
|
|
240
|
+
onEndTimeChange={(endTime) => update({ endTime })}
|
|
241
|
+
/>
|
|
242
|
+
{config.hasRepeat ? (
|
|
243
|
+
<RepeatField
|
|
244
|
+
freq={form.repeatFreq}
|
|
245
|
+
days={form.repeatDays}
|
|
246
|
+
endType={form.repeatEndType}
|
|
247
|
+
count={form.repeatCount}
|
|
248
|
+
untilDate={form.repeatUntilDate}
|
|
249
|
+
locale={locale}
|
|
250
|
+
onFreqChange={(repeatFreq) =>
|
|
251
|
+
update(
|
|
252
|
+
repeatFreq === 'weekly'
|
|
253
|
+
? { repeatFreq, repeatDays: defaultRepeatDaysForDateInput(form.date) }
|
|
254
|
+
: { repeatFreq },
|
|
255
|
+
)}
|
|
256
|
+
onToggleDay={(index) =>
|
|
257
|
+
update({ repeatDays: form.repeatDays.map((active, dayIndex) => (dayIndex === index ? !active : active)) })}
|
|
258
|
+
onEndTypeChange={(repeatEndType) => update({ repeatEndType })}
|
|
259
|
+
onCountChange={(repeatCount) => update({ repeatCount })}
|
|
260
|
+
onUntilDateChange={(repeatUntilDate) => update({ repeatUntilDate })}
|
|
261
|
+
/>
|
|
262
|
+
) : null}
|
|
263
|
+
{config.people && config.people !== 'assignee' ? (
|
|
264
|
+
<Field label={t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label)}>
|
|
265
|
+
<PeopleField
|
|
266
|
+
mode="multi"
|
|
267
|
+
includeCustomers
|
|
268
|
+
includeStaff={staffEnabled}
|
|
269
|
+
placeholder={t(PEOPLE_FIELD_TEXT[config.people].placeholderKey, PEOPLE_FIELD_TEXT[config.people].placeholder)}
|
|
270
|
+
ariaLabel={t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label)}
|
|
271
|
+
value={form.participants}
|
|
272
|
+
onChange={(participants) => update({ participants })}
|
|
273
|
+
/>
|
|
274
|
+
</Field>
|
|
275
|
+
) : null}
|
|
276
|
+
{config.people === 'assignee' && staffEnabled ? (
|
|
277
|
+
<Field label={t('customers.calendar.editor.assignee', 'Assignee')} error={errors.assignee}>
|
|
278
|
+
<PeopleField
|
|
279
|
+
mode="single"
|
|
280
|
+
includeCustomers={false}
|
|
281
|
+
includeStaff
|
|
282
|
+
placeholder={t('customers.calendar.editor.assigneePlaceholder', 'Assign to a team member…')}
|
|
283
|
+
ariaLabel={t('customers.calendar.editor.assignee', 'Assignee')}
|
|
284
|
+
value={form.assigneeUserId
|
|
285
|
+
? [{ userId: form.assigneeUserId, name: form.assigneeName ?? form.assigneeUserId, isCustomer: false }]
|
|
286
|
+
: []}
|
|
287
|
+
onChange={(entries) => {
|
|
288
|
+
const next = entries[entries.length - 1] ?? null
|
|
289
|
+
update({ assigneeUserId: next?.userId ?? null, assigneeName: next?.name ?? null })
|
|
290
|
+
}}
|
|
291
|
+
/>
|
|
292
|
+
</Field>
|
|
293
|
+
) : null}
|
|
294
|
+
</div>
|
|
295
|
+
<div className="flex w-full flex-col gap-4">
|
|
296
|
+
<Field label={t('customers.calendar.editor.relatedTo', 'Related to')} error={errors.relatedTo}>
|
|
297
|
+
<RelatedToField
|
|
298
|
+
label={t('customers.calendar.editor.relatedTo', 'Related to')}
|
|
299
|
+
value={form.relatedTo}
|
|
300
|
+
deal={form.dealId && form.dealLabel ? { id: form.dealId, label: form.dealLabel } : null}
|
|
301
|
+
onChange={(relatedTo) => update({ relatedTo })}
|
|
302
|
+
onDealChange={(deal) => update({ dealId: deal?.id ?? null, dealLabel: deal?.label ?? null })}
|
|
303
|
+
error={errors.relatedTo}
|
|
304
|
+
/>
|
|
305
|
+
</Field>
|
|
306
|
+
{config.location ? (
|
|
307
|
+
<LocationField
|
|
308
|
+
variant={config.location}
|
|
309
|
+
value={form.location}
|
|
310
|
+
onChange={(location) => update({ location })}
|
|
311
|
+
phoneContactIds={phoneContactIds}
|
|
312
|
+
/>
|
|
313
|
+
) : null}
|
|
314
|
+
{resourcesEnabled ? (
|
|
315
|
+
<Field label={t('customers.calendar.editor.resources', 'Resources')}>
|
|
316
|
+
<ResourcesField
|
|
317
|
+
placeholder={t('customers.calendar.editor.resourcesPlaceholder', 'Add a resource…')}
|
|
318
|
+
ariaLabel={t('customers.calendar.editor.resources', 'Resources')}
|
|
319
|
+
value={form.resources}
|
|
320
|
+
onChange={(resources) => update({ resources })}
|
|
321
|
+
/>
|
|
322
|
+
</Field>
|
|
323
|
+
) : null}
|
|
324
|
+
{config.hasPriority ? (
|
|
325
|
+
<Field label={t('customers.calendar.editor.priority.label', 'Priority')}>
|
|
326
|
+
<PriorityField
|
|
327
|
+
value={form.priority}
|
|
328
|
+
ariaLabel={t('customers.calendar.editor.priority.label', 'Priority')}
|
|
329
|
+
labels={{
|
|
330
|
+
low: t('customers.calendar.editor.priority.low', 'Low'),
|
|
331
|
+
medium: t('customers.calendar.editor.priority.medium', 'Medium'),
|
|
332
|
+
high: t('customers.calendar.editor.priority.high', 'High'),
|
|
333
|
+
}}
|
|
334
|
+
onChange={(priority) => update({ priority })}
|
|
335
|
+
/>
|
|
336
|
+
</Field>
|
|
337
|
+
) : null}
|
|
338
|
+
</div>
|
|
339
|
+
<Field label={t('customers.calendar.editor.description', 'Description')} className="lg:col-span-2">
|
|
340
|
+
<Textarea
|
|
341
|
+
value={form.description}
|
|
342
|
+
onChange={(event) => update({ description: event.target.value })}
|
|
343
|
+
placeholder={t('customers.calendar.editor.descriptionPlaceholder', 'Add details…')}
|
|
344
|
+
aria-label={t('customers.calendar.editor.description', 'Description')}
|
|
345
|
+
className="h-20 resize-none"
|
|
346
|
+
/>
|
|
347
|
+
</Field>
|
|
348
|
+
</div>
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function CalendarEventEditor({
|
|
353
|
+
open,
|
|
354
|
+
mode,
|
|
355
|
+
item,
|
|
356
|
+
defaultDate,
|
|
357
|
+
defaultRange,
|
|
358
|
+
typeLabels,
|
|
359
|
+
typeIcons,
|
|
360
|
+
conflictScope,
|
|
361
|
+
currentUserId,
|
|
362
|
+
resourcesEnabled,
|
|
363
|
+
staffEnabled,
|
|
364
|
+
onOpenChange,
|
|
365
|
+
onSaved,
|
|
366
|
+
}: CalendarEventEditorProps) {
|
|
367
|
+
const t = useT()
|
|
368
|
+
const [saving, setSaving] = React.useState(false)
|
|
369
|
+
const isEdit = mode === 'edit' && Boolean(item?.id)
|
|
370
|
+
|
|
371
|
+
// CrudForm reads initialValues once, so every dialog open gets a fresh form
|
|
372
|
+
// instance keyed by the open sequence + edited record.
|
|
373
|
+
const [openSeq, setOpenSeq] = React.useState(0)
|
|
374
|
+
const wasOpenRef = React.useRef(false)
|
|
375
|
+
React.useEffect(() => {
|
|
376
|
+
if (open && !wasOpenRef.current) setOpenSeq((seq) => seq + 1)
|
|
377
|
+
wasOpenRef.current = open
|
|
378
|
+
}, [open])
|
|
379
|
+
const formKey = `${mode}:${item?.id ?? 'new'}:${openSeq}`
|
|
380
|
+
|
|
381
|
+
const initialValues = React.useMemo<Record<string, unknown>>(() => {
|
|
382
|
+
if (isEdit && item) {
|
|
383
|
+
return {
|
|
384
|
+
...parseItemToFormState(item),
|
|
385
|
+
...customFieldInitialValues(item),
|
|
386
|
+
id: item.id,
|
|
387
|
+
updatedAt: item.updatedAt ?? undefined,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return { ...createDefaultFormState(defaultDate ?? null, undefined, defaultRange ?? null) }
|
|
391
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- openSeq re-seeds defaults per dialog open
|
|
392
|
+
}, [isEdit, item, defaultDate, defaultRange, openSeq])
|
|
393
|
+
|
|
394
|
+
const handleSubmit = React.useCallback(
|
|
395
|
+
async (values: Record<string, unknown>) => {
|
|
396
|
+
const form = formStateOfValues(values)
|
|
397
|
+
const config = KIND_CONFIG[form.kind]
|
|
398
|
+
const fieldErrors: Record<string, string> = {}
|
|
399
|
+
if (!form.title.trim()) {
|
|
400
|
+
fieldErrors.title = t('customers.calendar.editor.validation.titleRequired', 'Title is required')
|
|
401
|
+
}
|
|
402
|
+
if (!form.relatedTo) {
|
|
403
|
+
fieldErrors.relatedTo = t('customers.calendar.editor.validation.relatedToRequired', 'Select a person or company to link this event')
|
|
404
|
+
}
|
|
405
|
+
if (config.hasEnd && !form.allDay && computeDurationMinutes(form) === null) {
|
|
406
|
+
fieldErrors.ends = t('customers.calendar.editor.validation.endsBeforeStarts', 'End must be after start')
|
|
407
|
+
}
|
|
408
|
+
// A task must be assigned to a team member — surface the requirement inline
|
|
409
|
+
// instead of letting the save fail with no visible reason (#3747 feedback).
|
|
410
|
+
if (config.people === 'assignee' && staffEnabled !== false && !form.assigneeUserId) {
|
|
411
|
+
fieldErrors.assignee = t('customers.calendar.editor.validation.assigneeRequired', 'Assign this task to a team member')
|
|
412
|
+
}
|
|
413
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
414
|
+
throw createCrudFormError(Object.values(fieldErrors)[0], fieldErrors)
|
|
415
|
+
}
|
|
416
|
+
setSaving(true)
|
|
417
|
+
try {
|
|
418
|
+
const payload = buildInteractionPayload(form, {
|
|
419
|
+
mode,
|
|
420
|
+
id: item?.id,
|
|
421
|
+
resourcesEnabled: resourcesEnabled === true,
|
|
422
|
+
staffEnabled: staffEnabled !== false,
|
|
423
|
+
})
|
|
424
|
+
const custom = collectCustomFieldValues(values, {
|
|
425
|
+
transform: (value) => normalizeCustomFieldSubmitValue(value),
|
|
426
|
+
})
|
|
427
|
+
for (const [key, value] of Object.entries(custom)) payload[`cf_${key}`] = value
|
|
428
|
+
// CrudForm supplies the optimistic-lock header (auto-derived from
|
|
429
|
+
// initialValues.updatedAt) via scoped request headers around onSubmit.
|
|
430
|
+
await apiCallOrThrow('/api/customers/interactions', {
|
|
431
|
+
method: isEdit ? 'PUT' : 'POST',
|
|
432
|
+
headers: { 'content-type': 'application/json' },
|
|
433
|
+
body: JSON.stringify(payload),
|
|
434
|
+
})
|
|
435
|
+
flash(t('customers.calendar.editor.saved', 'Event saved'), 'success')
|
|
436
|
+
onOpenChange(false)
|
|
437
|
+
requestAnimationFrame(() => { onSaved() })
|
|
438
|
+
} catch (err) {
|
|
439
|
+
// Surface an optimistic-lock 409 as the persistent conflict bar and
|
|
440
|
+
// close — the bar renders at page level, behind the dialog overlay.
|
|
441
|
+
if (extractOptimisticLockConflict(err)) {
|
|
442
|
+
surfaceRecordConflict(err, t)
|
|
443
|
+
onOpenChange(false)
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
throw err
|
|
447
|
+
} finally {
|
|
448
|
+
setSaving(false)
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
[isEdit, item?.id, mode, onOpenChange, onSaved, resourcesEnabled, staffEnabled, t],
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
const groups = React.useMemo<CrudFormGroup[]>(
|
|
455
|
+
() => [
|
|
456
|
+
{
|
|
457
|
+
id: 'details',
|
|
458
|
+
bare: true,
|
|
459
|
+
component: (ctx) => (
|
|
460
|
+
<EditorBody
|
|
461
|
+
ctx={ctx}
|
|
462
|
+
open={open}
|
|
463
|
+
isEdit={isEdit}
|
|
464
|
+
item={item}
|
|
465
|
+
typeLabels={typeLabels}
|
|
466
|
+
typeIcons={typeIcons ?? {}}
|
|
467
|
+
conflictScope={conflictScope ?? 'all'}
|
|
468
|
+
currentUserId={currentUserId ?? null}
|
|
469
|
+
resourcesEnabled={resourcesEnabled === true}
|
|
470
|
+
staffEnabled={staffEnabled !== false}
|
|
471
|
+
/>
|
|
472
|
+
),
|
|
473
|
+
},
|
|
474
|
+
{ id: 'customFields', kind: 'customFields' },
|
|
475
|
+
],
|
|
476
|
+
[open, isEdit, item, typeLabels, typeIcons, conflictScope, currentUserId, resourcesEnabled, staffEnabled],
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
const handleKeyDown = useDialogKeyHandler({
|
|
480
|
+
onConfirm: () => {
|
|
481
|
+
const formElement = document.getElementById(FORM_ID)
|
|
482
|
+
if (formElement instanceof HTMLFormElement) formElement.requestSubmit()
|
|
483
|
+
},
|
|
484
|
+
disabled: saving,
|
|
485
|
+
})
|
|
486
|
+
|
|
193
487
|
const dialogTitle = isEdit ? t('customers.calendar.editor.title.edit', 'Edit event') : t('customers.calendar.editor.title.create', 'New event')
|
|
194
488
|
|
|
195
489
|
return (
|
|
196
490
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
197
491
|
<DialogContent
|
|
198
492
|
onKeyDown={handleKeyDown}
|
|
493
|
+
// Only dismiss on a genuine click OUTSIDE the whole editor. Radix
|
|
494
|
+
// relays a click inside the dialog (but outside an open DS
|
|
495
|
+
// DatePicker/Select popover) up to the dialog as an "interact
|
|
496
|
+
// outside" event when it dismisses that popover — without this guard
|
|
497
|
+
// it would tear down the entire editor. Keep it open when the target
|
|
498
|
+
// is inside the dialog body or inside any portalled popover.
|
|
499
|
+
onInteractOutside={(event) => {
|
|
500
|
+
const target = event.detail.originalEvent.target as HTMLElement | null
|
|
501
|
+
if (target?.closest('[data-dialog-content]') || target?.closest('[data-radix-popper-content-wrapper]')) {
|
|
502
|
+
event.preventDefault()
|
|
503
|
+
}
|
|
504
|
+
}}
|
|
199
505
|
aria-describedby={undefined}
|
|
200
506
|
dismissible={false}
|
|
201
|
-
className="flex h-dvh max-h-dvh w-screen max-w-none flex-col gap-0 overflow-hidden rounded-none border-0 bg-background p-0 shadow-xl sm:h-auto sm:max-h-
|
|
507
|
+
className="flex h-dvh max-h-dvh w-screen max-w-none flex-col gap-0 overflow-hidden rounded-none border-0 bg-background p-0 shadow-xl sm:h-auto sm:max-h-[calc(100dvh-4rem)] sm:w-full sm:max-w-lg sm:rounded-2xl sm:border-0 lg:max-w-3xl"
|
|
202
508
|
>
|
|
203
509
|
<VisuallyHidden>
|
|
204
510
|
<DialogTitle>{dialogTitle}</DialogTitle>
|
|
@@ -216,152 +522,29 @@ export function CalendarEventEditor({ open, mode, item, defaultDate, defaultRang
|
|
|
216
522
|
<X aria-hidden className="size-5" />
|
|
217
523
|
</IconButton>
|
|
218
524
|
</div>
|
|
219
|
-
<div
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
size="lg"
|
|
242
|
-
/>
|
|
243
|
-
</Field>
|
|
244
|
-
<Field label={t('customers.calendar.editor.relatedTo', 'Related to')} error={fieldErrors.relatedTo}>
|
|
245
|
-
<RelatedToField
|
|
246
|
-
label={t('customers.calendar.editor.relatedTo', 'Related to')}
|
|
247
|
-
value={form.relatedTo}
|
|
248
|
-
deal={form.dealId && form.dealLabel ? { id: form.dealId, label: form.dealLabel } : null}
|
|
249
|
-
onChange={(relatedTo) => update({ relatedTo })}
|
|
250
|
-
onDealChange={(deal) => update({ dealId: deal?.id ?? null, dealLabel: deal?.label ?? null })}
|
|
251
|
-
error={fieldErrors.relatedTo}
|
|
252
|
-
/>
|
|
253
|
-
</Field>
|
|
254
|
-
<ScheduleSection
|
|
255
|
-
dateLabel={config.dateLabel}
|
|
256
|
-
hasAllDay={config.hasAllDay}
|
|
257
|
-
hasEnd={config.hasEnd}
|
|
258
|
-
allDay={form.allDay}
|
|
259
|
-
date={form.date}
|
|
260
|
-
startTime={form.startTime}
|
|
261
|
-
endDate={form.endDate}
|
|
262
|
-
endTime={form.endTime}
|
|
263
|
-
locale={locale}
|
|
264
|
-
endsError={fieldErrors.ends}
|
|
265
|
-
onAllDayChange={(allDay) => update({ allDay })}
|
|
266
|
-
onDateChange={(date) => {
|
|
267
|
-
const untouchedDefault =
|
|
268
|
-
JSON.stringify(form.repeatDays) === JSON.stringify(defaultRepeatDaysForDateInput(form.date))
|
|
269
|
-
update(
|
|
270
|
-
untouchedDefault
|
|
271
|
-
? { date, repeatDays: defaultRepeatDaysForDateInput(date) }
|
|
272
|
-
: { date },
|
|
273
|
-
)
|
|
274
|
-
}}
|
|
275
|
-
onStartTimeChange={(startTime) => update({ startTime })}
|
|
276
|
-
onEndDateChange={(endDate) => update({ endDate })}
|
|
277
|
-
onEndTimeChange={(endTime) => update({ endTime })}
|
|
525
|
+
<div
|
|
526
|
+
className="flex-1 overflow-y-auto"
|
|
527
|
+
onScroll={() => {
|
|
528
|
+
// Tell the DS date/time fields to close their (controlled) popover
|
|
529
|
+
// so a portalled popover doesn't float over the form or drift away
|
|
530
|
+
// from its field while scrolling (#3747 feedback). Radix ignores
|
|
531
|
+
// synthetic dismiss events, so the fields drive their own `open`.
|
|
532
|
+
document.dispatchEvent(new CustomEvent(EDITOR_SCROLL_EVENT))
|
|
533
|
+
}}
|
|
534
|
+
>
|
|
535
|
+
<div className="px-4 py-4 sm:px-6 sm:py-5">
|
|
536
|
+
<CrudForm<Record<string, unknown>>
|
|
537
|
+
key={formKey}
|
|
538
|
+
formId={FORM_ID}
|
|
539
|
+
embedded
|
|
540
|
+
hideFooterActions
|
|
541
|
+
customFieldsManageMode="page"
|
|
542
|
+
fields={[]}
|
|
543
|
+
groups={groups}
|
|
544
|
+
initialValues={initialValues}
|
|
545
|
+
entityIds={INTERACTION_ENTITY_IDS}
|
|
546
|
+
onSubmit={handleSubmit}
|
|
278
547
|
/>
|
|
279
|
-
{config.hasRepeat ? (
|
|
280
|
-
<RepeatField
|
|
281
|
-
freq={form.repeatFreq}
|
|
282
|
-
days={form.repeatDays}
|
|
283
|
-
endType={form.repeatEndType}
|
|
284
|
-
count={form.repeatCount}
|
|
285
|
-
untilDate={form.repeatUntilDate}
|
|
286
|
-
locale={locale}
|
|
287
|
-
onFreqChange={(repeatFreq) =>
|
|
288
|
-
update(
|
|
289
|
-
repeatFreq === 'weekly'
|
|
290
|
-
? { repeatFreq, repeatDays: defaultRepeatDaysForDateInput(form.date) }
|
|
291
|
-
: { repeatFreq },
|
|
292
|
-
)}
|
|
293
|
-
onToggleDay={(index) =>
|
|
294
|
-
update({ repeatDays: form.repeatDays.map((active, dayIndex) => (dayIndex === index ? !active : active)) })}
|
|
295
|
-
onEndTypeChange={(repeatEndType) => update({ repeatEndType })}
|
|
296
|
-
onCountChange={(repeatCount) => update({ repeatCount })}
|
|
297
|
-
onUntilDateChange={(repeatUntilDate) => update({ repeatUntilDate })}
|
|
298
|
-
/>
|
|
299
|
-
) : null}
|
|
300
|
-
<Field label={t('customers.calendar.editor.category', 'Category')}>
|
|
301
|
-
<CategoryField
|
|
302
|
-
label={t('customers.calendar.editor.category', 'Category')}
|
|
303
|
-
value={form.category ?? form.kind}
|
|
304
|
-
options={categoryOptions}
|
|
305
|
-
colors={typeColors}
|
|
306
|
-
onChange={(category) => update({ category })}
|
|
307
|
-
/>
|
|
308
|
-
</Field>
|
|
309
|
-
{config.location ? (
|
|
310
|
-
<LocationField variant={config.location} value={form.location} onChange={(location) => update({ location })} />
|
|
311
|
-
) : null}
|
|
312
|
-
{config.people && config.people !== 'assignee' ? (
|
|
313
|
-
<Field label={t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label)}>
|
|
314
|
-
<PeopleField
|
|
315
|
-
mode="multi"
|
|
316
|
-
includeCustomers
|
|
317
|
-
placeholder={t(PEOPLE_FIELD_TEXT[config.people].placeholderKey, PEOPLE_FIELD_TEXT[config.people].placeholder)}
|
|
318
|
-
ariaLabel={t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label)}
|
|
319
|
-
value={form.participants}
|
|
320
|
-
onChange={(participants) => update({ participants })}
|
|
321
|
-
/>
|
|
322
|
-
</Field>
|
|
323
|
-
) : null}
|
|
324
|
-
{config.hasPriority ? (
|
|
325
|
-
<Field label={t('customers.calendar.editor.priority.label', 'Priority')}>
|
|
326
|
-
<SegmentGroup<EditorPriority>
|
|
327
|
-
size="md"
|
|
328
|
-
ariaLabel={t('customers.calendar.editor.priority.label', 'Priority')}
|
|
329
|
-
value={form.priority}
|
|
330
|
-
onChange={(priority) => update({ priority })}
|
|
331
|
-
options={[
|
|
332
|
-
{ value: 'low', label: t('customers.calendar.editor.priority.low', 'Low') },
|
|
333
|
-
{ value: 'medium', label: t('customers.calendar.editor.priority.medium', 'Medium') },
|
|
334
|
-
{ value: 'high', label: t('customers.calendar.editor.priority.high', 'High') },
|
|
335
|
-
]}
|
|
336
|
-
/>
|
|
337
|
-
</Field>
|
|
338
|
-
) : null}
|
|
339
|
-
{config.people === 'assignee' ? (
|
|
340
|
-
<Field label={t('customers.calendar.editor.assignee', 'Assignee')}>
|
|
341
|
-
<PeopleField
|
|
342
|
-
mode="single"
|
|
343
|
-
includeCustomers={false}
|
|
344
|
-
placeholder={t('customers.calendar.editor.assigneePlaceholder', 'Assign to a team member…')}
|
|
345
|
-
ariaLabel={t('customers.calendar.editor.assignee', 'Assignee')}
|
|
346
|
-
value={form.assigneeUserId
|
|
347
|
-
? [{ userId: form.assigneeUserId, name: form.assigneeName ?? form.assigneeUserId, isCustomer: false }]
|
|
348
|
-
: []}
|
|
349
|
-
onChange={(entries) => {
|
|
350
|
-
const next = entries[entries.length - 1] ?? null
|
|
351
|
-
update({ assigneeUserId: next?.userId ?? null, assigneeName: next?.name ?? null })
|
|
352
|
-
}}
|
|
353
|
-
/>
|
|
354
|
-
</Field>
|
|
355
|
-
) : null}
|
|
356
|
-
<Field label={t('customers.calendar.editor.description', 'Description')}>
|
|
357
|
-
<Textarea
|
|
358
|
-
value={form.description}
|
|
359
|
-
onChange={(event) => update({ description: event.target.value })}
|
|
360
|
-
placeholder={t('customers.calendar.editor.descriptionPlaceholder', 'Add details…')}
|
|
361
|
-
aria-label={t('customers.calendar.editor.description', 'Description')}
|
|
362
|
-
className="h-24 resize-none"
|
|
363
|
-
/>
|
|
364
|
-
</Field>
|
|
365
548
|
</div>
|
|
366
549
|
</div>
|
|
367
550
|
<div className="flex shrink-0 items-center justify-end gap-3 border-t border-border bg-background px-5 py-4">
|
|
@@ -373,8 +556,8 @@ export function CalendarEventEditor({ open, mode, item, defaultDate, defaultRang
|
|
|
373
556
|
{t('customers.calendar.editor.cancel', 'Cancel')}
|
|
374
557
|
</Button>
|
|
375
558
|
<Button
|
|
376
|
-
type="
|
|
377
|
-
|
|
559
|
+
type="submit"
|
|
560
|
+
form={FORM_ID}
|
|
378
561
|
disabled={saving}
|
|
379
562
|
>
|
|
380
563
|
{saving ? t('customers.calendar.editor.saving', 'Saving…') : t('customers.calendar.editor.save', 'Save event')}
|