@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
@@ -193,6 +193,11 @@
193
193
  "customers.audit.todos.link": "할 일 연결",
194
194
  "customers.audit.todos.unlink": "할 일 연결 해제",
195
195
  "customers.calendar.actions.newEvent": "새 이벤트",
196
+ "customers.calendar.activityTypes.errors.changeConfirmationRequired": "이벤트 유형을 변경하면 기존 값이 삭제됩니다.",
197
+ "customers.calendar.activityTypes.errors.fieldNotApplicable": "이 필드는 선택한 활동 유형에 적용되지 않습니다.",
198
+ "customers.calendar.activityTypes.errors.forbidden": "활동 유형을 볼 권한이 없습니다.",
199
+ "customers.calendar.activityTypes.errors.loadFailed": "활동 유형을 불러오지 못했습니다.",
200
+ "customers.calendar.activityTypes.errors.unavailable": "이 활동 유형은 사용할 수 없습니다.",
196
201
  "customers.calendar.agenda.eventsCount.few": "이벤트 {count}건",
197
202
  "customers.calendar.agenda.eventsCount.many": "이벤트 {count}건",
198
203
  "customers.calendar.agenda.eventsCount.one": "이벤트 {count}건",
@@ -232,6 +237,8 @@
232
237
  "customers.calendar.editor.assigneePlaceholder": "팀원에게 배정…",
233
238
  "customers.calendar.editor.attendees": "참석자",
234
239
  "customers.calendar.editor.cancel": "취소",
240
+ "customers.calendar.editor.catalogLoadFailed": "활동 유형을 불러오지 못했습니다. 다시 시도한 후 이벤트를 저장하세요.",
241
+ "customers.calendar.editor.catalogLoading": "활동 유형을 불러오는 중…",
235
242
  "customers.calendar.editor.category": "카테고리",
236
243
  "customers.calendar.editor.close": "닫기",
237
244
  "customers.calendar.editor.conflictTitle": "일정 충돌",
@@ -249,8 +256,14 @@
249
256
  "customers.calendar.editor.dealsSection": "거래",
250
257
  "customers.calendar.editor.description": "설명",
251
258
  "customers.calendar.editor.descriptionPlaceholder": "세부 정보 추가…",
259
+ "customers.calendar.editor.discardConfirm": "유형 변경 및 값 삭제",
260
+ "customers.calendar.editor.discardDescription": "이벤트 유형을 변경하면 새 유형에 적용되지 않는 값이 삭제됩니다.",
261
+ "customers.calendar.editor.discardFields": "삭제할 값:",
262
+ "customers.calendar.editor.discardTitle": "이벤트 유형을 변경할까요?",
263
+ "customers.calendar.editor.discardUnknownField": "사용자 정의 필드",
252
264
  "customers.calendar.editor.error": "이벤트를 저장하지 못했습니다",
253
265
  "customers.calendar.editor.guestBadge": "게스트",
266
+ "customers.calendar.editor.historicalType": "이 이벤트 유형은 새 이벤트에 사용할 수 없습니다. 현재 이벤트를 계속 편집하거나 다른 유형을 선택하세요.",
254
267
  "customers.calendar.editor.location": "장소",
255
268
  "customers.calendar.editor.locationPlaceholder": "장소 또는 링크 추가…",
256
269
  "customers.calendar.editor.multiDayHint": "여러 날 이벤트 · {count}일",
@@ -295,6 +308,7 @@
295
308
  "customers.calendar.editor.saved": "이벤트가 저장되었습니다",
296
309
  "customers.calendar.editor.saving": "저장 중…",
297
310
  "customers.calendar.editor.searching": "검색 중…",
311
+ "customers.calendar.editor.timezone": "시간대",
298
312
  "customers.calendar.editor.title.create": "새 이벤트",
299
313
  "customers.calendar.editor.title.edit": "이벤트 수정",
300
314
  "customers.calendar.editor.titleLabel.email": "제목",
@@ -303,6 +317,7 @@
303
317
  "customers.calendar.editor.titlePlaceholder": "제목 추가…",
304
318
  "customers.calendar.editor.to": "받는 사람",
305
319
  "customers.calendar.editor.typeSwitcher": "이벤트 유형",
320
+ "customers.calendar.editor.typeUnavailable": "저장하기 전에 사용 가능한 이벤트 유형을 선택하세요.",
306
321
  "customers.calendar.editor.types.call": "통화",
307
322
  "customers.calendar.editor.types.email": "이메일",
308
323
  "customers.calendar.editor.types.event": "이벤트",
@@ -312,6 +327,8 @@
312
327
  "customers.calendar.editor.validation.assigneeRequired": "이 작업을 팀 구성원에게 할당",
313
328
  "customers.calendar.editor.validation.endsBeforeStarts": "종료 시각은 시작 시각 이후여야 합니다",
314
329
  "customers.calendar.editor.validation.relatedToRequired": "이 이벤트에 연결할 연락처 또는 회사를 선택하세요",
330
+ "customers.calendar.editor.validation.timezoneGap": "선택한 시간대에 이 현지 시간이 존재하지 않습니다",
331
+ "customers.calendar.editor.validation.timezoneInvalid": "유효한 시간대를 선택하세요",
315
332
  "customers.calendar.editor.validation.titleRequired": "제목은 필수입니다",
316
333
  "customers.calendar.empty.action": "새 이벤트",
317
334
  "customers.calendar.empty.agenda": "이 기간에 예정된 일정이 없습니다",
@@ -368,6 +385,7 @@
368
385
  "customers.calendar.settings.conflictWarnings": "충돌 경고",
369
386
  "customers.calendar.settings.eventCategories": "이벤트 카테고리",
