@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.
Files changed (243) 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/catalog/commands/categories.js +1 -1
  9. package/dist/modules/catalog/commands/categories.js.map +2 -2
  10. package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
  11. package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
  12. package/dist/modules/catalog/commands/products.js +52 -17
  13. package/dist/modules/catalog/commands/products.js.map +2 -2
  14. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
  15. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  16. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
  17. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
  18. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
  19. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
  20. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  21. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  22. package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
  23. package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
  24. package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
  25. package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
  27. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
  28. package/dist/modules/customers/api/activity-types/route.js +151 -0
  29. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  30. package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
  31. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  32. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  33. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  34. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  35. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  36. package/dist/modules/customers/api/interactions/route.js +3 -0
  37. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  38. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  39. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  40. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  41. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  42. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
  43. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  44. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
  45. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  46. package/dist/modules/customers/calendar-event-types.js +548 -0
  47. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  48. package/dist/modules/customers/commands/dictionaries.js +70 -10
  49. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +187 -22
  51. package/dist/modules/customers/commands/interactions.js.map +3 -3
  52. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  53. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  54. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  55. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  56. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  57. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  58. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  59. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  60. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  61. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  63. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  65. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  66. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  67. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  68. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  69. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  70. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  71. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  72. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  73. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  74. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  75. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  76. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  77. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  78. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  79. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  80. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  81. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  82. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  83. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  85. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/types.js +1 -0
  87. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  88. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
  89. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  90. package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
  91. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
  92. package/dist/modules/customers/components/detail/DealForm.js +7 -3
  93. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  94. package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
  95. package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
  96. package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
  97. package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
  98. package/dist/modules/customers/components/detail/PersonCard.js +5 -2
  99. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  100. package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
  101. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
  102. package/dist/modules/customers/data/entities.js +6 -0
  103. package/dist/modules/customers/data/entities.js.map +2 -2
  104. package/dist/modules/customers/data/validators.js +39 -10
  105. package/dist/modules/customers/data/validators.js.map +2 -2
  106. package/dist/modules/customers/di.js +20 -1
  107. package/dist/modules/customers/di.js.map +2 -2
  108. package/dist/modules/customers/extension-points.js +13 -0
  109. package/dist/modules/customers/extension-points.js.map +2 -2
  110. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  111. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  112. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  113. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  114. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  115. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  117. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  119. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  120. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  121. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  122. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  123. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  124. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  125. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  126. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  127. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  128. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  129. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  130. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  131. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  132. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  133. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  134. package/dist/modules/messages/subscribers/message-notification.js +13 -1
  135. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  136. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
  137. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  138. package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
  139. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  140. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  141. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  142. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  143. package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
  144. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  145. package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
  146. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  148. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  149. package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
  150. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  151. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  152. package/generated/entities/customer_interaction/index.ts +1 -0
  153. package/generated/entity-fields-registry.ts +2 -0
  154. package/package.json +9 -8
  155. package/src/modules/auth/i18n/de.json +3 -0
  156. package/src/modules/auth/i18n/en.json +3 -0
  157. package/src/modules/auth/i18n/es.json +3 -0
  158. package/src/modules/auth/i18n/ko.json +3 -0
  159. package/src/modules/auth/i18n/pl.json +3 -0
  160. package/src/modules/catalog/commands/categories.ts +1 -1
  161. package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
  162. package/src/modules/catalog/commands/products.ts +58 -17
  163. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
  164. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
  165. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
  166. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  167. package/src/modules/communication_channels/i18n/de.json +4 -0
  168. package/src/modules/communication_channels/i18n/en.json +4 -0
  169. package/src/modules/communication_channels/i18n/es.json +4 -0
  170. package/src/modules/communication_channels/i18n/ko.json +4 -0
  171. package/src/modules/communication_channels/i18n/pl.json +4 -0
  172. package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
  173. package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
  174. package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
  175. package/src/modules/customers/api/activity-types/route.ts +154 -0
  176. package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
  177. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  178. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  179. package/src/modules/customers/api/interactions/route.ts +4 -0
  180. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  181. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  182. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
  183. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
  184. package/src/modules/customers/calendar-event-types.ts +674 -0
  185. package/src/modules/customers/commands/dictionaries.ts +84 -8
  186. package/src/modules/customers/commands/interactions.ts +244 -21
  187. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  188. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  189. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  190. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  191. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  192. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  193. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  194. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  195. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  196. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  197. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  198. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  199. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  200. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  201. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  202. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  203. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  204. package/src/modules/customers/components/calendar/types.ts +1 -0
  205. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
  206. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
  207. package/src/modules/customers/components/detail/DealForm.tsx +7 -3
  208. package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
  209. package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
  210. package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
  211. package/src/modules/customers/components/detail/types.ts +1 -0
  212. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
  213. package/src/modules/customers/data/entities.ts +8 -1
  214. package/src/modules/customers/data/validators.ts +42 -7
  215. package/src/modules/customers/di.ts +21 -0
  216. package/src/modules/customers/extension-points.ts +13 -0
  217. package/src/modules/customers/i18n/de.json +104 -8
  218. package/src/modules/customers/i18n/en.json +104 -8
  219. package/src/modules/customers/i18n/es.json +104 -8
  220. package/src/modules/customers/i18n/ko.json +104 -8
  221. package/src/modules/customers/i18n/pl.json +104 -8
  222. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  223. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  224. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  225. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  226. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  227. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  228. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  229. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  230. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  231. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  232. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  233. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  234. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  235. package/src/modules/messages/subscribers/message-notification.ts +13 -3
  236. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
  237. package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
  238. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  239. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  240. package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
  241. package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
  242. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
  243. package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
@@ -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