@open-mercato/core 0.9.0 → 0.9.1-develop.7459.1.99e3accd30
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/catalog/commands/categories.js +1 -1
- package/dist/modules/catalog/commands/categories.js.map +2 -2
- package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
- package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +52 -17
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
- 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/communication_channels/lib/inbound-message-origin.js +1 -1
- package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
- package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
- package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
- package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.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/deals/[id]/people/route.js +58 -5
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- 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/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
- package/dist/modules/customers/backend/customers/deals/[id]/page.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/components/detail/CompanyPeopleSection.js +78 -343
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
- package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +7 -3
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
- package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonCard.js +5 -2
- package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
- package/dist/modules/customers/components/linking/adapters/personAdapter.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/messages/subscribers/message-notification.js +13 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
- package/dist/modules/payment_gateways/lib/gateway-service.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/sales/components/documents/LineItemDialog.js +159 -81
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
- package/dist/modules/sales/components/documents/lineItemUtils.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/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
- package/dist/modules/sync_excel/lib/adapters/customers.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 +9 -8
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/catalog/commands/categories.ts +1 -1
- package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
- package/src/modules/catalog/commands/products.ts +58 -17
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
- package/src/modules/communication_channels/i18n/de.json +4 -0
- package/src/modules/communication_channels/i18n/en.json +4 -0
- package/src/modules/communication_channels/i18n/es.json +4 -0
- package/src/modules/communication_channels/i18n/ko.json +4 -0
- package/src/modules/communication_channels/i18n/pl.json +4 -0
- package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
- package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
- package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
- package/src/modules/customers/api/activity-types/route.ts +154 -0
- package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
- 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/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
- 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/CompanyPeopleSection.tsx +94 -388
- package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
- package/src/modules/customers/components/detail/DealForm.tsx +7 -3
- package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
- package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
- package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
- package/src/modules/customers/components/detail/types.ts +1 -0
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -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 +104 -8
- package/src/modules/customers/i18n/en.json +104 -8
- package/src/modules/customers/i18n/es.json +104 -8
- package/src/modules/customers/i18n/ko.json +104 -8
- package/src/modules/customers/i18n/pl.json +104 -8
- 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/messages/subscribers/message-notification.ts +13 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
- package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
- package/src/modules/planner/lib/availabilityMerge.ts +106 -41
- package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
- package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
- package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
|
@@ -8,30 +8,30 @@ import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
|
8
8
|
import { extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
9
9
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
10
10
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
11
|
+
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
11
12
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
12
13
|
import { CrudForm, type CrudFormGroup, type CrudFormGroupComponentProps } from '@open-mercato/ui/backend/CrudForm'
|
|
14
|
+
import { useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
15
|
+
import { projectCalendarCustomValues, useCalendarCustomFields, validateCalendarCustomValues } from './editor/useCalendarCustomFields'
|
|
13
16
|
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
|
|
14
17
|
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
15
18
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
16
19
|
import { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
17
20
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
18
21
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
22
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'
|
|
23
|
+
import { calendarDayEndInstant, calendarDayStartInstant, calendarWallTimeToInstant, calendarTimezoneOptions, defaultCalendarTimezone, isCalendarTimezone } from '../../lib/calendar/timezone'
|
|
19
24
|
import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
20
25
|
import { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'
|
|
21
26
|
import { E } from '#generated/entities.ids.generated'
|
|
22
27
|
import {
|
|
23
|
-
|
|
28
|
+
buildRecurrenceRule,
|
|
24
29
|
buildInteractionPayload,
|
|
25
30
|
computeDurationMinutes,
|
|
26
31
|
createDefaultFormState,
|
|
27
|
-
defaultRepeatDaysForDateInput,
|
|
28
|
-
editorKindOfInteractionType,
|
|
29
|
-
KIND_CONFIG,
|
|
30
32
|
parseItemToFormState,
|
|
31
33
|
resolveSavedOwnerUserId,
|
|
32
34
|
type EditorFormState,
|
|
33
|
-
type EditorKind,
|
|
34
|
-
type EditorPriority,
|
|
35
35
|
} from '../../lib/calendar/editorPayload'
|
|
36
36
|
import type { ConflictScope } from '../../lib/calendar/preferences'
|
|
37
37
|
import { renderDictionaryIcon } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
|
|
@@ -39,14 +39,11 @@ import { normalizeCustomFieldSubmitValue } from '../detail/customFieldUtils'
|
|
|
39
39
|
import type { CalendarItem } from './types'
|
|
40
40
|
import { EDITOR_SCROLL_EVENT, Field } from './editor/inputs'
|
|
41
41
|
import { SegmentGroup } from './editor/SegmentGroup'
|
|
42
|
-
import { PriorityField } from './editor/PriorityField'
|
|
43
42
|
import { RelatedToField } from './editor/RelatedToField'
|
|
44
|
-
import { RepeatField } from './editor/RepeatField'
|
|
45
|
-
import { PeopleField } from './editor/PeopleField'
|
|
46
|
-
import { ResourcesField } from './editor/ResourcesField'
|
|
47
|
-
import { ScheduleSection } from './editor/ScheduleSection'
|
|
48
|
-
import { LocationField } from './editor/LocationField'
|
|
49
43
|
import { useConflictProbe, useEditorLabelResolution } from './editor/hooks'
|
|
44
|
+
import { EventTypePanel } from './editor/EventTypePanel'
|
|
45
|
+
import { eventTypeConfig, eventTypeOptions, isSelectableEventType, selectedEventType, useEventTypeCatalog } from './editor/useEventTypeCatalog'
|
|
46
|
+
import type { ScopedCalendarEventType } from '../../lib/calendar/eventTypeResolver'
|
|
50
47
|
|
|
51
48
|
export interface CalendarEventEditorProps {
|
|
52
49
|
open: boolean
|
|
@@ -54,7 +51,8 @@ export interface CalendarEventEditorProps {
|
|
|
54
51
|
item?: CalendarItem | null
|
|
55
52
|
defaultDate?: Date
|
|
56
53
|
defaultRange?: { start: Date; end: Date } | null
|
|
57
|
-
|
|
54
|
+
/** @deprecated Event-type labels are loaded from the scoped catalog. */
|
|
55
|
+
typeLabels?: Record<string, string>
|
|
58
56
|
typeIcons?: Record<string, string | null>
|
|
59
57
|
conflictScope?: ConflictScope
|
|
60
58
|
currentUserId?: string | null
|
|
@@ -65,13 +63,7 @@ export interface CalendarEventEditorProps {
|
|
|
65
63
|
}
|
|
66
64
|
|
|
67
65
|
const FORM_ID = 'customers-calendar-event-editor'
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const PEOPLE_FIELD_TEXT = {
|
|
71
|
-
attendees: { labelKey: 'customers.calendar.editor.attendees', label: 'Attendees', placeholderKey: 'customers.calendar.editor.addPeoplePlaceholder', placeholder: 'Add staff or customer…' },
|
|
72
|
-
participants: { labelKey: 'customers.calendar.editor.participants', label: 'Participants', placeholderKey: 'customers.calendar.editor.addPeoplePlaceholder', placeholder: 'Add staff or customer…' },
|
|
73
|
-
to: { labelKey: 'customers.calendar.editor.to', label: 'To', placeholderKey: 'customers.calendar.editor.addRecipientPlaceholder', placeholder: 'Add recipient…' },
|
|
74
|
-
} as const
|
|
66
|
+
const INTERACTION_INJECTION_SPOT = `crud-form:${E.customers.customer_interaction.replace(/:/g, '.')}`
|
|
75
67
|
|
|
76
68
|
// CrudForm values carry the flattened EditorFormState keys (seeded via
|
|
77
69
|
// initialValues) plus cf_* custom-field keys managed by CrudForm itself.
|
|
@@ -87,35 +79,108 @@ function customFieldInitialValues(item: CalendarItem): Record<string, unknown> {
|
|
|
87
79
|
)
|
|
88
80
|
}
|
|
89
81
|
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
function typeChangeDiscardFields(error: unknown): string[] | null {
|
|
83
|
+
if (!error || typeof error !== 'object') return null
|
|
84
|
+
const details = error as { status?: unknown; code?: unknown; fields?: unknown }
|
|
85
|
+
if (details.status !== 409 || details.code !== 'calendar_type_change_confirmation_required') return null
|
|
86
|
+
if (!Array.isArray(details.fields) || details.fields.length === 0 ||
|
|
87
|
+
!details.fields.every((field) => typeof field === 'string' && field.length > 0)) return null
|
|
88
|
+
return details.fields
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type EditorBodyProps = Pick<CrudFormGroupComponentProps, 'values' | 'errors' | 'setValue'> & {
|
|
92
92
|
open: boolean
|
|
93
93
|
isEdit: boolean
|
|
94
94
|
item?: CalendarItem | null
|
|
95
|
-
typeLabels: Record<string, string>
|
|
96
|
-
typeIcons: Record<string, string | null>
|
|
97
95
|
conflictScope: ConflictScope
|
|
98
96
|
currentUserId: string | null
|
|
99
97
|
resourcesEnabled: boolean
|
|
100
98
|
staffEnabled: boolean
|
|
99
|
+
saving: boolean
|
|
100
|
+
catalogItems: readonly ScopedCalendarEventType[]
|
|
101
|
+
catalogReady: boolean
|
|
102
|
+
catalogError: boolean
|
|
103
|
+
onRetryCatalog(): void
|
|
104
|
+
onSelectedTypeChange(key: string): void
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function CatalogStatusAlert({ error, onRetry }: { error: boolean; onRetry(): void }) {
|
|
108
|
+
const t = useT()
|
|
109
|
+
return (
|
|
110
|
+
<Alert status={error ? 'error' : 'information'} className="rounded-lg lg:col-span-2">
|
|
111
|
+
<AlertDescription>
|
|
112
|
+
{error
|
|
113
|
+
? t('customers.calendar.editor.catalogLoadFailed')
|
|
114
|
+
: t('customers.calendar.editor.catalogLoading')}
|
|
115
|
+
</AlertDescription>
|
|
116
|
+
{error ? (
|
|
117
|
+
<Button type="button" variant="outline" onClick={onRetry}>
|
|
118
|
+
{t('customers.calendar.errors.retry')}
|
|
119
|
+
</Button>
|
|
120
|
+
) : null}
|
|
121
|
+
</Alert>
|
|
122
|
+
)
|
|
101
123
|
}
|
|
102
124
|
|
|
103
|
-
function
|
|
104
|
-
|
|
125
|
+
const TimezoneField = React.memo(function TimezoneField({
|
|
126
|
+
value,
|
|
127
|
+
error,
|
|
128
|
+
disabled,
|
|
129
|
+
onChange,
|
|
130
|
+
}: {
|
|
131
|
+
value: string | null | undefined
|
|
132
|
+
error?: string
|
|
133
|
+
disabled: boolean
|
|
134
|
+
onChange(timezone: string): void
|
|
135
|
+
}) {
|
|
136
|
+
const t = useT()
|
|
137
|
+
const [open, setOpen] = React.useState(false)
|
|
138
|
+
const timezone = value ?? defaultCalendarTimezone()
|
|
139
|
+
const options = React.useMemo(() => open ? calendarTimezoneOptions(timezone) : [timezone], [open, timezone])
|
|
140
|
+
const items = React.useMemo(
|
|
141
|
+
() => options.map((timezone) => <SelectItem key={timezone} value={timezone}>{timezone}</SelectItem>),
|
|
142
|
+
[options],
|
|
143
|
+
)
|
|
144
|
+
return (
|
|
145
|
+
<Field label={t('customers.calendar.editor.timezone', 'Time zone')} error={error}>
|
|
146
|
+
<Select value={timezone} onValueChange={onChange} disabled={disabled} open={open} onOpenChange={setOpen}>
|
|
147
|
+
<SelectTrigger aria-label={t('customers.calendar.editor.timezone', 'Time zone')}><SelectValue /></SelectTrigger>
|
|
148
|
+
<SelectContent>{items}</SelectContent>
|
|
149
|
+
</Select>
|
|
150
|
+
</Field>
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const TypeSwitcher = React.memo(SegmentGroup) as typeof SegmentGroup
|
|
155
|
+
const NO_RESOURCES: EditorFormState['resources'] = []
|
|
156
|
+
const MemoRelatedToField = React.memo(RelatedToField)
|
|
157
|
+
|
|
158
|
+
// CrudForm re-invokes the group renderer on every one of its own renders;
|
|
159
|
+
// memoizing on the form state identities skips the ones that change neither
|
|
160
|
+
// the values nor the errors.
|
|
161
|
+
const EditorBody = React.memo(function EditorBody({
|
|
162
|
+
values,
|
|
163
|
+
errors,
|
|
164
|
+
setValue,
|
|
105
165
|
open,
|
|
106
166
|
isEdit,
|
|
107
167
|
item,
|
|
108
|
-
typeLabels,
|
|
109
|
-
typeIcons,
|
|
110
168
|
conflictScope,
|
|
111
169
|
currentUserId,
|
|
112
170
|
resourcesEnabled,
|
|
113
171
|
staffEnabled,
|
|
172
|
+
saving,
|
|
173
|
+
catalogItems,
|
|
174
|
+
catalogReady,
|
|
175
|
+
catalogError,
|
|
176
|
+
onRetryCatalog,
|
|
177
|
+
onSelectedTypeChange,
|
|
114
178
|
}: EditorBodyProps) {
|
|
115
179
|
const t = useT()
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
180
|
+
const form = formStateOfValues(values)
|
|
181
|
+
const selectedType = form.category ?? form.kind
|
|
182
|
+
const definition = selectedEventType(catalogItems, selectedType)
|
|
183
|
+
const config = eventTypeConfig(definition)
|
|
119
184
|
|
|
120
185
|
const update = React.useCallback(
|
|
121
186
|
(patch: Partial<EditorFormState>) => {
|
|
@@ -124,6 +189,23 @@ function EditorBody({
|
|
|
124
189
|
[setValue],
|
|
125
190
|
)
|
|
126
191
|
|
|
192
|
+
const updateTimezone = React.useCallback((timezone: string) => setValue('timezone', timezone), [setValue])
|
|
193
|
+
const selectType = React.useCallback((type: string) => {
|
|
194
|
+
const nextDefinition = selectedEventType(catalogItems, type)
|
|
195
|
+
update({ kind: nextDefinition.behavior.baseKind, category: type })
|
|
196
|
+
onSelectedTypeChange(type)
|
|
197
|
+
}, [catalogItems, update, onSelectedTypeChange])
|
|
198
|
+
const updateRelatedTo = React.useCallback((relatedTo: EditorFormState['relatedTo']) => update({ relatedTo }), [update])
|
|
199
|
+
const updateDeal = React.useCallback(
|
|
200
|
+
(deal: { id: string; label: string } | null) => update({ dealId: deal?.id ?? null, dealLabel: deal?.label ?? null }),
|
|
201
|
+
[update],
|
|
202
|
+
)
|
|
203
|
+
const deal = React.useMemo(
|
|
204
|
+
() => form.dealId && form.dealLabel ? { id: form.dealId, label: form.dealLabel } : null,
|
|
205
|
+
[form.dealId, form.dealLabel],
|
|
206
|
+
)
|
|
207
|
+
const capabilities = React.useMemo(() => ({ resourcesEnabled, staffEnabled }), [resourcesEnabled, staffEnabled])
|
|
208
|
+
|
|
127
209
|
useEditorLabelResolution(open, form, update)
|
|
128
210
|
// Probe against the owner the interaction will actually be SAVED with so the
|
|
129
211
|
// editor warning matches the grid's post-save conflict rings: tasks own via
|
|
@@ -133,24 +215,13 @@ function EditorBody({
|
|
|
133
215
|
// Self-exclude by the underlying interaction id (raw.id): findEditorConflictItems
|
|
134
216
|
// drops candidates by raw.id, and every expanded occurrence of a recurring series
|
|
135
217
|
// shares it — so the edited record never conflicts with itself.
|
|
136
|
-
|
|
218
|
+
// Resources count toward a conflict only when the selected type can book them.
|
|
219
|
+
const draftResources = resourcesEnabled && definition.behavior.fields.resources ? form.resources : NO_RESOURCES
|
|
220
|
+
const conflict = useConflictProbe(open, form, config, isEdit && item ? item.raw.id : null, draftOwnerUserId, conflictScope, currentUserId, draftResources)
|
|
137
221
|
|
|
138
|
-
const kindLabels = React.useMemo(
|
|
139
|
-
() => ({
|
|
140
|
-
meeting: t('customers.calendar.editor.types.meeting', 'meeting'),
|
|
141
|
-
call: t('customers.calendar.editor.types.call', 'call'),
|
|
142
|
-
email: t('customers.calendar.editor.types.email', 'email'),
|
|
143
|
-
note: t('customers.calendar.editor.types.note', 'note'),
|
|
144
|
-
event: t('customers.calendar.editor.types.event', 'event'),
|
|
145
|
-
task: t('customers.calendar.editor.types.task', 'task'),
|
|
146
|
-
}),
|
|
147
|
-
[t],
|
|
148
|
-
) satisfies Record<EditorKind, string>
|
|
149
|
-
|
|
150
|
-
const selectedType = form.category ?? form.kind
|
|
151
222
|
const typeOptions = React.useMemo(
|
|
152
|
-
() =>
|
|
153
|
-
[
|
|
223
|
+
() => eventTypeOptions(catalogItems, selectedType, t),
|
|
224
|
+
[catalogItems, selectedType, t],
|
|
154
225
|
)
|
|
155
226
|
const typeSwitcherOptions = React.useMemo(
|
|
156
227
|
() =>
|
|
@@ -162,18 +233,6 @@ function EditorBody({
|
|
|
162
233
|
[typeOptions],
|
|
163
234
|
)
|
|
164
235
|
|
|
165
|
-
// Candidates for the Call "insert phone from contact" button: the linked
|
|
166
|
-
// person (companies have no phone) + customer attendees. Their userId is the
|
|
167
|
-
// person id, so the picker can resolve primary_phone from the people API.
|
|
168
|
-
const phoneContactIds = React.useMemo(() => {
|
|
169
|
-
const ids: string[] = []
|
|
170
|
-
if (form.relatedTo && form.relatedTo.kind !== 'company') ids.push(form.relatedTo.id)
|
|
171
|
-
for (const participant of form.participants) {
|
|
172
|
-
if (participant.isCustomer && participant.userId) ids.push(participant.userId)
|
|
173
|
-
}
|
|
174
|
-
return Array.from(new Set(ids))
|
|
175
|
-
}, [form.relatedTo, form.participants])
|
|
176
|
-
|
|
177
236
|
const titleLabel = form.kind === 'email'
|
|
178
237
|
? t('customers.calendar.editor.titleLabel.email', 'Subject')
|
|
179
238
|
: form.kind === 'note'
|
|
@@ -181,11 +240,6 @@ function EditorBody({
|
|
|
181
240
|
: t('customers.calendar.editor.titleLabel.generic', 'Title')
|
|
182
241
|
|
|
183
242
|
return (
|
|
184
|
-
// Single column on phones (full-screen sheet). On lg+ the dialog widens and
|
|
185
|
-
// the fields group into two thematic columns: WHEN (schedule + repeat) on
|
|
186
|
-
// the left, CONTEXT (related record, category, location) on the right;
|
|
187
|
-
// people/resources and the task fields pair up below; title, description
|
|
188
|
-
// and the type switcher span both columns.
|
|
189
243
|
<div className="grid w-full grid-cols-1 items-start gap-4 lg:grid-cols-2 lg:gap-x-6">
|
|
190
244
|
{conflict ? (
|
|
191
245
|
<Alert status="warning" className="rounded-lg lg:col-span-2">
|
|
@@ -193,11 +247,20 @@ function EditorBody({
|
|
|
193
247
|
<AlertDescription>{conflict}</AlertDescription>
|
|
194
248
|
</Alert>
|
|
195
249
|
) : null}
|
|
250
|
+
{!catalogReady ? (
|
|
251
|
+
<CatalogStatusAlert error={catalogError} onRetry={onRetryCatalog} />
|
|
252
|
+
) : null}
|
|
253
|
+
{catalogReady && (definition.historical || catalogItems.some((item) => item.key === selectedType && !item.selectable)) ? (
|
|
254
|
+
<Alert status="warning" className="rounded-lg lg:col-span-2">
|
|
255
|
+
<AlertDescription>{t('customers.calendar.editor.historicalType')}</AlertDescription>
|
|
256
|
+
</Alert>
|
|
257
|
+
) : null}
|
|
196
258
|
<div className="w-full lg:col-span-2">
|
|
197
|
-
<
|
|
259
|
+
<TypeSwitcher<string>
|
|
198
260
|
ariaLabel={t('customers.calendar.editor.typeSwitcher', 'Event type')}
|
|
199
261
|
value={selectedType}
|
|
200
|
-
onChange={
|
|
262
|
+
onChange={selectType}
|
|
263
|
+
disabled={!catalogReady}
|
|
201
264
|
options={typeSwitcherOptions}
|
|
202
265
|
/>
|
|
203
266
|
</div>
|
|
@@ -212,127 +275,33 @@ function EditorBody({
|
|
|
212
275
|
size="lg"
|
|
213
276
|
/>
|
|
214
277
|
</Field>
|
|
215
|
-
<
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
endsError={errors.ends}
|
|
226
|
-
onAllDayChange={(allDay) => update({ allDay })}
|
|
227
|
-
onDateChange={(date) => {
|
|
228
|
-
const untouchedDefault =
|
|
229
|
-
JSON.stringify(form.repeatDays) === JSON.stringify(defaultRepeatDaysForDateInput(form.date))
|
|
230
|
-
update(
|
|
231
|
-
untouchedDefault
|
|
232
|
-
? { date, repeatDays: defaultRepeatDaysForDateInput(date) }
|
|
233
|
-
: { date },
|
|
234
|
-
)
|
|
235
|
-
}}
|
|
236
|
-
onStartTimeChange={(startTime) => update({ startTime })}
|
|
237
|
-
onEndDateChange={(endDate) => update({ endDate })}
|
|
238
|
-
onEndTimeChange={(endTime) => update({ endTime })}
|
|
239
|
-
/>
|
|
240
|
-
{config.hasRepeat ? (
|
|
241
|
-
<RepeatField
|
|
242
|
-
freq={form.repeatFreq}
|
|
243
|
-
days={form.repeatDays}
|
|
244
|
-
endType={form.repeatEndType}
|
|
245
|
-
count={form.repeatCount}
|
|
246
|
-
untilDate={form.repeatUntilDate}
|
|
247
|
-
onFreqChange={(repeatFreq) =>
|
|
248
|
-
update(
|
|
249
|
-
repeatFreq === 'weekly'
|
|
250
|
-
? { repeatFreq, repeatDays: defaultRepeatDaysForDateInput(form.date) }
|
|
251
|
-
: { repeatFreq },
|
|
252
|
-
)}
|
|
253
|
-
onToggleDay={(index) =>
|
|
254
|
-
update({ repeatDays: form.repeatDays.map((active, dayIndex) => (dayIndex === index ? !active : active)) })}
|
|
255
|
-
onEndTypeChange={(repeatEndType) => update({ repeatEndType })}
|
|
256
|
-
onCountChange={(repeatCount) => update({ repeatCount })}
|
|
257
|
-
onUntilDateChange={(repeatUntilDate) => update({ repeatUntilDate })}
|
|
258
|
-
/>
|
|
259
|
-
) : null}
|
|
260
|
-
{config.people && config.people !== 'assignee' ? (
|
|
261
|
-
<Field label={t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label)}>
|
|
262
|
-
<PeopleField
|
|
263
|
-
mode="multi"
|
|
264
|
-
includeCustomers
|
|
265
|
-
includeStaff={staffEnabled}
|
|
266
|
-
placeholder={t(PEOPLE_FIELD_TEXT[config.people].placeholderKey, PEOPLE_FIELD_TEXT[config.people].placeholder)}
|
|
267
|
-
ariaLabel={t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label)}
|
|
268
|
-
value={form.participants}
|
|
269
|
-
onChange={(participants) => update({ participants })}
|
|
278
|
+
<TimezoneField value={form.timezone} error={errors.timezone} disabled={saving} onChange={updateTimezone} />
|
|
279
|
+
<div className="lg:col-span-2">
|
|
280
|
+
<Field label={t('customers.calendar.editor.relatedTo', 'Related to')} error={errors.relatedTo}>
|
|
281
|
+
<MemoRelatedToField
|
|
282
|
+
label={t('customers.calendar.editor.relatedTo', 'Related to')}
|
|
283
|
+
value={form.relatedTo}
|
|
284
|
+
deal={deal}
|
|
285
|
+
onChange={updateRelatedTo}
|
|
286
|
+
onDealChange={updateDeal}
|
|
287
|
+
error={errors.relatedTo}
|
|
270
288
|
/>
|
|
271
289
|
</Field>
|
|
272
|
-
) : null}
|
|
273
|
-
{config.people === 'assignee' && staffEnabled ? (
|
|
274
|
-
<Field label={t('customers.calendar.editor.assignee', 'Assignee')} error={errors.assignee}>
|
|
275
|
-
<PeopleField
|
|
276
|
-
mode="single"
|
|
277
|
-
includeCustomers={false}
|
|
278
|
-
includeStaff
|
|
279
|
-
placeholder={t('customers.calendar.editor.assigneePlaceholder', 'Assign to a team member…')}
|
|
280
|
-
ariaLabel={t('customers.calendar.editor.assignee', 'Assignee')}
|
|
281
|
-
value={form.assigneeUserId
|
|
282
|
-
? [{ userId: form.assigneeUserId, name: form.assigneeName ?? form.assigneeUserId, isCustomer: false }]
|
|
283
|
-
: []}
|
|
284
|
-
onChange={(entries) => {
|
|
285
|
-
const next = entries[entries.length - 1] ?? null
|
|
286
|
-
update({ assigneeUserId: next?.userId ?? null, assigneeName: next?.name ?? null })
|
|
287
|
-
}}
|
|
288
|
-
/>
|
|
289
|
-
</Field>
|
|
290
|
-
) : null}
|
|
291
290
|
</div>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
{config.location ? (
|
|
304
|
-
<LocationField
|
|
305
|
-
variant={config.location}
|
|
306
|
-
value={form.location}
|
|
307
|
-
onChange={(location) => update({ location })}
|
|
308
|
-
phoneContactIds={phoneContactIds}
|
|
309
|
-
/>
|
|
310
|
-
) : null}
|
|
311
|
-
{resourcesEnabled ? (
|
|
312
|
-
<Field label={t('customers.calendar.editor.resources', 'Resources')}>
|
|
313
|
-
<ResourcesField
|
|
314
|
-
placeholder={t('customers.calendar.editor.resourcesPlaceholder', 'Add a resource…')}
|
|
315
|
-
ariaLabel={t('customers.calendar.editor.resources', 'Resources')}
|
|
316
|
-
value={form.resources}
|
|
317
|
-
onChange={(resources) => update({ resources })}
|
|
318
|
-
/>
|
|
319
|
-
</Field>
|
|
320
|
-
) : null}
|
|
321
|
-
{config.hasPriority ? (
|
|
322
|
-
<Field label={t('customers.calendar.editor.priority.label', 'Priority')}>
|
|
323
|
-
<PriorityField
|
|
324
|
-
value={form.priority}
|
|
325
|
-
ariaLabel={t('customers.calendar.editor.priority.label', 'Priority')}
|
|
326
|
-
labels={{
|
|
327
|
-
low: t('customers.calendar.editor.priority.low', 'Low'),
|
|
328
|
-
medium: t('customers.calendar.editor.priority.medium', 'Medium'),
|
|
329
|
-
high: t('customers.calendar.editor.priority.high', 'High'),
|
|
330
|
-
}}
|
|
331
|
-
onChange={(priority) => update({ priority })}
|
|
291
|
+
{catalogReady ? (
|
|
292
|
+
<div className="lg:col-span-2">
|
|
293
|
+
<EventTypePanel
|
|
294
|
+
definition={definition}
|
|
295
|
+
panelKey={definition.panelKey}
|
|
296
|
+
mode={isEdit ? 'edit' : 'create'}
|
|
297
|
+
values={values}
|
|
298
|
+
errors={errors}
|
|
299
|
+
disabled={saving}
|
|
300
|
+
capabilities={capabilities}
|
|
301
|
+
setValue={setValue}
|
|
332
302
|
/>
|
|
333
|
-
</
|
|
303
|
+
</div>
|
|
334
304
|
) : null}
|
|
335
|
-
</div>
|
|
336
305
|
<Field label={t('customers.calendar.editor.description', 'Description')} className="lg:col-span-2">
|
|
337
306
|
<Textarea
|
|
338
307
|
value={form.description}
|
|
@@ -344,7 +313,7 @@ function EditorBody({
|
|
|
344
313
|
</Field>
|
|
345
314
|
</div>
|
|
346
315
|
)
|
|
347
|
-
}
|
|
316
|
+
})
|
|
348
317
|
|
|
349
318
|
export function CalendarEventEditor({
|
|
350
319
|
open,
|
|
@@ -352,8 +321,6 @@ export function CalendarEventEditor({
|
|
|
352
321
|
item,
|
|
353
322
|
defaultDate,
|
|
354
323
|
defaultRange,
|
|
355
|
-
typeLabels,
|
|
356
|
-
typeIcons,
|
|
357
324
|
conflictScope,
|
|
358
325
|
currentUserId,
|
|
359
326
|
resourcesEnabled,
|
|
@@ -363,7 +330,54 @@ export function CalendarEventEditor({
|
|
|
363
330
|
}: CalendarEventEditorProps) {
|
|
364
331
|
const t = useT()
|
|
365
332
|
const [saving, setSaving] = React.useState(false)
|
|
333
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
334
|
+
const dirtyRef = React.useRef(false)
|
|
335
|
+
const closeConfirmPendingRef = React.useRef(false)
|
|
336
|
+
const [closePromptOpen, setClosePromptOpen] = React.useState(false)
|
|
337
|
+
// The shared confirm answers Cmd/Ctrl+Enter with its confirm action, which
|
|
338
|
+
// here discards the draft — on the very shortcut that saves the form. The
|
|
339
|
+
// shortcut is swallowed while the prompt is up; the buttons and Escape (keep
|
|
340
|
+
// editing) still answer it. Owned by an effect so the listener is removed
|
|
341
|
+
// even when the editor unmounts with the prompt still open.
|
|
342
|
+
React.useEffect(() => {
|
|
343
|
+
if (!closePromptOpen) return
|
|
344
|
+
const swallowSubmitShortcut = (event: KeyboardEvent) => {
|
|
345
|
+
if (event.key !== 'Enter' || !(event.metaKey || event.ctrlKey)) return
|
|
346
|
+
event.preventDefault()
|
|
347
|
+
event.stopImmediatePropagation()
|
|
348
|
+
}
|
|
349
|
+
window.addEventListener('keydown', swallowSubmitShortcut, true)
|
|
350
|
+
return () => window.removeEventListener('keydown', swallowSubmitShortcut, true)
|
|
351
|
+
}, [closePromptOpen])
|
|
352
|
+
const requestClose = React.useCallback(async () => {
|
|
353
|
+
if (saving || closeConfirmPendingRef.current) return
|
|
354
|
+
if (dirtyRef.current) {
|
|
355
|
+
closeConfirmPendingRef.current = true
|
|
356
|
+
setClosePromptOpen(true)
|
|
357
|
+
try {
|
|
358
|
+
const confirmed = await confirm({
|
|
359
|
+
title: t('ui.forms.confirmUnsavedChanges', 'You have unsaved changes. Are you sure you want to leave?'),
|
|
360
|
+
confirmText: t('customers.deals.detail.unsavedConfirm', 'Discard changes'),
|
|
361
|
+
cancelText: t('customers.deals.detail.unsavedCancel', 'Keep editing'),
|
|
362
|
+
variant: 'destructive',
|
|
363
|
+
})
|
|
364
|
+
if (!confirmed) return
|
|
365
|
+
} finally {
|
|
366
|
+
closeConfirmPendingRef.current = false
|
|
367
|
+
setClosePromptOpen(false)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
onOpenChange(false)
|
|
371
|
+
}, [confirm, onOpenChange, saving, t])
|
|
372
|
+
const handleDirtyChange = React.useCallback((dirty: boolean) => { dirtyRef.current = dirty }, [])
|
|
373
|
+
React.useEffect(() => { if (!open) dirtyRef.current = false }, [open])
|
|
366
374
|
const isEdit = mode === 'edit' && Boolean(item?.id)
|
|
375
|
+
const catalog = useEventTypeCatalog(open)
|
|
376
|
+
const { items: catalogItems, status: catalogStatus, retry: retryCatalog } = catalog
|
|
377
|
+
const [selectedTypeKey, setSelectedTypeKey] = React.useState(() => isEdit && item ? item.interactionType : 'meeting')
|
|
378
|
+
const autoSelectedCreateType = !isEdit && catalog.status === 'ready' && !isSelectableEventType(catalog.items, 'meeting')
|
|
379
|
+
? catalog.items.find((entry) => entry.selectable && !entry.historical) ?? null
|
|
380
|
+
: null
|
|
367
381
|
|
|
368
382
|
// CrudForm reads initialValues once, so every dialog open gets a fresh form
|
|
369
383
|
// instance keyed by the open sequence + edited record.
|
|
@@ -373,7 +387,7 @@ export function CalendarEventEditor({
|
|
|
373
387
|
if (open && !wasOpenRef.current) setOpenSeq((seq) => seq + 1)
|
|
374
388
|
wasOpenRef.current = open
|
|
375
389
|
}, [open])
|
|
376
|
-
const formKey = `${mode}:${item?.id ?? 'new'}:${openSeq}`
|
|
390
|
+
const formKey = `${mode}:${item?.id ?? 'new'}:${openSeq}:${autoSelectedCreateType?.key ?? 'default'}`
|
|
377
391
|
|
|
378
392
|
const initialValues = React.useMemo<Record<string, unknown>>(() => {
|
|
379
393
|
if (isEdit && item) {
|
|
@@ -384,22 +398,56 @@ export function CalendarEventEditor({
|
|
|
384
398
|
updatedAt: item.updatedAt ?? undefined,
|
|
385
399
|
}
|
|
386
400
|
}
|
|
387
|
-
|
|
401
|
+
const defaults = createDefaultFormState(defaultDate ?? null, undefined, defaultRange ?? null)
|
|
402
|
+
if (!autoSelectedCreateType) return { ...defaults }
|
|
403
|
+
return {
|
|
404
|
+
...defaults,
|
|
405
|
+
kind: autoSelectedCreateType.behavior.baseKind,
|
|
406
|
+
category: autoSelectedCreateType.key,
|
|
407
|
+
}
|
|
388
408
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- openSeq re-seeds defaults per dialog open
|
|
389
|
-
}, [isEdit, item, defaultDate, defaultRange, openSeq])
|
|
409
|
+
}, [isEdit, item, defaultDate, defaultRange, openSeq, autoSelectedCreateType])
|
|
410
|
+
const initialTypeKey = String(initialValues.category ?? initialValues.kind ?? 'meeting')
|
|
411
|
+
React.useEffect(() => { setSelectedTypeKey(initialTypeKey) }, [initialTypeKey, formKey])
|
|
412
|
+
const selectedDefinition = selectedEventType(catalog.items, selectedTypeKey)
|
|
413
|
+
const customFields = useCalendarCustomFields(open, E.customers.customer_interaction, selectedDefinition.behavior.customFieldsetIds)
|
|
414
|
+
const injectionWidgets = useInjectionWidgets(INTERACTION_INJECTION_SPOT)
|
|
415
|
+
const canCreateType = isSelectableEventType(catalog.items, selectedTypeKey)
|
|
416
|
+
const formReady = isEdit || catalog.status === 'ready'
|
|
390
417
|
|
|
391
418
|
const handleSubmit = React.useCallback(
|
|
392
419
|
async (values: Record<string, unknown>) => {
|
|
393
420
|
const form = formStateOfValues(values)
|
|
394
|
-
|
|
421
|
+
if (catalogStatus !== 'ready' || customFields.status !== 'ready' || injectionWidgets.loading || injectionWidgets.error) {
|
|
422
|
+
throw createCrudFormError(t('customers.calendar.editor.catalogLoadFailed'))
|
|
423
|
+
}
|
|
424
|
+
const definition = selectedEventType(catalogItems, form.category ?? form.kind)
|
|
425
|
+
const selectedKey = form.category ?? form.kind
|
|
426
|
+
if ((mode === 'create' || selectedKey !== initialTypeKey) &&
|
|
427
|
+
!isSelectableEventType(catalogItems, selectedKey)) {
|
|
428
|
+
throw createCrudFormError(t('customers.calendar.editor.typeUnavailable'))
|
|
429
|
+
}
|
|
430
|
+
const config = eventTypeConfig(definition)
|
|
395
431
|
const fieldErrors: Record<string, string> = {}
|
|
432
|
+
const timezone = form.timezone ?? defaultCalendarTimezone()
|
|
433
|
+
const startTime = form.allDay && config.hasAllDay ? '00:00' : form.startTime
|
|
434
|
+
if (!isCalendarTimezone(timezone)) fieldErrors.timezone = t('customers.calendar.editor.validation.timezoneInvalid', 'Choose a valid time zone')
|
|
435
|
+
else if (!(form.allDay && config.hasAllDay ? calendarDayStartInstant(form.date, timezone) : calendarWallTimeToInstant(form.date, startTime, timezone))) {
|
|
436
|
+
fieldErrors[form.allDay && config.hasAllDay ? 'date' : 'startTime'] = t('customers.calendar.editor.validation.timezoneGap', 'This local time does not exist in the selected time zone')
|
|
437
|
+
}
|
|
438
|
+
if (config.hasEnd && !form.allDay && isCalendarTimezone(timezone) && !calendarWallTimeToInstant(form.endDate, form.endTime, timezone)) {
|
|
439
|
+
fieldErrors.ends = t('customers.calendar.editor.validation.timezoneGap', 'This local time does not exist in the selected time zone')
|
|
440
|
+
}
|
|
441
|
+
if (config.hasRepeat && form.repeatFreq !== 'none' && isCalendarTimezone(timezone) && form.repeatEndType === 'date' && form.repeatUntilDate && !calendarDayEndInstant(form.repeatUntilDate, timezone)) {
|
|
442
|
+
fieldErrors.repeatUntilDate = t('customers.calendar.editor.validation.timezoneGap', 'This local time does not exist in the selected time zone')
|
|
443
|
+
}
|
|
396
444
|
if (!form.title.trim()) {
|
|
397
445
|
fieldErrors.title = t('customers.calendar.editor.validation.titleRequired', 'Title is required')
|
|
398
446
|
}
|
|
399
447
|
if (!form.relatedTo) {
|
|
400
448
|
fieldErrors.relatedTo = t('customers.calendar.editor.validation.relatedToRequired', 'Select a person or company to link this event')
|
|
401
449
|
}
|
|
402
|
-
if (config.hasEnd && !form.allDay && computeDurationMinutes(form) === null) {
|
|
450
|
+
if (!fieldErrors.ends && !fieldErrors.startTime && !fieldErrors.date && config.hasEnd && !form.allDay && computeDurationMinutes(form) === null) {
|
|
403
451
|
fieldErrors.ends = t('customers.calendar.editor.validation.endsBeforeStarts', 'End must be after start')
|
|
404
452
|
}
|
|
405
453
|
// A task must be assigned to a team member — surface the requirement inline
|
|
@@ -410,25 +458,89 @@ export function CalendarEventEditor({
|
|
|
410
458
|
if (Object.keys(fieldErrors).length > 0) {
|
|
411
459
|
throw createCrudFormError(Object.values(fieldErrors)[0], fieldErrors)
|
|
412
460
|
}
|
|
461
|
+
const customValidation = validateCalendarCustomValues(values, customFields.definitions)
|
|
462
|
+
if (!customValidation.ok) {
|
|
463
|
+
const translatedErrors = Object.fromEntries(Object.entries(customValidation.fieldErrors).map(([key, message]) => [key, t(message, message)]))
|
|
464
|
+
throw createCrudFormError(Object.values(translatedErrors)[0], translatedErrors)
|
|
465
|
+
}
|
|
413
466
|
setSaving(true)
|
|
414
467
|
try {
|
|
415
|
-
const payload = buildInteractionPayload(form, {
|
|
468
|
+
const payload = buildInteractionPayload({ ...form, timezone }, {
|
|
469
|
+
config,
|
|
416
470
|
mode,
|
|
417
471
|
id: item?.id,
|
|
418
472
|
resourcesEnabled: resourcesEnabled === true,
|
|
419
473
|
staffEnabled: staffEnabled !== false,
|
|
420
474
|
})
|
|
421
|
-
|
|
475
|
+
payload.enforceSelectableType = true
|
|
476
|
+
const applicable = definition.behavior.fields
|
|
477
|
+
const time = form.allDay && applicable.allDay ? '00:00' : form.startTime
|
|
478
|
+
payload.time = time
|
|
479
|
+
payload.scheduledAt = (form.allDay && applicable.allDay ? calendarDayStartInstant(form.date, timezone) : calendarWallTimeToInstant(form.date, time, timezone))!.toISOString()
|
|
480
|
+
payload.timezone = timezone
|
|
481
|
+
payload.allDay = applicable.allDay ? form.allDay : null
|
|
482
|
+
payload.durationMinutes = applicable.endTime && !(form.allDay && applicable.allDay) ? computeDurationMinutes(form) : null
|
|
483
|
+
payload.location = applicable.location === 'none' ? null : form.location.trim() || null
|
|
484
|
+
const recurrenceRule = applicable.recurrence ? buildRecurrenceRule(form) : null
|
|
485
|
+
payload.recurrenceRule = recurrenceRule
|
|
486
|
+
payload.recurrenceEnd = recurrenceRule && form.repeatEndType === 'date' && form.repeatUntilDate
|
|
487
|
+
? calendarDayEndInstant(form.repeatUntilDate, timezone)!.toISOString()
|
|
488
|
+
: null
|
|
489
|
+
payload.participants = applicable.people !== 'none' && applicable.people !== 'assignee' && form.participants.length
|
|
490
|
+
? form.participants.map((participant) => ({
|
|
491
|
+
userId: participant.userId,
|
|
492
|
+
name: participant.name,
|
|
493
|
+
email: participant.email,
|
|
494
|
+
status: participant.isCustomer ? 'customer' : 'pending',
|
|
495
|
+
}))
|
|
496
|
+
: null
|
|
497
|
+
if (applicable.people === 'assignee' && staffEnabled !== false) payload.ownerUserId = form.assigneeUserId ?? null
|
|
498
|
+
else delete payload.ownerUserId
|
|
499
|
+
if (applicable.priority) payload.priority = form.priority === 'low' ? 10 : form.priority === 'high' ? 90 : 50
|
|
500
|
+
else delete payload.priority
|
|
501
|
+
if (!applicable.resources) delete payload.linkedEntities
|
|
502
|
+
const custom = collectCustomFieldValues(projectCalendarCustomValues(values, customFields.definitions), {
|
|
422
503
|
transform: (value) => normalizeCustomFieldSubmitValue(value),
|
|
423
504
|
})
|
|
424
505
|
for (const [key, value] of Object.entries(custom)) payload[`cf_${key}`] = value
|
|
425
506
|
// CrudForm supplies the optimistic-lock header (auto-derived from
|
|
426
507
|
// initialValues.updatedAt) via scoped request headers around onSubmit.
|
|
427
|
-
|
|
508
|
+
const submitInteraction = (confirmDiscardInapplicableValues: boolean) => apiCallOrThrow('/api/customers/interactions', {
|
|
428
509
|
method: isEdit ? 'PUT' : 'POST',
|
|
429
510
|
headers: { 'content-type': 'application/json' },
|
|
430
|
-
body: JSON.stringify(
|
|
511
|
+
body: JSON.stringify(confirmDiscardInapplicableValues
|
|
512
|
+
? { ...payload, confirmDiscardInapplicableValues: true }
|
|
513
|
+
: payload),
|
|
431
514
|
})
|
|
515
|
+
try {
|
|
516
|
+
await submitInteraction(false)
|
|
517
|
+
} catch (error) {
|
|
518
|
+
const fields = typeChangeDiscardFields(error)
|
|
519
|
+
if (!fields) throw error
|
|
520
|
+
const approved = await confirm({
|
|
521
|
+
title: t('customers.calendar.editor.discardTitle'),
|
|
522
|
+
text: `${t('customers.calendar.editor.discardDescription')} ${t('customers.calendar.editor.discardFields')} ${fields.map((field) => {
|
|
523
|
+
const definition = customFields.allDefinitions.find((entry) => `cf_${entry.key}` === field)
|
|
524
|
+
if (definition?.label) return definition.label
|
|
525
|
+
const labelKeys: Record<string, string> = {
|
|
526
|
+
location: 'customers.calendar.editor.location',
|
|
527
|
+
participants: 'customers.calendar.editor.attendees',
|
|
528
|
+
linkedEntities: 'customers.calendar.editor.resources',
|
|
529
|
+
allDay: 'customers.calendar.editor.allDay',
|
|
530
|
+
durationMinutes: 'customers.calendar.editor.dates.ends',
|
|
531
|
+
priority: 'customers.calendar.editor.priority.label',
|
|
532
|
+
recurrenceRule: 'customers.calendar.editor.repeat.label',
|
|
533
|
+
recurrenceEnd: 'customers.calendar.editor.repeat.ends',
|
|
534
|
+
}
|
|
535
|
+
return t(labelKeys[field] ?? 'customers.calendar.editor.discardUnknownField')
|
|
536
|
+
}).join(', ')}`,
|
|
537
|
+
confirmText: t('customers.calendar.editor.discardConfirm'),
|
|
538
|
+
cancelText: t('customers.calendar.editor.cancel'),
|
|
539
|
+
variant: 'destructive',
|
|
540
|
+
})
|
|
541
|
+
if (!approved) return
|
|
542
|
+
await submitInteraction(true)
|
|
543
|
+
}
|
|
432
544
|
flash(t('customers.calendar.editor.saved', 'Event saved'), 'success')
|
|
433
545
|
onOpenChange(false)
|
|
434
546
|
requestAnimationFrame(() => { onSaved() })
|
|
@@ -445,7 +557,7 @@ export function CalendarEventEditor({
|
|
|
445
557
|
setSaving(false)
|
|
446
558
|
}
|
|
447
559
|
},
|
|
448
|
-
[isEdit, item?.id, mode, onOpenChange, onSaved, resourcesEnabled, staffEnabled, t],
|
|
560
|
+
[catalogItems, catalogStatus, customFields.status, customFields.definitions, customFields.allDefinitions, injectionWidgets.loading, injectionWidgets.error, confirm, initialTypeKey, isEdit, item?.id, mode, onOpenChange, onSaved, resourcesEnabled, staffEnabled, t],
|
|
449
561
|
)
|
|
450
562
|
|
|
451
563
|
const groups = React.useMemo<CrudFormGroup[]>(
|
|
@@ -455,36 +567,43 @@ export function CalendarEventEditor({
|
|
|
455
567
|
bare: true,
|
|
456
568
|
component: (ctx) => (
|
|
457
569
|
<EditorBody
|
|
458
|
-
|
|
570
|
+
values={ctx.values}
|
|
571
|
+
errors={ctx.errors}
|
|
572
|
+
setValue={ctx.setValue}
|
|
459
573
|
open={open}
|
|
460
574
|
isEdit={isEdit}
|
|
461
575
|
item={item}
|
|
462
|
-
typeLabels={typeLabels}
|
|
463
|
-
typeIcons={typeIcons ?? {}}
|
|
464
576
|
conflictScope={conflictScope ?? 'all'}
|
|
465
577
|
currentUserId={currentUserId ?? null}
|
|
466
578
|
resourcesEnabled={resourcesEnabled === true}
|
|
467
579
|
staffEnabled={staffEnabled !== false}
|
|
580
|
+
saving={saving}
|
|
581
|
+
catalogItems={catalogItems}
|
|
582
|
+
catalogReady={catalogStatus === 'ready'}
|
|
583
|
+
catalogError={catalogStatus === 'error'}
|
|
584
|
+
onRetryCatalog={retryCatalog}
|
|
585
|
+
onSelectedTypeChange={setSelectedTypeKey}
|
|
468
586
|
/>
|
|
469
587
|
),
|
|
470
588
|
},
|
|
471
|
-
{ id: 'customFields',
|
|
589
|
+
...(customFields.fields.length ? [{ id: 'customFields', title: t('entities.customFields.title'), fields: customFields.fields.map((field) => field.id) }] : []),
|
|
472
590
|
],
|
|
473
|
-
[open, isEdit, item,
|
|
591
|
+
[open, isEdit, item, conflictScope, currentUserId, resourcesEnabled, staffEnabled, saving, catalogItems, catalogStatus, retryCatalog, customFields.fields, t],
|
|
474
592
|
)
|
|
475
593
|
|
|
476
594
|
const handleKeyDown = useDialogKeyHandler({
|
|
595
|
+
onCancel: () => { void requestClose() },
|
|
477
596
|
onConfirm: () => {
|
|
478
597
|
const formElement = document.getElementById(FORM_ID)
|
|
479
598
|
if (formElement instanceof HTMLFormElement) formElement.requestSubmit()
|
|
480
599
|
},
|
|
481
|
-
disabled: saving,
|
|
600
|
+
disabled: saving || customFields.status !== 'ready' || injectionWidgets.loading || !!injectionWidgets.error || catalog.status !== 'ready' || (mode === 'create' && !canCreateType),
|
|
482
601
|
})
|
|
483
602
|
|
|
484
603
|
const dialogTitle = isEdit ? t('customers.calendar.editor.title.edit', 'Edit event') : t('customers.calendar.editor.title.create', 'New event')
|
|
485
604
|
|
|
486
605
|
return (
|
|
487
|
-
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
606
|
+
<Dialog open={open} onOpenChange={(nextOpen) => { if (nextOpen) onOpenChange(true); else void requestClose() }}>
|
|
488
607
|
<DialogContent
|
|
489
608
|
onKeyDown={handleKeyDown}
|
|
490
609
|
// Only dismiss on a genuine click OUTSIDE the whole editor. Radix
|
|
@@ -492,10 +611,13 @@ export function CalendarEventEditor({
|
|
|
492
611
|
// DatePicker/Select popover) up to the dialog as an "interact
|
|
493
612
|
// outside" event when it dismisses that popover — without this guard
|
|
494
613
|
// it would tear down the entire editor. Keep it open when the target
|
|
495
|
-
// is inside the dialog body or inside any portalled popover.
|
|
614
|
+
// is inside the dialog body or inside any portalled popover. The
|
|
615
|
+
// shared confirm renders as a native modal `<dialog>` on top of the
|
|
616
|
+
// editor; answering it is not an outside click either, otherwise it
|
|
617
|
+
// would re-request the close it is asking about.
|
|
496
618
|
onInteractOutside={(event) => {
|
|
497
619
|
const target = event.detail.originalEvent.target as HTMLElement | null
|
|
498
|
-
if (target?.closest('[data-dialog-content]') || target?.closest('[data-radix-popper-content-wrapper]')) {
|
|
620
|
+
if (target?.closest('[data-dialog-content]') || target?.closest('[data-radix-popper-content-wrapper]') || target?.closest('dialog')) {
|
|
499
621
|
event.preventDefault()
|
|
500
622
|
}
|
|
501
623
|
}}
|
|
@@ -512,7 +634,7 @@ export function CalendarEventEditor({
|
|
|
512
634
|
<IconButton
|
|
513
635
|
variant="ghost"
|
|
514
636
|
size="sm"
|
|
515
|
-
onClick={() =>
|
|
637
|
+
onClick={() => { void requestClose() }}
|
|
516
638
|
aria-label={t('customers.calendar.editor.close', 'Close')}
|
|
517
639
|
className="shrink-0 text-muted-foreground"
|
|
518
640
|
>
|
|
@@ -530,18 +652,24 @@ export function CalendarEventEditor({
|
|
|
530
652
|
}}
|
|
531
653
|
>
|
|
532
654
|
<div className="px-4 py-4 sm:px-6 sm:py-5">
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
655
|
+
{formReady ? (
|
|
656
|
+
<CrudForm<Record<string, unknown>>
|
|
657
|
+
key={formKey}
|
|
658
|
+
formId={FORM_ID}
|
|
659
|
+
embedded
|
|
660
|
+
trackDirtyWhenEmbedded
|
|
661
|
+
onDirtyChange={handleDirtyChange}
|
|
662
|
+
hideFooterActions
|
|
663
|
+
customFieldsManageMode="page"
|
|
664
|
+
fields={customFields.fields}
|
|
665
|
+
groups={groups}
|
|
666
|
+
initialValues={initialValues}
|
|
667
|
+
injectionSpotId={INTERACTION_INJECTION_SPOT}
|
|
668
|
+
onSubmit={handleSubmit}
|
|
669
|
+
/>
|
|
670
|
+
) : (
|
|
671
|
+
<CatalogStatusAlert error={catalog.status === 'error'} onRetry={catalog.retry} />
|
|
672
|
+
)}
|
|
545
673
|
</div>
|
|
546
674
|
</div>
|
|
547
675
|
<div className="flex shrink-0 items-center justify-end gap-3 border-t border-border bg-background px-5 py-4">
|
|
@@ -555,12 +683,13 @@ export function CalendarEventEditor({
|
|
|
555
683
|
<Button
|
|
556
684
|
type="submit"
|
|
557
685
|
form={FORM_ID}
|
|
558
|
-
disabled={saving}
|
|
686
|
+
disabled={saving || customFields.status !== 'ready' || injectionWidgets.loading || !!injectionWidgets.error || catalog.status !== 'ready' || (mode === 'create' && !canCreateType)}
|
|
559
687
|
>
|
|
560
688
|
{saving ? t('customers.calendar.editor.saving', 'Saving…') : t('customers.calendar.editor.save', 'Save event')}
|
|
561
689
|
</Button>
|
|
562
690
|
</div>
|
|
563
691
|
</DialogContent>
|
|
692
|
+
{ConfirmDialogElement}
|
|
564
693
|
</Dialog>
|
|
565
694
|
)
|
|
566
695
|
}
|