370
387
  "customers.calendar.settings.eventCategoriesHint": "직접 지정하는 그룹 라벨입니다(예: 팀 미팅, 영업 통화). 이벤트 생성 시 선택할 수 있습니다.",
388
+ "customers.calendar.settings.manageActivityTypes": "활동 유형 관리",
371
389
  "customers.calendar.settings.max": "(최대 {count}개)",
372
390
  "customers.calendar.settings.removeTag": "{tag} 제거",
373
391
  "customers.calendar.settings.save": "변경사항 저장",
@@ -798,6 +816,71 @@
798
816
  "customers.companies.presets.inactive60": "60일 초과 비활성",
799
817
  "customers.companies.presets.myAccounts": "내 담당 계정",
800
818
  "customers.companies.presets.recentlyCreated": "최근 생성됨",
819
+ "customers.config.activityTypes.appearance": "모양",
820
+ "customers.config.activityTypes.behavior": "양식 동작",
821
+ "customers.config.activityTypes.columns.active": "선택 가능",
822
+ "customers.config.activityTypes.columns.behavior": "동작",
823
+ "customers.config.activityTypes.columns.fields": "필드",
824
+ "customers.config.activityTypes.columns.fieldsets": "필드셋",
825
+ "customers.config.activityTypes.columns.key": "고정 키",
826
+ "customers.config.activityTypes.columns.order": "순서",
827
+ "customers.config.activityTypes.columns.source": "출처",
828
+ "customers.config.activityTypes.columns.type": "활동 유형",
829
+ "customers.config.activityTypes.configure": "설정",
830
+ "customers.config.activityTypes.delete": "삭제",
831
+ "customers.config.activityTypes.empty": "사용 가능한 활동 유형이 없습니다.",
832
+ "customers.config.activityTypes.fields.allDay": "하루 종일",
833
+ "customers.config.activityTypes.fields.baseKind": "기본 동작",
834
+ "customers.config.activityTypes.fields.color": "색상 (#RRGGBB)",
835
+ "customers.config.activityTypes.fields.customFieldsetIds": "사용자 지정 필드셋",
836
+ "customers.config.activityTypes.fields.endTime": "종료 시간",
837
+ "customers.config.activityTypes.fields.icon": "아이콘",
838
+ "customers.config.activityTypes.fields.label": "레이블",
839
+ "customers.config.activityTypes.fields.location": "위치 필드",
840
+ "customers.config.activityTypes.fields.order": "표시 순서",
841
+ "customers.config.activityTypes.fields.people": "사람 필드",
842
+ "customers.config.activityTypes.fields.priority": "우선순위",
843
+ "customers.config.activityTypes.fields.recurrence": "반복",
844
+ "customers.config.activityTypes.fields.resources": "리소스",
845
+ "customers.config.activityTypes.fields.selectable": "새 이벤트에 사용 가능",
846
+ "customers.config.activityTypes.fields.value": "고정 키",
847
+ "customers.config.activityTypes.fieldsetsUnavailable": "필드셋을 사용할 수 없습니다. 저장하기 전에 다시 불러오세요.",
848
+ "customers.config.activityTypes.inactiveHint": "현재 모듈이 이 재정의의 적용을 막고 있습니다.",
849
+ "customers.config.activityTypes.inherited": "상속됨",
850
+ "customers.config.activityTypes.inheritedHint": "저장하면 이 조직에 대한 로컬 재정의가 생성됩니다.",
851
+ "customers.config.activityTypes.invalidBehavior": "양식 동작 값을 확인하세요.",
852
+ "customers.config.activityTypes.invalidKey": "소문자, 숫자, 하이픈 또는 밑줄로 된 키를 사용하세요.",
853
+ "customers.config.activityTypes.loadFailed": "활동 유형을 불러오지 못했습니다.",
854
+ "customers.config.activityTypes.loadingFieldsets": "필드셋 불러오는 중…",
855
+ "customers.config.activityTypes.local": "로컬 재정의",
856
+ "customers.config.activityTypes.missing": "누락됨",
857
+ "customers.config.activityTypes.missingHint": "설정된 일부 필드셋이 더 이상 존재하지 않습니다.",
858
+ "customers.config.activityTypes.new": "새 활동 유형",
859
+ "customers.config.activityTypes.no": "아니요",
860
+ "customers.config.activityTypes.noFieldsets": "아직 사용자 지정 필드셋이 정의되지 않았습니다.",
861
+ "customers.config.activityTypes.options.assignee": "담당자",
862
+ "customers.config.activityTypes.options.attendees": "참석자",
863
+ "customers.config.activityTypes.options.call": "통화",
864
+ "customers.config.activityTypes.options.email": "이메일",
865
+ "customers.config.activityTypes.options.event": "이벤트",
866
+ "customers.config.activityTypes.options.location": "위치",
867
+ "customers.config.activityTypes.options.meeting": "회의",
868
+ "customers.config.activityTypes.options.none": "없음",
869
+ "customers.config.activityTypes.options.note": "메모",
870
+ "customers.config.activityTypes.options.participants": "참여자",
871
+ "customers.config.activityTypes.options.phoneLink": "전화 링크",
872
+ "customers.config.activityTypes.options.recipients": "수신자",
873
+ "customers.config.activityTypes.options.task": "작업",
874
+ "customers.config.activityTypes.refresh": "새로고침",
875
+ "customers.config.activityTypes.removed": "활동 유형 설정을 제거했습니다.",
876
+ "customers.config.activityTypes.reset": "재정의 초기화",
877
+ "customers.config.activityTypes.resetConfirm": "로컬 활동 유형 설정을 제거할까요?",
878
+ "customers.config.activityTypes.retry": "다시 시도",
879
+ "customers.config.activityTypes.save": "저장",
880
+ "customers.config.activityTypes.saveFailed": "활동 유형을 저장하지 못했습니다.",
881
+ "customers.config.activityTypes.saved": "활동 유형을 저장했습니다.",
882
+ "customers.config.activityTypes.view": "보기",
883
+ "customers.config.activityTypes.yes": "예",
801
884
  "customers.config.addressFormat.description": "고객 모듈 전반에서 주소 입력 폼과 표시 방식을 어떻게 구성할지 선택하세요.",
