@open-mercato/core 0.8.1-develop.7452.1.5e211edd51 → 0.8.1-develop.7455.1.8f7e53badb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
  3. package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
  4. package/dist/generated/entities/customer_interaction/index.js +2 -0
  5. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  9. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  10. package/dist/modules/customers/api/activity-types/route.js +151 -0
  11. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  12. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  13. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  14. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  15. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  16. package/dist/modules/customers/api/interactions/route.js +3 -0
  17. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  18. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  19. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  20. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  21. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  22. package/dist/modules/customers/calendar-event-types.js +548 -0
  23. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  24. package/dist/modules/customers/commands/dictionaries.js +70 -10
  25. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  26. package/dist/modules/customers/commands/interactions.js +187 -22
  27. package/dist/modules/customers/commands/interactions.js.map +3 -3
  28. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  29. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  30. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  31. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  32. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  33. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  34. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  35. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  36. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  37. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  38. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  39. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  40. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  41. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  42. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  43. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  44. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  45. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  46. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  47. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  48. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  49. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  50. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  51. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  52. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  53. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  54. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  55. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  56. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  57. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  58. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  59. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  60. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  61. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  62. package/dist/modules/customers/components/calendar/types.js +1 -0
  63. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  64. package/dist/modules/customers/data/entities.js +6 -0
  65. package/dist/modules/customers/data/entities.js.map +2 -2
  66. package/dist/modules/customers/data/validators.js +39 -10
  67. package/dist/modules/customers/data/validators.js.map +2 -2
  68. package/dist/modules/customers/di.js +20 -1
  69. package/dist/modules/customers/di.js.map +2 -2
  70. package/dist/modules/customers/extension-points.js +13 -0
  71. package/dist/modules/customers/extension-points.js.map +2 -2
  72. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  73. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  74. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  75. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  76. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  77. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  78. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  79. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  80. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  81. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  82. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  83. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  84. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  85. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  86. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  87. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  88. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  89. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  90. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  91. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  92. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  93. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  94. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  95. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  96. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  97. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  98. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  99. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  100. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  101. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  102. package/generated/entities/customer_interaction/index.ts +1 -0
  103. package/generated/entity-fields-registry.ts +2 -0
  104. package/package.json +7 -7
  105. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  106. package/src/modules/customers/api/activity-types/route.ts +154 -0
  107. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  108. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  109. package/src/modules/customers/api/interactions/route.ts +4 -0
  110. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  111. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  112. package/src/modules/customers/calendar-event-types.ts +674 -0
  113. package/src/modules/customers/commands/dictionaries.ts +84 -8
  114. package/src/modules/customers/commands/interactions.ts +244 -21
  115. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  116. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  117. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  118. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  119. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  120. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  121. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  122. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  123. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  124. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  125. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  126. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  127. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  128. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  129. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  130. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  131. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  132. package/src/modules/customers/components/calendar/types.ts +1 -0
  133. package/src/modules/customers/components/detail/types.ts +1 -0
  134. package/src/modules/customers/data/entities.ts +8 -1
  135. package/src/modules/customers/data/validators.ts +42 -7
  136. package/src/modules/customers/di.ts +21 -0
  137. package/src/modules/customers/extension-points.ts +13 -0
  138. package/src/modules/customers/i18n/de.json +83 -0
  139. package/src/modules/customers/i18n/en.json +83 -0
  140. package/src/modules/customers/i18n/es.json +83 -0
  141. package/src/modules/customers/i18n/ko.json +83 -0
  142. package/src/modules/customers/i18n/pl.json +83 -0
  143. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  144. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  145. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  146. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  147. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  148. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  149. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  150. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  151. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  152. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  153. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  154. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  155. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  156. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  157. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  158. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
@@ -8,25 +8,27 @@ 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 } from "@open-mercato/ui/backend/CrudForm";
14
+ import { useInjectionWidgets } from "@open-mercato/ui/backend/injection/InjectionSpot";
15
+ import { projectCalendarCustomValues, useCalendarCustomFields, validateCalendarCustomValues } from "./editor/useCalendarCustomFields.js";
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.js";
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.js";
22
27
  import {
23
- buildEditorTypeOptions,
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
  } from "../../lib/calendar/editorPayload.js";
@@ -34,21 +36,12 @@ import { renderDictionaryIcon } from "@open-mercato/core/modules/dictionaries/co
34
36
  import { normalizeCustomFieldSubmitValue } from "../detail/customFieldUtils.js";
35
37
  import { EDITOR_SCROLL_EVENT, Field } from "./editor/inputs.js";
36
38
  import { SegmentGroup } from "./editor/SegmentGroup.js";
37
- import { PriorityField } from "./editor/PriorityField.js";
38
39
  import { RelatedToField } from "./editor/RelatedToField.js";
39
- import { RepeatField } from "./editor/RepeatField.js";
40
- import { PeopleField } from "./editor/PeopleField.js";
41
- import { ResourcesField } from "./editor/ResourcesField.js";
42
- import { ScheduleSection } from "./editor/ScheduleSection.js";
43
- import { LocationField } from "./editor/LocationField.js";
44
40
  import { useConflictProbe, useEditorLabelResolution } from "./editor/hooks.js";
41
+ import { EventTypePanel } from "./editor/EventTypePanel.js";
42
+ import { eventTypeConfig, eventTypeOptions, isSelectableEventType, selectedEventType, useEventTypeCatalog } from "./editor/useEventTypeCatalog.js";
45
43
  const FORM_ID = "customers-calendar-event-editor";
46
- const INTERACTION_ENTITY_IDS = [E.customers.customer_interaction];
47
- const PEOPLE_FIELD_TEXT = {
48
- attendees: { labelKey: "customers.calendar.editor.attendees", label: "Attendees", placeholderKey: "customers.calendar.editor.addPeoplePlaceholder", placeholder: "Add staff or customer\u2026" },
49
- participants: { labelKey: "customers.calendar.editor.participants", label: "Participants", placeholderKey: "customers.calendar.editor.addPeoplePlaceholder", placeholder: "Add staff or customer\u2026" },
50
- to: { labelKey: "customers.calendar.editor.to", label: "To", placeholderKey: "customers.calendar.editor.addRecipientPlaceholder", placeholder: "Add recipient\u2026" }
51
- };
44
+ const INTERACTION_INJECTION_SPOT = `crud-form:${E.customers.customer_interaction.replace(/:/g, ".")}`;
52
45
  function formStateOfValues(values) {
53
46
  return values;
54
47
  }