802
885
  "customers.config.addressFormat.error": "주소 설정을 불러오지 못했습니다",
803
886
  "customers.config.addressFormat.errorSave": "주소 설정 업데이트에 실패했습니다",
@@ -193,6 +193,11 @@
193
193
  "customers.audit.todos.link": "Powiąż zadanie",
194
194
  "customers.audit.todos.unlink": "Odepnij zadanie",
195
195
  "customers.calendar.actions.newEvent": "Nowe wydarzenie",
196
+ "customers.calendar.activityTypes.errors.changeConfirmationRequired": "Zmiana typu wydarzenia usunie istniejące wartości.",
197
+ "customers.calendar.activityTypes.errors.fieldNotApplicable": "To pole nie dotyczy wybranego typu aktywności.",
198
+ "customers.calendar.activityTypes.errors.forbidden": "Nie masz uprawnień do wyświetlania typów aktywności.",
199
+ "customers.calendar.activityTypes.errors.loadFailed": "Nie udało się wczytać typów aktywności.",
200
+ "customers.calendar.activityTypes.errors.unavailable": "Ten typ aktywności jest niedostępny.",
196
201
  "customers.calendar.agenda.eventsCount.few": "{count} wydarzenia",
197
202
  "customers.calendar.agenda.eventsCount.many": "{count} wydarzeń",
198
203
  "customers.calendar.agenda.eventsCount.one": "{count} wydarzenie",
@@ -232,6 +237,8 @@
232
237
  "customers.calendar.editor.assigneePlaceholder": "Przypisz do członka zespołu…",
233
238
  "customers.calendar.editor.attendees": "Uczestnicy",
234
239
  "customers.calendar.editor.cancel": "Anuluj",
240
+ "customers.calendar.editor.catalogLoadFailed": "Nie udało się wczytać typów aktywności. Spróbuj ponownie, aby zapisać wydarzenie.",
241
+ "customers.calendar.editor.catalogLoading": "Wczytywanie typów aktywności…",
235
242
  "customers.calendar.editor.category": "Kategoria",
236
243
  "customers.calendar.editor.close": "Zamknij",
237
244
  "customers.calendar.editor.conflictTitle": "Konflikt w kalendarzu",
@@ -249,8 +256,14 @@
249
256
  "customers.calendar.editor.dealsSection": "Szanse",
250
257
  "customers.calendar.editor.description": "Opis",
251
258
  "customers.calendar.editor.descriptionPlaceholder": "Dodaj szczegóły…",
259
+ "customers.calendar.editor.discardConfirm": "Zmień typ i usuń wartości",
260
+ "customers.calendar.editor.discardDescription": "Zmiana typu wydarzenia usunie wartości, które nie dotyczą nowego typu.",
261
+ "customers.calendar.editor.discardFields": "Wartości do usunięcia:",
262
+ "customers.calendar.editor.discardTitle": "Zmienić typ wydarzenia?",
263
+ "customers.calendar.editor.discardUnknownField": "Pole niestandardowe",
252
264
  "customers.calendar.editor.error": "Nie udało się zapisać wydarzenia",
253
265
  "customers.calendar.editor.guestBadge": "Gość",
266
+ "customers.calendar.editor.historicalType": "Ten typ wydarzenia nie jest dostępny dla nowych wydarzeń. Możesz dalej edytować to wydarzenie albo wybrać inny typ.",
254
267
  "customers.calendar.editor.location": "Lokalizacja",
255
268
  "customers.calendar.editor.locationPlaceholder": "Dodaj lokalizację lub link…",
256
269
  "customers.calendar.editor.multiDayHint": "Wydarzenie wielodniowe · {count} dni",
@@ -295,6 +308,7 @@
295
308
  "customers.calendar.editor.saved": "Wydarzenie zapisane",
296
309
  "customers.calendar.editor.saving": "Zapisywanie…",
297
310
  "customers.calendar.editor.searching": "Wyszukiwanie…",
311
+ "customers.calendar.editor.timezone": "Strefa czasowa",
298
312
  "customers.calendar.editor.title.create": "Nowe wydarzenie",
299
313
  "customers.calendar.editor.title.edit": "Edytuj wydarzenie",
300
314
  "customers.calendar.editor.titleLabel.email": "Temat",
@@ -303,6 +317,7 @@
303
317
  "customers.calendar.editor.titlePlaceholder": "Dodaj tytuł…",
304
318
  "customers.calendar.editor.to": "Do",
305
319
  "customers.calendar.editor.typeSwitcher": "Typ wydarzenia",
320
+ "customers.calendar.editor.typeUnavailable": "Przed zapisaniem wybierz dostępny typ wydarzenia.",
306
321
  "customers.calendar.editor.types.call": "Telefon",
307
322
  "customers.calendar.editor.types.email": "E-mail",
308
323
  "customers.calendar.editor.types.event": "Wydarzenie",
@@ -312,6 +327,8 @@
312
327
  "customers.calendar.editor.validation.assigneeRequired": "Przypisz to zadanie do członka zespołu",
313
328
  "customers.calendar.editor.validation.endsBeforeStarts": "Zakończenie musi być po rozpoczęciu",
314
329
  "customers.calendar.editor.validation.relatedToRequired": "Wybierz osobę lub firmę, aby powiązać to wydarzenie",
330
+ "customers.calendar.editor.validation.timezoneGap": "Ta godzina nie istnieje w wybranej strefie czasowej",
331
+ "customers.calendar.editor.validation.timezoneInvalid": "Wybierz poprawną strefę czasową",
315
332
  "customers.calendar.editor.validation.titleRequired": "Tytuł jest wymagany",
316
333
  "customers.calendar.empty.action": "Nowe wydarzenie",
317
334
  "customers.calendar.empty.agenda": "Brak zaplanowanych wydarzeń w tym okresie",
@@ -368,6 +385,7 @@
368
385
  "customers.calendar.settings.conflictWarnings": "Ostrzeżenia o konfliktach",
369
386
  "customers.calendar.settings.eventCategories": "Kategorie wydarzeń",
370
387
  "customers.calendar.settings.eventCategoriesHint": "Twoje własne etykiety grupujące (np. Spotkanie zespołu, Telefon sprzedażowy). Pojawiają się podczas tworzenia wydarzenia.",
388
+ "customers.calendar.settings.manageActivityTypes": "Zarządzaj typami aktywności",
371
389
  "customers.calendar.settings.max": "(maks. {count})",
372
390
  "customers.calendar.settings.removeTag": "Usuń {tag}",
373
391
  "customers.calendar.settings.save": "Zapisz zmiany",
@@ -798,6 +816,71 @@
798
816
  "customers.companies.presets.inactive60": "Nieaktywne > 60 dni",
799
817
  "customers.companies.presets.myAccounts": "Moje konta",
800
818
  "customers.companies.presets.recentlyCreated": "Ostatnio utworzone",
819
+ "customers.config.activityTypes.appearance": "Wygląd",
820
+ "customers.config.activityTypes.behavior": "Zachowanie formularza",
821
+ "customers.config.activityTypes.columns.active": "Możliwość wyboru",
822
+ "customers.config.activityTypes.columns.behavior": "Zachowanie",
823
+ "customers.config.activityTypes.columns.fields": "Pola",
824
+ "customers.config.activityTypes.columns.fieldsets": "Zestawy pól",
825
+ "customers.config.activityTypes.columns.key": "Stały klucz",
826
+ "customers.config.activityTypes.columns.order": "Kolejność",
827
+ "customers.config.activityTypes.columns.source": "Źródło",
828
+ "customers.config.activityTypes.columns.type": "Typ aktywności",
829
+ "customers.config.activityTypes.configure": "Konfiguruj",
830
+ "customers.config.activityTypes.delete": "Usuń",
831
+ "customers.config.activityTypes.empty": "Brak dostępnych typów aktywności.",
832
+ "customers.config.activityTypes.fields.allDay": "Cały dzień",
833
+ "customers.config.activityTypes.fields.baseKind": "Podstawowe zachowanie",
834
+ "customers.config.activityTypes.fields.color": "Kolor (#RRGGBB)",
835
+ "customers.config.activityTypes.fields.customFieldsetIds": "Zestawy pól niestandardowych",
836
+ "customers.config.activityTypes.fields.endTime": "Czas zakończenia",
837
+ "customers.config.activityTypes.fields.icon": "Ikona",
838
+ "customers.config.activityTypes.fields.label": "Etykieta",
839
+ "customers.config.activityTypes.fields.location": "Pole lokalizacji",
840
+ "customers.config.activityTypes.fields.order": "Kolejność wyświetlania",
841
+ "customers.config.activityTypes.fields.people": "Pole osób",
842
+ "customers.config.activityTypes.fields.priority": "Priorytet",
843
+ "customers.config.activityTypes.fields.recurrence": "Powtarzanie",
844
+ "customers.config.activityTypes.fields.resources": "Zasoby",
845
+ "customers.config.activityTypes.fields.selectable": "Dostępny dla nowych wydarzeń",
846
+ "customers.config.activityTypes.fields.value": "Stały klucz",
847
+ "customers.config.activityTypes.fieldsetsUnavailable": "Zestawy pól są niedostępne. Wczytaj je ponownie przed zapisem.",
848
+ "customers.config.activityTypes.inactiveHint": "Moduł obecnie uniemożliwia zastosowanie tego nadpisania.",
849
+ "customers.config.activityTypes.inherited": "Dziedziczony",
850
+ "customers.config.activityTypes.inheritedHint": "Zapis utworzy lokalne nadpisanie dla tej organizacji.",
851
+ "customers.config.activityTypes.invalidBehavior": "Sprawdź wartości zachowania formularza.",
852
+ "customers.config.activityTypes.invalidKey": "Użyj klucza z małych liter, cyfr, łączników lub podkreśleń.",
853
+ "customers.config.activityTypes.loadFailed": "Nie udało się wczytać typów aktywności.",
854
+ "customers.config.activityTypes.loadingFieldsets": "Wczytywanie zestawów pól…",
855
+ "customers.config.activityTypes.local": "Lokalne nadpisanie",
856
+ "customers.config.activityTypes.missing": "brak",
857
+ "customers.config.activityTypes.missingHint": "Niektóre skonfigurowane zestawy pól już nie istnieją.",
858
+ "customers.config.activityTypes.new": "Nowy typ aktywności",
859
+ "customers.config.activityTypes.no": "Nie",
860
+ "customers.config.activityTypes.noFieldsets": "Nie zdefiniowano jeszcze zestawów pól niestandardowych.",
861
+ "customers.config.activityTypes.options.assignee": "Osoba przypisana",
862
+ "customers.config.activityTypes.options.attendees": "Uczestnicy",
863
+ "customers.config.activityTypes.options.call": "Połączenie",
864
+ "customers.config.activityTypes.options.email": "E-mail",
865
+ "customers.config.activityTypes.options.event": "Wydarzenie",
866
+ "customers.config.activityTypes.options.location": "Lokalizacja",
867
+ "customers.config.activityTypes.options.meeting": "Spotkanie",
868
+ "customers.config.activityTypes.options.none": "Brak",
869
+ "customers.config.activityTypes.options.note": "Notatka",
870
+ "customers.config.activityTypes.options.participants": "Osoby uczestniczące",
871
+ "customers.config.activityTypes.options.phoneLink": "Łącze telefoniczne",
872
+ "customers.config.activityTypes.options.recipients": "Odbiorcy",
873
+ "customers.config.activityTypes.options.task": "Zadanie",
874
+ "customers.config.activityTypes.refresh": "Odśwież",
875
+ "customers.config.activityTypes.removed": "Usunięto konfigurację typu aktywności.",
876
+ "customers.config.activityTypes.reset": "Zresetuj nadpisanie",
877
+ "customers.config.activityTypes.resetConfirm": "Usunąć lokalną konfigurację typu aktywności?",
878
+ "customers.config.activityTypes.retry": "Ponów",
879
+ "customers.config.activityTypes.save": "Zapisz",
880
+ "customers.config.activityTypes.saveFailed": "Nie udało się zapisać typu aktywności.",
881
+ "customers.config.activityTypes.saved": "Zapisano typ aktywności.",
882
+ "customers.config.activityTypes.view": "Wyświetl",
883
+ "customers.config.activityTypes.yes": "Tak",
801
884
  "customers.config.addressFormat.description": "Wybierz, jak powinny być zbudowane formularze i widoki w module klientów.",