@@ -59,46 +52,94 @@ function customFieldInitialValues(item) {
59
52
  Object.entries(customValues).map(([key, value]) => [`cf_${key}`, value])
60
53
  );
61
54
  }
62
- function EditorBody({
63
- ctx,
55
+ function typeChangeDiscardFields(error) {
56
+ if (!error || typeof error !== "object") return null;
57
+ const details = error;
58
+ if (details.status !== 409 || details.code !== "calendar_type_change_confirmation_required") return null;
59
+ if (!Array.isArray(details.fields) || details.fields.length === 0 || !details.fields.every((field) => typeof field === "string" && field.length > 0)) return null;
60
+ return details.fields;
61
+ }
62
+ function CatalogStatusAlert({ error, onRetry }) {
63
+ const t = useT();
64
+ return /* @__PURE__ */ jsxs(Alert, { status: error ? "error" : "information", className: "rounded-lg lg:col-span-2", children: [
65
+ /* @__PURE__ */ jsx(AlertDescription, { children: error ? t("customers.calendar.editor.catalogLoadFailed") : t("customers.calendar.editor.catalogLoading") }),
66
+ error ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: onRetry, children: t("customers.calendar.errors.retry") }) : null
67
+ ] });
68
+ }
69
+ const TimezoneField = React.memo(function TimezoneField2({
70
+ value,
71
+ error,
72
+ disabled,
73
+ onChange
74
+ }) {
75
+ const t = useT();
76
+ const [open, setOpen] = React.useState(false);
77
+ const timezone = value ?? defaultCalendarTimezone();
78
+ const options = React.useMemo(() => open ? calendarTimezoneOptions(timezone) : [timezone], [open, timezone]);
79
+ const items = React.useMemo(
80
+ () => options.map((timezone2) => /* @__PURE__ */ jsx(SelectItem, { value: timezone2, children: timezone2 }, timezone2)),
81
+ [options]
82
+ );
83
+ return /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.timezone", "Time zone"), error, children: /* @__PURE__ */ jsxs(Select, { value: timezone, onValueChange: onChange, disabled, open, onOpenChange: setOpen, children: [
84
+ /* @__PURE__ */ jsx(SelectTrigger, { "aria-label": t("customers.calendar.editor.timezone", "Time zone"), children: /* @__PURE__ */ jsx(SelectValue, {}) }),
85
+ /* @__PURE__ */ jsx(SelectContent, { children: items })
86
+ ] }) });
87
+ });
88
+ const TypeSwitcher = React.memo(SegmentGroup);
89
+ const NO_RESOURCES = [];
90
+ const MemoRelatedToField = React.memo(RelatedToField);
91
+ const EditorBody = React.memo(function EditorBody2({
92
+ values,
93
+ errors,
94
+ setValue,
64
95
  open,
65
96
  isEdit,
66
97
  item,
67
- typeLabels,
68
- typeIcons,
69
98
  conflictScope,
70
99
  currentUserId,
71
100
  resourcesEnabled,
72
- staffEnabled
101
+ staffEnabled,
102
+ saving,
103
+ catalogItems,
104
+ catalogReady,
105
+ catalogError,
106
+ onRetryCatalog,
107
+ onSelectedTypeChange
73
108
  }) {
74
109
  const t = useT();
75
- const { setValue, errors } = ctx;
76
- const form = formStateOfValues(ctx.values);
77
- const config = KIND_CONFIG[form.kind];
110
+ const form = formStateOfValues(values);
111
+ const selectedType = form.category ?? form.kind;
112
+ const definition = selectedEventType(catalogItems, selectedType);
113
+ const config = eventTypeConfig(definition);
78
114
  const update = React.useCallback(
79
115
  (patch) => {
80
116
  for (const [key, value] of Object.entries(patch)) setValue(key, value);
81
117
  },
82
118
  [setValue]
83
119
  );
120
+ const updateTimezone = React.useCallback((timezone) => setValue("timezone", timezone), [setValue]);
121
+ const selectType = React.useCallback((type) => {
122
+ const nextDefinition = selectedEventType(catalogItems, type);
123
+ update({ kind: nextDefinition.behavior.baseKind, category: type });
124
+ onSelectedTypeChange(type);
125
+ }, [catalogItems, update, onSelectedTypeChange]);
126
+ const updateRelatedTo = React.useCallback((relatedTo) => update({ relatedTo }), [update]);
127
+ const updateDeal = React.useCallback(
128
+ (deal2) => update({ dealId: deal2?.id ?? null, dealLabel: deal2?.label ?? null }),
129
+ [update]
130
+ );
131
+ const deal = React.useMemo(
132
+ () => form.dealId && form.dealLabel ? { id: form.dealId, label: form.dealLabel } : null,
133
+ [form.dealId, form.dealLabel]
134
+ );
135
+ const capabilities = React.useMemo(() => ({ resourcesEnabled, staffEnabled }), [resourcesEnabled, staffEnabled]);
84
136
  useEditorLabelResolution(open, form, update);
85
137
  const draftOwnerUserId = resolveSavedOwnerUserId(config, form, isEdit, item?.ownerUserId ?? null);
86
- const conflict = useConflictProbe(open, form, config, isEdit && item ? item.raw.id : null, draftOwnerUserId, conflictScope, currentUserId);
87
- const kindLabels = React.useMemo(
88
- () => ({
89
- meeting: t("customers.calendar.editor.types.meeting", "meeting"),
90
- call: t("customers.calendar.editor.types.call", "call"),
91
- email: t("customers.calendar.editor.types.email", "email"),
92
- note: t("customers.calendar.editor.types.note", "note"),
93
- event: t("customers.calendar.editor.types.event", "event"),
94
- task: t("customers.calendar.editor.types.task", "task")
95
- }),
96
- [t]
97
- );
98
- const selectedType = form.category ?? form.kind;
138
+ const draftResources = resourcesEnabled && definition.behavior.fields.resources ? form.resources : NO_RESOURCES;
139
+ const conflict = useConflictProbe(open, form, config, isEdit && item ? item.raw.id : null, draftOwnerUserId, conflictScope, currentUserId, draftResources);
99
140
  const typeOptions = React.useMemo(
100
- () => buildEditorTypeOptions({ typeLabels, typeIcons, selectedValue: selectedType, kindLabels }),
101
- [typeLabels, typeIcons, selectedType, kindLabels]
141
+ () => eventTypeOptions(catalogItems, selectedType, t),
142
+ [catalogItems, selectedType, t]
102
143
  );
103
144
  const typeSwitcherOptions = React.useMemo(
104
145
  () => typeOptions.map((option) => ({
@@ -108,182 +149,79 @@ function EditorBody({
108
149
  })),
109
150
  [typeOptions]
110
151
  );
111
- const phoneContactIds = React.useMemo(() => {
112
- const ids = [];
113
- if (form.relatedTo && form.relatedTo.kind !== "company") ids.push(form.relatedTo.id);
114
- for (const participant of form.participants) {
115
- if (participant.isCustomer && participant.userId) ids.push(participant.userId);
116
- }
117
- return Array.from(new Set(ids));
118
- }, [form.relatedTo, form.participants]);
119
152
  const titleLabel = form.kind === "email" ? t("customers.calendar.editor.titleLabel.email", "Subject") : form.kind === "note" ? t("customers.calendar.editor.titleLabel.note", "Note") : t("customers.calendar.editor.titleLabel.generic", "Title");
120
- return (
121
- // Single column on phones (full-screen sheet). On lg+ the dialog widens and
122
- // the fields group into two thematic columns: WHEN (schedule + repeat) on
123
- // the left, CONTEXT (related record, category, location) on the right;
124
- // people/resources and the task fields pair up below; title, description
125
- // and the type switcher span both columns.
126
- /* @__PURE__ */ jsxs("div", { className: "grid w-full grid-cols-1 items-start gap-4 lg:grid-cols-2 lg:gap-x-6", children: [
127
- conflict ? /* @__PURE__ */ jsxs(Alert, { status: "warning", className: "rounded-lg lg:col-span-2", children: [
128
- /* @__PURE__ */ jsx(AlertTitle, { children: t("customers.calendar.editor.conflictTitle", "Calendar conflict") }),
129
- /* @__PURE__ */ jsx(AlertDescription, { children: conflict })
130
- ] }) : null,
131
- /* @__PURE__ */ jsx("div", { className: "w-full lg:col-span-2", children: /* @__PURE__ */ jsx(
132
- SegmentGroup,
133
- {
134
- ariaLabel: t("customers.calendar.editor.typeSwitcher", "Event type"),
135
- value: selectedType,
136
- onChange: (type) => update({ kind: editorKindOfInteractionType(type), category: type }),
137
- options: typeSwitcherOptions
138
- }
139
- ) }),
140
- /* @__PURE__ */ jsx(Field, { label: titleLabel, error: errors.title, className: "lg:col-span-2", children: /* @__PURE__ */ jsx(
141
- Input,
142
- {
143
- type: "text",
144
- value: form.title,
145
- onChange: (event) => update({ title: event.target.value }),
146
- placeholder: t("customers.calendar.editor.titlePlaceholder", "Add a title\u2026"),
147
- "aria-label": titleLabel,
148
- autoFocus: true,
149
- size: "lg"
150
- }
151
- ) }),
152
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
153
- /* @__PURE__ */ jsx(
154
- ScheduleSection,
155
- {
156
- dateLabel: config.dateLabel,
157
- hasAllDay: config.hasAllDay,
158
- hasEnd: config.hasEnd,
159
- allDay: form.allDay,
160
- date: form.date,
161
- startTime: form.startTime,
162
- endDate: form.endDate,
163
- endTime: form.endTime,
164
- endsError: errors.ends,
165
- onAllDayChange: (allDay) => update({ allDay }),
166
- onDateChange: (date) => {
167
- const untouchedDefault = JSON.stringify(form.repeatDays) === JSON.stringify(defaultRepeatDaysForDateInput(form.date));
168
- update(
169
- untouchedDefault ? { date, repeatDays: defaultRepeatDaysForDateInput(date) } : { date }
170
- );
171
- },
172
- onStartTimeChange: (startTime) => update({ startTime }),
173
- onEndDateChange: (endDate) => update({ endDate }),
174
- onEndTimeChange: (endTime) => update({ endTime })
175
- }
176
- ),
177
- config.hasRepeat ? /* @__PURE__ */ jsx(
178
- RepeatField,
179
- {
180
- freq: form.repeatFreq,
181
- days: form.repeatDays,
182
- endType: form.repeatEndType,
183
- count: form.repeatCount,
184
- untilDate: form.repeatUntilDate,
185
- onFreqChange: (repeatFreq) => update(
186
- repeatFreq === "weekly" ? { repeatFreq, repeatDays: defaultRepeatDaysForDateInput(form.date) } : { repeatFreq }
187
- ),
188
- onToggleDay: (index) => update({ repeatDays: form.repeatDays.map((active, dayIndex) => dayIndex === index ? !active : active) }),
189
- onEndTypeChange: (repeatEndType) => update({ repeatEndType }),
190
- onCountChange: (repeatCount) => update({ repeatCount }),
191
- onUntilDateChange: (repeatUntilDate) => update({ repeatUntilDate })
192
- }
193
- ) : null,
194
- config.people && config.people !== "assignee" ? /* @__PURE__ */ jsx(Field, { label: t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label), children: /* @__PURE__ */ jsx(
195
- PeopleField,
196
- {
197
- mode: "multi",
198
- includeCustomers: true,
199
- includeStaff: staffEnabled,
200
- placeholder: t(PEOPLE_FIELD_TEXT[config.people].placeholderKey, PEOPLE_FIELD_TEXT[config.people].placeholder),
201
- ariaLabel: t(PEOPLE_FIELD_TEXT[config.people].labelKey, PEOPLE_FIELD_TEXT[config.people].label),
202
- value: form.participants,
203
- onChange: (participants) => update({ participants })
204
- }
205
- ) }) : null,
206
- config.people === "assignee" && staffEnabled ? /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.assignee", "Assignee"), error: errors.assignee, children: /* @__PURE__ */ jsx(
207
- PeopleField,
208
- {
209
- mode: "single",
210
- includeCustomers: false,
211
- includeStaff: true,
212
- placeholder: t("customers.calendar.editor.assigneePlaceholder", "Assign to a team member\u2026"),
213
- ariaLabel: t("customers.calendar.editor.assignee", "Assignee"),
214
- value: form.assigneeUserId ? [{ userId: form.assigneeUserId, name: form.assigneeName ?? form.assigneeUserId, isCustomer: false }] : [],
215
- onChange: (entries) => {
216
- const next = entries[entries.length - 1] ?? null;
217
- update({ assigneeUserId: next?.userId ?? null, assigneeName: next?.name ?? null });
218
- }
219
- }
220
- ) }) : null
221
- ] }),
222
- /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-4", children: [
223
- /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.relatedTo", "Related to"), error: errors.relatedTo, children: /* @__PURE__ */ jsx(
224
- RelatedToField,
225
- {
226
- label: t("customers.calendar.editor.relatedTo", "Related to"),
227
- value: form.relatedTo,
228
- deal: form.dealId && form.dealLabel ? { id: form.dealId, label: form.dealLabel } : null,
229
- onChange: (relatedTo) => update({ relatedTo }),
230
- onDealChange: (deal) => update({ dealId: deal?.id ?? null, dealLabel: deal?.label ?? null }),
231
- error: errors.relatedTo
232
- }
233
- ) }),
234
- config.location ? /* @__PURE__ */ jsx(
235
- LocationField,
236
- {
237
- variant: config.location,
238
- value: form.location,
239
- onChange: (location) => update({ location }),
240
- phoneContactIds
241
- }
242
- ) : null,
243
- resourcesEnabled ? /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.resources", "Resources"), children: /* @__PURE__ */ jsx(
244
- ResourcesField,
245
- {
246
- placeholder: t("customers.calendar.editor.resourcesPlaceholder", "Add a resource\u2026"),
247
- ariaLabel: t("customers.calendar.editor.resources", "Resources"),
248
- value: form.resources,
249
- onChange: (resources) => update({ resources })
250
- }
251
- ) }) : null,
252
- config.hasPriority ? /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.priority.label", "Priority"), children: /* @__PURE__ */ jsx(
253
- PriorityField,
254
- {
255
- value: form.priority,
256
- ariaLabel: t("customers.calendar.editor.priority.label", "Priority"),
257
- labels: {
258
- low: t("customers.calendar.editor.priority.low", "Low"),
259
- medium: t("customers.calendar.editor.priority.medium", "Medium"),
260
- high: t("customers.calendar.editor.priority.high", "High")
261
- },
262
- onChange: (priority) => update({ priority })
263
- }
264
- ) }) : null
265
- ] }),
266
- /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.description", "Description"), className: "lg:col-span-2", children: /* @__PURE__ */ jsx(
267
- Textarea,
268
- {
269
- value: form.description,
270
- onChange: (event) => update({ description: event.target.value }),
271
- placeholder: t("customers.calendar.editor.descriptionPlaceholder", "Add details\u2026"),
272
- "aria-label": t("customers.calendar.editor.description", "Description"),
273
- className: "h-20 resize-none"
274
- }
275
- ) })
276
- ] })
277
- );
278
- }
153
+ return /* @__PURE__ */ jsxs("div", { className: "grid w-full grid-cols-1 items-start gap-4 lg:grid-cols-2 lg:gap-x-6", children: [
154
+ conflict ? /* @__PURE__ */ jsxs(Alert, { status: "warning", className: "rounded-lg lg:col-span-2", children: [
155
+ /* @__PURE__ */ jsx(AlertTitle, { children: t("customers.calendar.editor.conflictTitle", "Calendar conflict") }),
156
+ /* @__PURE__ */ jsx(AlertDescription, { children: conflict })
157
+ ] }) : null,
158
+ !catalogReady ? /* @__PURE__ */ jsx(CatalogStatusAlert, { error: catalogError, onRetry: onRetryCatalog }) : null,
159
+ catalogReady && (definition.historical || catalogItems.some((item2) => item2.key === selectedType && !item2.selectable)) ? /* @__PURE__ */ jsx(Alert, { status: "warning", className: "rounded-lg lg:col-span-2", children: /* @__PURE__ */ jsx(AlertDescription, { children: t("customers.calendar.editor.historicalType") }) }) : null,
160
+ /* @__PURE__ */ jsx("div", { className: "w-full lg:col-span-2", children: /* @__PURE__ */ jsx(
161
+ TypeSwitcher,
162
+ {
163
+ ariaLabel: t("customers.calendar.editor.typeSwitcher", "Event type"),
164
+ value: selectedType,
165
+ onChange: selectType,
166
+ disabled: !catalogReady,
167
+ options: typeSwitcherOptions
168
+ }
169
+ ) }),
170
+ /* @__PURE__ */ jsx(Field, { label: titleLabel, error: errors.title, className: "lg:col-span-2", children: /* @__PURE__ */ jsx(
171
+ Input,
172
+ {
173
+ type: "text",
174
+ value: form.title,
175
+ onChange: (event) => update({ title: event.target.value }),
176
+ placeholder: t("customers.calendar.editor.titlePlaceholder", "Add a title\u2026"),
177
+ "aria-label": titleLabel,
178
+ autoFocus: true,
179
+ size: "lg"
180
+ }
181
+ ) }),
182
+ /* @__PURE__ */ jsx(TimezoneField, { value: form.timezone, error: errors.timezone, disabled: saving, onChange: updateTimezone }),
183
+ /* @__PURE__ */ jsx("div", { className: "lg:col-span-2", children: /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.relatedTo", "Related to"), error: errors.relatedTo, children: /* @__PURE__ */ jsx(
184
+ MemoRelatedToField,
185
+ {
186
+ label: t("customers.calendar.editor.relatedTo", "Related to"),
187
+ value: form.relatedTo,
188
+ deal,
189
+ onChange: updateRelatedTo,
190
+ onDealChange: updateDeal,
191
+ error: errors.relatedTo
192
+ }
193
+ ) }) }),
194
+ catalogReady ? /* @__PURE__ */ jsx("div", { className: "lg:col-span-2", children: /* @__PURE__ */ jsx(
195
+ EventTypePanel,
196
+ {
197
+ definition,
198
+ panelKey: definition.panelKey,
199
+ mode: isEdit ? "edit" : "create",
200
+ values,
201
+ errors,
202
+ disabled: saving,
203
+ capabilities,
204
+ setValue
205
+ }
206
+ ) }) : null,
207
+ /* @__PURE__ */ jsx(Field, { label: t("customers.calendar.editor.description", "Description"), className: "lg:col-span-2", children: /* @__PURE__ */ jsx(
208
+ Textarea,
209
+ {
210
+ value: form.description,
211
+ onChange: (event) => update({ description: event.target.value }),
212
+ placeholder: t("customers.calendar.editor.descriptionPlaceholder", "Add details\u2026"),
213
+ "aria-label": t("customers.calendar.editor.description", "Description"),
214
+ className: "h-20 resize-none"
215
+ }
216
+ ) })
217
+ ] });
218
+ });
279
219
  function CalendarEventEditor({
280
220
  open,
281
221
  mode,
282
222
  item,
283
223
  defaultDate,
284
224
  defaultRange,
285
- typeLabels,
286
- typeIcons,
287
225
  conflictScope,
288
226
  currentUserId,
289
227
  resourcesEnabled,
@@ -293,14 +231,58 @@ function CalendarEventEditor({
293
231
  }) {
294
232
  const t = useT();
295
233
  const [saving, setSaving] = React.useState(false);
234
+ const { confirm, ConfirmDialogElement } = useConfirmDialog();
235
+ const dirtyRef = React.useRef(false);
236
+ const closeConfirmPendingRef = React.useRef(false);
237
+ const [closePromptOpen, setClosePromptOpen] = React.useState(false);
238
+ React.useEffect(() => {
239
+ if (!closePromptOpen) return;
240
+ const swallowSubmitShortcut = (event) => {
241
+ if (event.key !== "Enter" || !(event.metaKey || event.ctrlKey)) return;
242
+ event.preventDefault();
243
+ event.stopImmediatePropagation();
244
+ };
245
+ window.addEventListener("keydown", swallowSubmitShortcut, true);
246
+ return () => window.removeEventListener("keydown", swallowSubmitShortcut, true);
247
+ }, [closePromptOpen]);
248
+ const requestClose = React.useCallback(async () => {
249
+ if (saving || closeConfirmPendingRef.current) return;
250
+ if (dirtyRef.current) {
251
+ closeConfirmPendingRef.current = true;
252
+ setClosePromptOpen(true);
253
+ try {
254
+ const confirmed = await confirm({
255
+ title: t("ui.forms.confirmUnsavedChanges", "You have unsaved changes. Are you sure you want to leave?"),
256
+ confirmText: t("customers.deals.detail.unsavedConfirm", "Discard changes"),
257
+ cancelText: t("customers.deals.detail.unsavedCancel", "Keep editing"),
258
+ variant: "destructive"
259
+ });
260
+ if (!confirmed) return;
261
+ } finally {
262
+ closeConfirmPendingRef.current = false;
263
+ setClosePromptOpen(false);
264
+ }
265
+ }
266
+ onOpenChange(false);
267
+ }, [confirm, onOpenChange, saving, t]);
268
+ const handleDirtyChange = React.useCallback((dirty) => {
269
+ dirtyRef.current = dirty;
270
+ }, []);
271
+ React.useEffect(() => {
272
+ if (!open) dirtyRef.current = false;
273
+ }, [open]);
296
274
  const isEdit = mode === "edit" && Boolean(item?.id);
275
+ const catalog = useEventTypeCatalog(open);
276
+ const { items: catalogItems, status: catalogStatus, retry: retryCatalog } = catalog;
277
+ const [selectedTypeKey, setSelectedTypeKey] = React.useState(() => isEdit && item ? item.interactionType : "meeting");
278
+ const autoSelectedCreateType = !isEdit && catalog.status === "ready" && !isSelectableEventType(catalog.items, "meeting") ? catalog.items.find((entry) => entry.selectable && !entry.historical) ?? null : null;
297
279
  const [openSeq, setOpenSeq] = React.useState(0);
298
280
  const wasOpenRef = React.useRef(false);
299
281
  React.useEffect(() => {
300
282
  if (open && !wasOpenRef.current) setOpenSeq((seq) => seq + 1);
301
283
  wasOpenRef.current = open;
302
284
  }, [open]);
303
- const formKey = `${mode}:${item?.id ?? "new"}:${openSeq}`;
285
+ const formKey = `${mode}:${item?.id ?? "new"}:${openSeq}:${autoSelectedCreateType?.key ?? "default"}`;
304
286
  const initialValues = React.useMemo(() => {
305
287
  if (isEdit && item) {
306
288
  return {
@@ -310,20 +292,55 @@ function CalendarEventEditor({
310
292
  updatedAt: item.updatedAt ?? void 0
311
293
  };
312
294
  }
313
- return { ...createDefaultFormState(defaultDate ?? null, void 0, defaultRange ?? null) };
314
- }, [isEdit, item, defaultDate, defaultRange, openSeq]);
295
+ const defaults = createDefaultFormState(defaultDate ?? null, void 0, defaultRange ?? null);
296
+ if (!autoSelectedCreateType) return { ...defaults };
297
+ return {
298
+ ...defaults,
299
+ kind: autoSelectedCreateType.behavior.baseKind,
300
+ category: autoSelectedCreateType.key
301
+ };
302
+ }, [isEdit, item, defaultDate, defaultRange, openSeq, autoSelectedCreateType]);
303
+ const initialTypeKey = String(initialValues.category ?? initialValues.kind ?? "meeting");
304
+ React.useEffect(() => {
305
+ setSelectedTypeKey(initialTypeKey);
306
+ }, [initialTypeKey, formKey]);
307
+ const selectedDefinition = selectedEventType(catalog.items, selectedTypeKey);
308
+ const customFields = useCalendarCustomFields(open, E.customers.customer_interaction, selectedDefinition.behavior.customFieldsetIds);
309
+ const injectionWidgets = useInjectionWidgets(INTERACTION_INJECTION_SPOT);
310
+ const canCreateType = isSelectableEventType(catalog.items, selectedTypeKey);
311
+ const formReady = isEdit || catalog.status === "ready";
315
312
  const handleSubmit = React.useCallback(
316
313
  async (values) => {
317
314
  const form = formStateOfValues(values);
318
- const config = KIND_CONFIG[form.kind];
315
+ if (catalogStatus !== "ready" || customFields.status !== "ready" || injectionWidgets.loading || injectionWidgets.error) {
316
+ throw createCrudFormError(t("customers.calendar.editor.catalogLoadFailed"));
317
+ }
318
+ const definition = selectedEventType(catalogItems, form.category ?? form.kind);
319
+ const selectedKey = form.category ?? form.kind;
320
+ if ((mode === "create" || selectedKey !== initialTypeKey) && !isSelectableEventType(catalogItems, selectedKey)) {
321
+ throw createCrudFormError(t("customers.calendar.editor.typeUnavailable"));
322
+ }
323
+ const config = eventTypeConfig(definition);
319
324
  const fieldErrors = {};
325
+ const timezone = form.timezone ?? defaultCalendarTimezone();
326
+ const startTime = form.allDay && config.hasAllDay ? "00:00" : form.startTime;
327
+ if (!isCalendarTimezone(timezone)) fieldErrors.timezone = t("customers.calendar.editor.validation.timezoneInvalid", "Choose a valid time zone");
328
+ else if (!(form.allDay && config.hasAllDay ? calendarDayStartInstant(form.date, timezone) : calendarWallTimeToInstant(form.date, startTime, timezone))) {
329
+ fieldErrors[form.allDay && config.hasAllDay ? "date" : "startTime"] = t("customers.calendar.editor.validation.timezoneGap", "This local time does not exist in the selected time zone");
330
+ }
331
+ if (config.hasEnd && !form.allDay && isCalendarTimezone(timezone) && !calendarWallTimeToInstant(form.endDate, form.endTime, timezone)) {
332
+ fieldErrors.ends = t("customers.calendar.editor.validation.timezoneGap", "This local time does not exist in the selected time zone");
333
+ }
334
+ if (config.hasRepeat && form.repeatFreq !== "none" && isCalendarTimezone(timezone) && form.repeatEndType === "date" && form.repeatUntilDate && !calendarDayEndInstant(form.repeatUntilDate, timezone)) {
335
+ fieldErrors.repeatUntilDate = t("customers.calendar.editor.validation.timezoneGap", "This local time does not exist in the selected time zone");
336
+ }
320
337
  if (!form.title.trim()) {
321
338
  fieldErrors.title = t("customers.calendar.editor.validation.titleRequired", "Title is required");
322
339
  }
323
340
  if (!form.relatedTo) {
324
341
  fieldErrors.relatedTo = t("customers.calendar.editor.validation.relatedToRequired", "Select a person or company to link this event");
325
342
  }
326
- if (config.hasEnd && !form.allDay && computeDurationMinutes(form) === null) {
343
+ if (!fieldErrors.ends && !fieldErrors.startTime && !fieldErrors.date && config.hasEnd && !form.allDay && computeDurationMinutes(form) === null) {
327
344
  fieldErrors.ends = t("customers.calendar.editor.validation.endsBeforeStarts", "End must be after start");
328
345
  }
329
346
  if (config.people === "assignee" && staffEnabled !== false && !form.assigneeUserId) {
@@ -332,23 +349,81 @@ function CalendarEventEditor({
332
349
  if (Object.keys(fieldErrors).length > 0) {
333
350
  throw createCrudFormError(Object.values(fieldErrors)[0], fieldErrors);
334
351
  }
352
+ const customValidation = validateCalendarCustomValues(values, customFields.definitions);
353
+ if (!customValidation.ok) {
354
+ const translatedErrors = Object.fromEntries(Object.entries(customValidation.fieldErrors).map(([key, message]) => [key, t(message, message)]));
355
+ throw createCrudFormError(Object.values(translatedErrors)[0], translatedErrors);
356
+ }
335
357
  setSaving(true);
336
358
  try {
337
- const payload = buildInteractionPayload(form, {
359
+ const payload = buildInteractionPayload({ ...form, timezone }, {
360
+ config,
338
361
  mode,
339
362
  id: item?.id,
340
363
  resourcesEnabled: resourcesEnabled === true,
341
364
  staffEnabled: staffEnabled !== false
342
365
  });
343
- const custom = collectCustomFieldValues(values, {
366
+ payload.enforceSelectableType = true;
367
+ const applicable = definition.behavior.fields;
368
+ const time = form.allDay && applicable.allDay ? "00:00" : form.startTime;
369
+ payload.time = time;
370
+ payload.scheduledAt = (form.allDay && applicable.allDay ? calendarDayStartInstant(form.date, timezone) : calendarWallTimeToInstant(form.date, time, timezone)).toISOString();
371
+ payload.timezone = timezone;
372
+ payload.allDay = applicable.allDay ? form.allDay : null;
373
+ payload.durationMinutes = applicable.endTime && !(form.allDay && applicable.allDay) ? computeDurationMinutes(form) : null;
374
+ payload.location = applicable.location === "none" ? null : form.location.trim() || null;
375
+ const recurrenceRule = applicable.recurrence ? buildRecurrenceRule(form) : null;
376
+ payload.recurrenceRule = recurrenceRule;
377
+ payload.recurrenceEnd = recurrenceRule && form.repeatEndType === "date" && form.repeatUntilDate ? calendarDayEndInstant(form.repeatUntilDate, timezone).toISOString() : null;
378
+ payload.participants = applicable.people !== "none" && applicable.people !== "assignee" && form.participants.length ? form.participants.map((participant) => ({
379
+ userId: participant.userId,
380
+ name: participant.name,
381
+ email: participant.email,
382
+ status: participant.isCustomer ? "customer" : "pending"
383
+ })) : null;
384
+ if (applicable.people === "assignee" && staffEnabled !== false) payload.ownerUserId = form.assigneeUserId ?? null;
385
+ else delete payload.ownerUserId;
386
+ if (applicable.priority) payload.priority = form.priority === "low" ? 10 : form.priority === "high" ? 90 : 50;
387
+ else delete payload.priority;
388
+ if (!applicable.resources) delete payload.linkedEntities;
389
+ const custom = collectCustomFieldValues(projectCalendarCustomValues(values, customFields.definitions), {
344
390
  transform: (value) => normalizeCustomFieldSubmitValue(value)
345
391
  });
346
392
  for (const [key, value] of Object.entries(custom)) payload[`cf_${key}`] = value;
347
- await apiCallOrThrow("/api/customers/interactions", {
393
+ const submitInteraction = (confirmDiscardInapplicableValues) => apiCallOrThrow("/api/customers/interactions", {
348
394
  method: isEdit ? "PUT" : "POST",
349
395
  headers: { "content-type": "application/json" },
350
- body: JSON.stringify(payload)
396
+ body: JSON.stringify(confirmDiscardInapplicableValues ? { ...payload, confirmDiscardInapplicableValues: true } : payload)
351
397
  });
398
+ try {
399
+ await submitInteraction(false);
400
+ } catch (error) {
401
+ const fields = typeChangeDiscardFields(error);
402
+ if (!fields) throw error;
403
+ const approved = await confirm({
404
+ title: t("customers.calendar.editor.discardTitle"),
405
+ text: `${t("customers.calendar.editor.discardDescription")} ${t("customers.calendar.editor.discardFields")} ${fields.map((field) => {
406
+ const definition2 = customFields.allDefinitions.find((entry) => `cf_${entry.key}` === field);
407
+ if (definition2?.label) return definition2.label;
408
+ const labelKeys = {
409
+ location: "customers.calendar.editor.location",
410
+ participants: "customers.calendar.editor.attendees",
411
+ linkedEntities: "customers.calendar.editor.resources",
412
+ allDay: "customers.calendar.editor.allDay",
413
+ durationMinutes: "customers.calendar.editor.dates.ends",
414
+ priority: "customers.calendar.editor.priority.label",
415
+ recurrenceRule: "customers.calendar.editor.repeat.label",
416
+ recurrenceEnd: "customers.calendar.editor.repeat.ends"
417
+ };
418
+ return t(labelKeys[field] ?? "customers.calendar.editor.discardUnknownField");
419
+ }).join(", ")}`,
420
+ confirmText: t("customers.calendar.editor.discardConfirm"),
421
+ cancelText: t("customers.calendar.editor.cancel"),
422
+ variant: "destructive"
423
+ });
424
+ if (!approved) return;
425
+ await submitInteraction(true);
426
+ }
352
427
  flash(t("customers.calendar.editor.saved", "Event saved"), "success");
353
428
  onOpenChange(false);
354
429
  requestAnimationFrame(() => {
@@ -365,7 +440,7 @@ function CalendarEventEditor({
365
440
  setSaving(false);
366
441
  }
367
442
  },
368
- [isEdit, item?.id, mode, onOpenChange, onSaved, resourcesEnabled, staffEnabled, t]
443
+ [catalogItems, catalogStatus, customFields.status, customFields.definitions, customFields.allDefinitions, injectionWidgets.loading, injectionWidgets.error, confirm, initialTypeKey, isEdit, item?.id, mode, onOpenChange, onSaved, resourcesEnabled, staffEnabled, t]
369
444
  );
370
445
  const groups = React.useMemo(
371
446
  () => [
@@ -375,108 +450,127 @@ function CalendarEventEditor({
375
450
  component: (ctx) => /* @__PURE__ */ jsx(
376
451
  EditorBody,
377
452
  {
378
- ctx,
453
+ values: ctx.values,
454
+ errors: ctx.errors,
455
+ setValue: ctx.setValue,
379
456
  open,
380
457
  isEdit,
381
458
  item,
382
- typeLabels,
383
- typeIcons: typeIcons ?? {},
384
459
  conflictScope: conflictScope ?? "all",
385
460
  currentUserId: currentUserId ?? null,
386
461
  resourcesEnabled: resourcesEnabled === true,
387
- staffEnabled: staffEnabled !== false
462
+ staffEnabled: staffEnabled !== false,
463
+ saving,
464
+ catalogItems,
465
+ catalogReady: catalogStatus === "ready",
466
+ catalogError: catalogStatus === "error",
467
+ onRetryCatalog: retryCatalog,
468
+ onSelectedTypeChange: setSelectedTypeKey
388
469
  }
389
470
  )
390
471
  },
391
- { id: "customFields", kind: "customFields" }
472
+ ...customFields.fields.length ? [{ id: "customFields", title: t("entities.customFields.title"), fields: customFields.fields.map((field) => field.id) }] : []
392
473
  ],
393
- [open, isEdit, item, typeLabels, typeIcons, conflictScope, currentUserId, resourcesEnabled, staffEnabled]
474
+ [open, isEdit, item, conflictScope, currentUserId, resourcesEnabled, staffEnabled, saving, catalogItems, catalogStatus, retryCatalog, customFields.fields, t]
394
475
  );
395
476
  const handleKeyDown = useDialogKeyHandler({
477
+ onCancel: () => {
478
+ void requestClose();
479
+ },
396
480
  onConfirm: () => {
397
481
  const formElement = document.getElementById(FORM_ID);
398
482
  if (formElement instanceof HTMLFormElement) formElement.requestSubmit();
399
483
  },
400
- disabled: saving
484
+ disabled: saving || customFields.status !== "ready" || injectionWidgets.loading || !!injectionWidgets.error || catalog.status !== "ready" || mode === "create" && !canCreateType
401
485
  });
402
486
  const dialogTitle = isEdit ? t("customers.calendar.editor.title.edit", "Edit event") : t("customers.calendar.editor.title.create", "New event");
403
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
404
- DialogContent,
405
- {
406
- onKeyDown: handleKeyDown,
407
- onInteractOutside: (event) => {
408
- const target = event.detail.originalEvent.target;
409
- if (target?.closest("[data-dialog-content]") || target?.closest("[data-radix-popper-content-wrapper]")) {
410
- event.preventDefault();
411
- }
412
- },
413
- "aria-describedby": void 0,
414
- dismissible: false,
415
- 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",
416
- children: [
417
- /* @__PURE__ */ jsx(VisuallyHidden, { children: /* @__PURE__ */ jsx(DialogTitle, { children: dialogTitle }) }),
418
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-3 border-b border-border bg-background py-4 pl-5 pr-4", children: [
419
- /* @__PURE__ */ jsx(Calendar, { "aria-hidden": true, className: "size-6 shrink-0 text-foreground", strokeWidth: 1.75 }),
420
- /* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1 text-sm font-medium leading-5 text-foreground", children: dialogTitle }),
421
- /* @__PURE__ */ jsx(
422
- IconButton,
423
- {
424
- variant: "ghost",
425
- size: "sm",
426
- onClick: () => onOpenChange(false),
427
- "aria-label": t("customers.calendar.editor.close", "Close"),
428
- className: "shrink-0 text-muted-foreground",
429
- children: /* @__PURE__ */ jsx(X, { "aria-hidden": true, className: "size-5" })
430
- }
431
- )
432
- ] }),
433
- /* @__PURE__ */ jsx(
434
- "div",
435
- {
436
- className: "flex-1 overflow-y-auto",
437
- onScroll: () => {
438
- document.dispatchEvent(new CustomEvent(EDITOR_SCROLL_EVENT));
439
- },
440
- children: /* @__PURE__ */ jsx("div", { className: "px-4 py-4 sm:px-6 sm:py-5", children: /* @__PURE__ */ jsx(
441
- CrudForm,
442
- {
443
- formId: FORM_ID,
444
- embedded: true,
445
- hideFooterActions: true,
446
- customFieldsManageMode: "page",
447
- fields: [],
448
- groups,
449
- initialValues,
450
- entityIds: INTERACTION_ENTITY_IDS,
451
- onSubmit: handleSubmit
452
- },
453
- formKey
454
- ) })
487
+ return /* @__PURE__ */ jsxs(Dialog, { open, onOpenChange: (nextOpen) => {
488
+ if (nextOpen) onOpenChange(true);
489
+ else void requestClose();
490
+ }, children: [
491
+ /* @__PURE__ */ jsxs(
492
+ DialogContent,
493
+ {
494
+ onKeyDown: handleKeyDown,
495
+ onInteractOutside: (event) => {
496
+ const target = event.detail.originalEvent.target;
497
+ if (target?.closest("[data-dialog-content]") || target?.closest("[data-radix-popper-content-wrapper]") || target?.closest("dialog")) {
498
+ event.preventDefault();
455
499
  }
456
- ),
457
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-end gap-3 border-t border-border bg-background px-5 py-4", children: [
500
+ },
501
+ "aria-describedby": void 0,
502
+ dismissible: false,
503
+ 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",
504
+ children: [
505
+ /* @__PURE__ */ jsx(VisuallyHidden, { children: /* @__PURE__ */ jsx(DialogTitle, { children: dialogTitle }) }),
506
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-3 border-b border-border bg-background py-4 pl-5 pr-4", children: [
507
+ /* @__PURE__ */ jsx(Calendar, { "aria-hidden": true, className: "size-6 shrink-0 text-foreground", strokeWidth: 1.75 }),
508
+ /* @__PURE__ */ jsx("p", { className: "min-w-0 flex-1 text-sm font-medium leading-5 text-foreground", children: dialogTitle }),
509
+ /* @__PURE__ */ jsx(
510
+ IconButton,
511
+ {
512
+ variant: "ghost",
513
+ size: "sm",
514
+ onClick: () => {
515
+ void requestClose();
516
+ },
517
+ "aria-label": t("customers.calendar.editor.close", "Close"),
518
+ className: "shrink-0 text-muted-foreground",
519
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": true, className: "size-5" })
520
+ }
521
+ )
522
+ ] }),
458
523
  /* @__PURE__ */ jsx(
459
- Button,
524
+ "div",
460
525
  {
461
- type: "button",
462
- variant: "outline",
463
- onClick: () => onOpenChange(false),
464
- children: t("customers.calendar.editor.cancel", "Cancel")
526
+ className: "flex-1 overflow-y-auto",
527
+ onScroll: () => {
528
+ document.dispatchEvent(new CustomEvent(EDITOR_SCROLL_EVENT));
529
+ },
530
+ children: /* @__PURE__ */ jsx("div", { className: "px-4 py-4 sm:px-6 sm:py-5", children: formReady ? /* @__PURE__ */ jsx(
531
+ CrudForm,
532
+ {
533
+ formId: FORM_ID,
534
+ embedded: true,
535
+ trackDirtyWhenEmbedded: true,
536
+ onDirtyChange: handleDirtyChange,
537
+ hideFooterActions: true,
538
+ customFieldsManageMode: "page",
539
+ fields: customFields.fields,
540
+ groups,
541
+ initialValues,
542
+ injectionSpotId: INTERACTION_INJECTION_SPOT,
543
+ onSubmit: handleSubmit
544
+ },
545
+ formKey
546
+ ) : /* @__PURE__ */ jsx(CatalogStatusAlert, { error: catalog.status === "error", onRetry: catalog.retry }) })
465
547
  }
466
548
  ),
467
- /* @__PURE__ */ jsx(
468
- Button,
469
- {
470
- type: "submit",
471
- form: FORM_ID,
472
- disabled: saving,
473
- children: saving ? t("customers.calendar.editor.saving", "Saving\u2026") : t("customers.calendar.editor.save", "Save event")
474
- }
475
- )
476
- ] })
477
- ]
478
- }
479
- ) });
549
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-end gap-3 border-t border-border bg-background px-5 py-4", children: [
550
+ /* @__PURE__ */ jsx(
551
+ Button,
552
+ {
553
+ type: "button",
554
+ variant: "outline",
555
+ onClick: () => onOpenChange(false),
556
+ children: t("customers.calendar.editor.cancel", "Cancel")
557
+ }
558
+ ),
559
+ /* @__PURE__ */ jsx(
560
+ Button,
561
+ {
562
+ type: "submit",
563
+ form: FORM_ID,
564
+ disabled: saving || customFields.status !== "ready" || injectionWidgets.loading || !!injectionWidgets.error || catalog.status !== "ready" || mode === "create" && !canCreateType,
565
+ children: saving ? t("customers.calendar.editor.saving", "Saving\u2026") : t("customers.calendar.editor.save", "Save event")
566
+ }
567
+ )
568
+ ] })
569
+ ]
570
+ }
571
+ ),
572
+ ConfirmDialogElement
573
+ ] });
480
574
  }
481
575
  export {
482
576
  CalendarEventEditor