802
885
  "customers.config.addressFormat.error": "Nie udało się wczytać ustawień adresu",
803
886
  "customers.config.addressFormat.errorSave": "Nie udało się zaktualizować ustawień adresu",
@@ -1,29 +1,70 @@
1
1
  import type { CalendarItem, CalendarItemStatus, CalendarParticipant } from '../../components/calendar/types'
2
+ import { parseLinkedEntities } from './editorPayload'
2
3
  import { participantActorKey } from './participantIdentity'
3
4
  import type { ConflictScope } from './preferences'
4
5
 
5
6
  export const EDITOR_DRAFT_CONFLICT_ID = '__draft__'
6
7
 
7
8
  export type FindConflictsOptions = {
8
- // 'all' (default) flags any actor-sharing overlap; 'mine' only flags overlaps
9
- // where `currentUserId` is an actor of BOTH events (the user is double-booked).
10
- // 'mine' with no `currentUserId` degrades to 'all' — it cannot resolve "mine".
9
+ // 'all' (default) flags any overlap that shares a person or a resource. 'mine'
10
+ // narrows the PEOPLE overlaps to those where `currentUserId` is an actor of BOTH
11
+ // events (the user is double-booked); a shared resource is flagged in both
12
+ // scopes. 'mine' with no `currentUserId` degrades to 'all' — it cannot resolve
13
+ // "mine".
11
14
  scope?: ConflictScope
12
15
  currentUserId?: string | null
13
16
  }
14
17
 
15
- function sharesActor(first: CalendarItem, second: CalendarItem): boolean {
16
- if (first.ownerUserId && second.ownerUserId && first.ownerUserId === second.ownerUserId) return true
17
- if (first.participants.length === 0 || second.participants.length === 0) return false
18
- const firstActorKeys = new Set(
19
- first.participants
20
- .map(participantActorKey)
21
- .filter((actorKey): actorKey is string => actorKey !== null),
22
- )
23
- return second.participants.some((participant) => {
24
- const actorKey = participantActorKey(participant)
25
- return actorKey !== null && firstActorKeys.has(actorKey)
26
- })
18
+ export type ConflictSubject = { kind: 'person' | 'resource'; key: string; label: string }
19
+
20
+ function participantLabel(participant: CalendarParticipant): string {
21
+ return participant.name?.trim() || participant.email?.trim() || ''
22
+ }
23
+
24
+ function bookedResources(item: CalendarItem): Array<{ id: string; label: string }> {
25
+ return parseLinkedEntities((item.raw as { linkedEntities?: unknown }).linkedEntities).resources
26
+ }
27
+
28
+ /**
29
+ * The people and resources two events have in common — what makes an overlap a
30
+ * double-booking. A person is shared through the owner or a participant; a
31
+ * resource through a `resource` link. Labels are empty when the event carries no
32
+ * name for the subject.
33
+ */
34
+ export function sharedConflictSubjects(first: CalendarItem, second: CalendarItem): ConflictSubject[] {
35
+ const shared = new Map<string, ConflictSubject>()
36
+ const labelOf = (item: CalendarItem, actorKey: string): string => {
37
+ const match = item.participants.find((participant) => participantActorKey(participant) === actorKey)
38
+ return match ? participantLabel(match) : ''
39
+ }
40
+ if (first.ownerUserId && second.ownerUserId && first.ownerUserId === second.ownerUserId) {
41
+ const key = `user:${first.ownerUserId}`
42
+ shared.set(key, { kind: 'person', key, label: labelOf(first, key) || labelOf(second, key) })
43
+ }
44
+ if (first.participants.length > 0 && second.participants.length > 0) {
45
+ const secondActorKeys = new Set(
46
+ second.participants
47
+ .map(participantActorKey)
48
+ .filter((actorKey): actorKey is string => actorKey !== null),
49
+ )
50
+ for (const participant of first.participants) {
51
+ const key = participantActorKey(participant)
52
+ if (key === null || !secondActorKeys.has(key) || shared.has(key)) continue
53
+ shared.set(key, { kind: 'person', key, label: participantLabel(participant) || labelOf(second, key) })
54
+ }
55
+ }
56
+ const firstResources = bookedResources(first)
57
+ if (firstResources.length > 0) {
58
+ const secondResources = new Map(bookedResources(second).map((resource) => [resource.id, resource]))
59
+ for (const resource of firstResources) {
60
+ const other = secondResources.get(resource.id)
61
+ if (!other) continue
62
+ const key = `resource:${resource.id}`
63
+ const label = resource.label !== resource.id ? resource.label : other.label !== other.id ? other.label : ''
64
+ shared.set(key, { kind: 'resource', key, label })
65
+ }
66
+ }
67
+ return [...shared.values()]
27
68
  }
28
69
 
29
70
  function isActor(item: CalendarItem, userId: string): boolean {
@@ -52,12 +93,20 @@ export function findConflicts(items: CalendarItem[], options: FindConflictsOptio
52
93
  const other = candidates[lookahead]
53
94
  if (other.start.getTime() >= currentEnd) break
54
95
  if (current.start.getTime() >= other.end.getTime()) continue
55
- // Org-wide rule first: the two events must share at least one actor.
56
- if (!sharesActor(current, other)) continue
57
- // 'mine' narrows that set to the current user's OWN double-bookings — they
58
- // must be an actor of both events. Because they are then the shared actor,
59
- // 'mine' is always a strict subset of 'all' (it never invents new conflicts).
60
- if (restrictToCurrentUser && !(isActor(current, currentUserId) && isActor(other, currentUserId))) continue
96
+ // Org-wide rule first: the two events must share at least one actor or
97
+ // booked resource.
98
+ const shared = sharedConflictSubjects(current, other)
99
+ if (shared.length === 0) continue
100
+ // 'mine' narrows the people overlaps to the current user's OWN
101
+ // double-bookings — they must be an actor of both events. Because they are
102
+ // then the shared actor, 'mine' is always a strict subset of 'all' (it never
103
+ // invents new conflicts). A double-booked resource is not anyone's in
104
+ // particular — a room cannot host two events — so it is flagged in both scopes.
105
+ if (
106
+ restrictToCurrentUser
107
+ && !shared.some((subject) => subject.kind === 'resource')
108
+ && !(isActor(current, currentUserId) && isActor(other, currentUserId))
109
+ ) continue
61
110
  appendConflict(conflicts, current.id, other.id)
62
111
  appendConflict(conflicts, other.id, current.id)
63
112
  }
@@ -74,20 +123,29 @@ export type EditorConflictDraft = {
74
123
  // exactly as the grid excludes canceled items, so the editor never warns about
75
124
  // a record the grid would not badge/ring. Defaults to 'planned'.
76
125
  status?: CalendarItemStatus
126
+ // Resources the draft books. Omitted when the type has no resources field.
127
+ resources?: Array<{ id: string; label: string }>
128
+ // Display name of `ownerUserId`, when the editor knows it (a task's assignee).
129
+ // An owner who is not also a participant has no name on either event.
130
+ ownerLabel?: string | null
77
131
  }
78
132
 
133
+ export type EditorConflict = { item: CalendarItem; shared: ConflictSubject[] }
134
+
79
135
  /**
80
136
  * Resolves the items a calendar editor draft conflicts with, using the SAME
81
- * `findConflicts` rules as the grid (overlap + shared owner/participant) so the
82
- * editor's conflict warning is always consistent with the grid badges/rings.
83
- * The edited record itself (`excludeId`) is never treated as a conflict.
137
+ * `findConflicts` rules as the grid (overlap + shared owner/participant/resource)
138
+ * so the editor's conflict warning is always consistent with the grid
139
+ * badges/rings. Each conflict names the people and resources that are
140
+ * double-booked. The edited record itself (`excludeId`) is never treated as a
141
+ * conflict.
84
142
  */
85
- export function findEditorConflictItems(
143
+ export function findEditorConflicts(
86
144
  draft: EditorConflictDraft,
87
145
  others: CalendarItem[],
88
146
  excludeId: string | null,
89
147
  options: FindConflictsOptions = {},
90
- ): CalendarItem[] {
148
+ ): EditorConflict[] {
91
149
  const draftStatus: CalendarItemStatus = draft.status ?? 'planned'
92
150
  const draftItem: CalendarItem = {
93
151
  id: EDITOR_DRAFT_CONFLICT_ID,
@@ -108,7 +166,12 @@ export function findEditorConflictItems(
108
166
  color: null,
109
167
  isRecurringOccurrence: false,
110
168
  updatedAt: null,
111
- raw: { id: EDITOR_DRAFT_CONFLICT_ID, interactionType: 'meeting', status: draftStatus },
169
+ raw: {
170
+ id: EDITOR_DRAFT_CONFLICT_ID,
171
+ interactionType: 'meeting',
172
+ status: draftStatus,
173
+ linkedEntities: (draft.resources ?? []).map((resource) => ({ id: resource.id, type: 'resource', label: resource.label })),
174
+ },
112
175
  }
113
176
  // Exclude the edited record by its underlying interaction id (`raw.id`), which
114
177
  // also drops every expanded occurrence of an edited recurring series (those
@@ -122,4 +185,20 @@ export function findEditorConflictItems(
122
185
  return conflictIds
123
186
  .map((id) => byId.get(id))
124
187
  .filter((item): item is CalendarItem => Boolean(item))
188
+ .map((item) => ({
189
+ item,
190
+ shared: sharedConflictSubjects(draftItem, item).map((subject) =>
191
+ !subject.label && draft.ownerLabel && subject.key === `user:${draft.ownerUserId}`
192
+ ? { ...subject, label: draft.ownerLabel }
193
+ : subject),
194
+ }))
195
+ }
196
+
197
+ export function findEditorConflictItems(
198
+ draft: EditorConflictDraft,
199
+ others: CalendarItem[],
200
+ excludeId: string | null,
201
+ options: FindConflictsOptions = {},
202
+ ): CalendarItem[] {
203
+ return findEditorConflicts(draft, others, excludeId, options).map((conflict) => conflict.item)
125
204
  }
@@ -1,5 +1,7 @@
1
1
  import type { CalendarItem } from '../../components/calendar/types'
2
+ import { calendarEventTypeKeySchema, resolveCalendarEventType } from '../../calendar-event-types'
2
3
  import { parseRecurrenceRule } from './recurrence'
4
+ import { calendarDayEndInstant, calendarDayStartInstant, calendarInstantToWallTime, calendarWallTimeToInstant, defaultCalendarTimezone, isCalendarTimezone } from './timezone'
3
5
 
4
6
  export type EditorKind = 'meeting' | 'call' | 'email' | 'note' | 'event' | 'task'
5
7
 
@@ -41,7 +43,11 @@ const KIND_BY_INTERACTION_TYPE: Record<string, EditorKind> = {
41
43
  deadline: 'task',
42
44
  }
43
45
 
46
+ /** @deprecated Resolve the effective calendar event type and read `behavior.baseKind`. */
44
47
  export function editorKindOfInteractionType(interactionType: string): EditorKind {
48
+ const parsedKey = calendarEventTypeKeySchema.safeParse(interactionType)
49
+ const effectiveKind = parsedKey.success ? resolveCalendarEventType(parsedKey.data)?.behavior.baseKind : undefined
50
+ if (effectiveKind) return effectiveKind
45
51
  return KIND_BY_INTERACTION_TYPE[interactionType] ?? 'meeting'
46
52
  }
47
53
 
@@ -161,6 +167,7 @@ export type EditorFormState = {
161
167
  allDay: boolean
162
168
  date: string
163
169
  startTime: string
170
+ timezone?: string
164
171
  endDate: string
165
172
  endTime: string
166
173
  repeatFreq: EditorRepeatFreq
@@ -245,6 +252,7 @@ export function createDefaultFormState(
245
252
  }
246
253
  return {
247
254
  kind: 'meeting',
255
+ timezone: defaultCalendarTimezone(),
248
256
  title: '',
249
257
  relatedTo: null,
250
258
  dealId: null,
@@ -286,21 +294,23 @@ export function buildRecurrenceRule(state: EditorFormState): string | null {
286
294
  }
287
295
  if (state.repeatEndType === 'count') rule += `;COUNT=${state.repeatCount}`
288
296
  if (state.repeatEndType === 'date' && state.repeatUntilDate) {
289
- rule += `;UNTIL=${state.repeatUntilDate.replace(/-/g, '')}T235959Z`
297
+ const until = state.timezone ? calendarDayEndInstant(state.repeatUntilDate, state.timezone) : null
298
+ rule += `;UNTIL=${until ? until.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z') : `${state.repeatUntilDate.replace(/-/g, '')}T235959Z`}`
290
299
  }
291
300
  return rule
292
301
  }
293
302
 
294
303
  export function computeDurationMinutes(state: EditorFormState): number | null {
295
- const start = new Date(`${state.date}T${state.startTime}:00`)
296
- const end = new Date(`${state.endDate}T${state.endTime}:00`)
297
- if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return null
304
+ const start = state.timezone ? calendarWallTimeToInstant(state.date, state.startTime, state.timezone) : new Date(`${state.date}T${state.startTime}:00`)
305
+ const end = state.timezone ? calendarWallTimeToInstant(state.endDate, state.endTime, state.timezone) : new Date(`${state.endDate}T${state.endTime}:00`)
306
+ if (!start || !end || Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return null
298
307
  const minutes = Math.round((end.getTime() - start.getTime()) / 60_000)
299
308
  return minutes > 0 ? minutes : null
300
309
  }
301
310
 
302
311
  export type BuildPayloadOptions = {
303
312
  mode: 'create' | 'edit'
313
+ config?: EditorKindConfig
304
314
  id?: string
305
315
  /**
306
316
  * When the resources module is loaded the payload owns `linkedEntities`
@@ -317,10 +327,16 @@ export type BuildPayloadOptions = {
317
327
  }
318
328
 
319
329
  export function buildInteractionPayload(state: EditorFormState, options: BuildPayloadOptions): Record<string, unknown> {
320
- const config = KIND_CONFIG[state.kind]
330
+ const config = options.config ?? KIND_CONFIG[state.kind]
321
331
  const time = state.allDay && config.hasAllDay ? '00:00' : state.startTime
322
- const scheduledAt = new Date(`${state.date}T${time}:00`).toISOString()
332
+ const instant = state.timezone ? (state.allDay && config.hasAllDay ? calendarDayStartInstant(state.date, state.timezone) : calendarWallTimeToInstant(state.date, time, state.timezone)) : new Date(`${state.date}T${time}:00`)
333
+ if (!instant || !Number.isFinite(instant.getTime())) throw new Error('[internal] Invalid calendar local time')
334
+ const scheduledAt = instant.toISOString()
323
335
  const recurrenceRule = config.hasRepeat ? buildRecurrenceRule(state) : null
336
+ const recurrenceEnd = recurrenceRule && state.repeatEndType === 'date' && state.repeatUntilDate
337
+ ? state.timezone ? calendarDayEndInstant(state.repeatUntilDate, state.timezone) : new Date(state.repeatUntilDate)
338
+ : null
339
+ if (recurrenceRule && state.repeatEndType === 'date' && state.repeatUntilDate && !recurrenceEnd) throw new Error('[internal] Invalid calendar recurrence day')
324
340
  const payload: Record<string, unknown> = {
325
341
  ...(options.mode === 'edit' && options.id ? { id: options.id } : {}),
326
342
  entityId: state.relatedTo?.id ?? null,
@@ -332,14 +348,12 @@ export function buildInteractionPayload(state: EditorFormState, options: BuildPa
332
348
  date: state.date,
333
349
  time,
334
350
  scheduledAt,
351
+ ...(state.timezone ? { timezone: state.timezone } : {}),
335
352
  durationMinutes: config.hasEnd && !(state.allDay && config.hasAllDay) ? computeDurationMinutes(state) : null,
336
353
  allDay: config.hasAllDay ? state.allDay : null,
337
354
  location: config.location ? state.location.trim() || null : null,
338
355
  recurrenceRule,
339
- recurrenceEnd:
340
- recurrenceRule && state.repeatEndType === 'date' && state.repeatUntilDate
341
- ? new Date(state.repeatUntilDate).toISOString()
342
- : null,
356
+ recurrenceEnd: recurrenceEnd?.toISOString() ?? null,
343
357
  participants:
344
358
  config.people && config.people !== 'assignee' && state.participants.length > 0
345
359
  ? state.participants.map((participant) => ({
@@ -374,10 +388,11 @@ function readUnknownNumber(value: unknown): number | null {
374
388
 
375
389
  type ParsedRepeat = Pick<EditorFormState, 'repeatFreq' | 'repeatDays' | 'repeatEndType' | 'repeatCount' | 'repeatUntilDate'>
376
390
 
377
- function parseRepeatFromRule(rawRule: unknown, start: Date): ParsedRepeat {
391
+ function parseRepeatFromRule(rawRule: unknown, start: Date, timezone?: string): ParsedRepeat {
392
+ const wallStart = timezone ? new Date(`${calendarInstantToWallTime(start, timezone).date}T12:00:00`) : start
378
393
  const fallback: ParsedRepeat = {
379
394
  repeatFreq: 'none',
380
- repeatDays: defaultRepeatDays(start),
395
+ repeatDays: defaultRepeatDays(wallStart),
381
396
  repeatEndType: 'never',
382
397
  repeatCount: 8,
383
398
  repeatUntilDate: '',
@@ -386,7 +401,7 @@ function parseRepeatFromRule(rawRule: unknown, start: Date): ParsedRepeat {
386
401
  if (!ruleText) return fallback
387
402
  const parsed = parseRecurrenceRule(ruleText)
388
403
  if (!parsed) return fallback
389
- const repeatDays = defaultRepeatDays(start)
404
+ const repeatDays = defaultRepeatDays(wallStart)
390
405
  if (parsed.byDay) {
391
406
  repeatDays.fill(false)
392
407
  for (const jsWeekday of parsed.byDay) repeatDays[(jsWeekday + 6) % 7] = true
@@ -399,7 +414,7 @@ function parseRepeatFromRule(rawRule: unknown, start: Date): ParsedRepeat {
399
414
  repeatCount = parsed.count
400
415
  } else if (parsed.until) {
401
416
  repeatEndType = 'date'
402
- repeatUntilDate = `${parsed.until.getUTCFullYear()}-${padDatePart(parsed.until.getUTCMonth() + 1)}-${padDatePart(parsed.until.getUTCDate())}`
417
+ repeatUntilDate = timezone ? calendarInstantToWallTime(parsed.until, timezone).date : `${parsed.until.getUTCFullYear()}-${padDatePart(parsed.until.getUTCMonth() + 1)}-${padDatePart(parsed.until.getUTCDate())}`
403
418
  }
404
419
  return {
405
420
  repeatFreq: parsed.freq === 'DAILY' ? 'daily' : 'weekly',
@@ -427,6 +442,9 @@ export function parseItemToFormState(item: CalendarItem): EditorFormState {
427
442
  const kind = editorKindOfInteractionType(item.interactionType)
428
443
  const raw = item.raw as Record<string, unknown>
429
444
  const { resources, preservedLinkedEntities } = parseLinkedEntities(raw.linkedEntities)
445
+ const timezone = isCalendarTimezone(raw.timezone) ? raw.timezone : defaultCalendarTimezone()
446
+ const startWall = calendarInstantToWallTime(item.start, timezone)
447
+ const endWall = calendarInstantToWallTime(item.end, timezone)
430
448
  return {
431
449
  kind,
432
450
  title: item.title,
@@ -434,11 +452,13 @@ export function parseItemToFormState(item: CalendarItem): EditorFormState {
434
452
  dealId: item.dealId,
435
453
  dealLabel: null,
436
454
  allDay: item.allDay,
437
- date: formatLocalDateInput(item.start),
438
- startTime: formatLocalTimeInput(item.start),
439
- endDate: formatLocalDateInput(item.end),
440
- endTime: formatLocalTimeInput(item.end),
441
- ...parseRepeatFromRule(item.raw.recurrenceRule, item.start),
455
+ timezone,
456
+ date: startWall.date,
457
+ startTime: startWall.time,
458
+ endDate: endWall.date,
459
+ endTime: endWall.time,
460
+ ...parseRepeatFromRule(item.raw.recurrenceRule, item.start, isCalendarTimezone(raw.timezone) ? timezone : undefined),
461
+ ...(item.raw.recurrenceEnd && isCalendarTimezone(raw.timezone) ? { repeatUntilDate: calendarInstantToWallTime(new Date(item.raw.recurrenceEnd), timezone).date } : {}),
442
462
  category: item.interactionType,
443
463
  location: item.location ?? '',
444
464
  participants: parseParticipants(item),