@open-mercato/core 0.9.0 → 0.9.1-develop.7459.1.99e3accd30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
- package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
- package/dist/generated/entities/customer_interaction/index.js +2 -0
- package/dist/generated/entities/customer_interaction/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/commands/categories.js +1 -1
- package/dist/modules/catalog/commands/categories.js.map +2 -2
- package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
- package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +52 -17
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
- package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
- package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
- package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
- package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
- package/dist/modules/customers/api/activity-types/route.js +151 -0
- package/dist/modules/customers/api/activity-types/route.js.map +7 -0
- package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -0
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/calendar-event-types.js +548 -0
- package/dist/modules/customers/calendar-event-types.js.map +7 -0
- package/dist/modules/customers/commands/dictionaries.js +70 -10
- package/dist/modules/customers/commands/dictionaries.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +187 -22
- package/dist/modules/customers/commands/interactions.js.map +3 -3
- package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
- package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
- package/dist/modules/customers/components/DictionarySettings.js +3 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
- package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
- package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
- package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
- package/dist/modules/customers/components/calendar/types.js +1 -0
- package/dist/modules/customers/components/calendar/types.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
- package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +7 -3
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
- package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonCard.js +5 -2
- package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
- package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
- package/dist/modules/customers/data/entities.js +6 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +39 -10
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/di.js +20 -1
- package/dist/modules/customers/di.js.map +2 -2
- package/dist/modules/customers/extension-points.js +13 -0
- package/dist/modules/customers/extension-points.js.map +2 -2
- package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
- package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
- package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
- package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
- package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
- package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
- package/dist/modules/customers/lib/calendar/timezone.js +111 -0
- package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
- package/dist/modules/customers/lib/interactionReadModel.js +1 -0
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +1 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
- package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
- package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
- package/dist/modules/messages/subscribers/message-notification.js +13 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/planner/lib/availabilityMerge.js +101 -38
- package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
- package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/generated/entities/customer_dictionary_entry/index.ts +1 -0
- package/generated/entities/customer_interaction/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +9 -8
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/catalog/commands/categories.ts +1 -1
- package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
- package/src/modules/catalog/commands/products.ts +58 -17
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
- package/src/modules/communication_channels/i18n/de.json +4 -0
- package/src/modules/communication_channels/i18n/en.json +4 -0
- package/src/modules/communication_channels/i18n/es.json +4 -0
- package/src/modules/communication_channels/i18n/ko.json +4 -0
- package/src/modules/communication_channels/i18n/pl.json +4 -0
- package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
- package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
- package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
- package/src/modules/customers/api/activity-types/route.ts +154 -0
- package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
- package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
- package/src/modules/customers/api/interactions/route.ts +4 -0
- package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
- package/src/modules/customers/calendar-event-types.ts +674 -0
- package/src/modules/customers/commands/dictionaries.ts +84 -8
- package/src/modules/customers/commands/interactions.ts +244 -21
- package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
- package/src/modules/customers/components/DictionarySettings.tsx +5 -3
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
- package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
- package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
- package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
- package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
- package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
- package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
- package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
- package/src/modules/customers/components/detail/DealForm.tsx +7 -3
- package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
- package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
- package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
- package/src/modules/customers/components/detail/types.ts +1 -0
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
- package/src/modules/customers/data/entities.ts +8 -1
- package/src/modules/customers/data/validators.ts +42 -7
- package/src/modules/customers/di.ts +21 -0
- package/src/modules/customers/extension-points.ts +13 -0
- package/src/modules/customers/i18n/de.json +104 -8
- package/src/modules/customers/i18n/en.json +104 -8
- package/src/modules/customers/i18n/es.json +104 -8
- package/src/modules/customers/i18n/ko.json +104 -8
- package/src/modules/customers/i18n/pl.json +104 -8
- package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
- package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
- package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
- package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
- package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
- package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
- package/src/modules/customers/lib/calendar/timezone.ts +117 -0
- package/src/modules/customers/lib/interactionReadModel.ts +1 -0
- package/src/modules/customers/lib/todoCompatibility.ts +2 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
- package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
- package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
- package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
- package/src/modules/messages/subscribers/message-notification.ts +13 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
- package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
- package/src/modules/planner/lib/availabilityMerge.ts +106 -41
- package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
- package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
- package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
|
+
import { waitForAsyncRegistration } from '@open-mercato/shared/lib/bootstrap/factory'
|
|
4
|
+
import {
|
|
5
|
+
getInjectionRegistryVersion,
|
|
6
|
+
loadInjectionWidgetsForSpot,
|
|
7
|
+
} from '@open-mercato/shared/modules/widgets/injection-loader'
|
|
8
|
+
import {
|
|
9
|
+
calendarEventTypeBehaviorSchema,
|
|
10
|
+
calendarEventTypes,
|
|
11
|
+
getCalendarEventTypes,
|
|
12
|
+
getCalendarEventTypeTombstones,
|
|
13
|
+
resolveCalendarEventType,
|
|
14
|
+
registerWidgetCalendarEventTypeContributions,
|
|
15
|
+
type CalendarEventTypeBehavior,
|
|
16
|
+
type CalendarEventTypeProvenance,
|
|
17
|
+
type EffectiveCalendarEventType,
|
|
18
|
+
} from '../../calendar-event-types'
|
|
19
|
+
import { CustomerDictionaryEntry } from '../../data/entities'
|
|
20
|
+
import { extensionPoints } from '../../extension-points'
|
|
21
|
+
|
|
22
|
+
export type ScopedCalendarEventType = Readonly<{
|
|
23
|
+
key: string
|
|
24
|
+
label: string
|
|
25
|
+
labelKey?: string
|
|
26
|
+
icon?: string | null
|
|
27
|
+
color?: string | null
|
|
28
|
+
behavior: CalendarEventTypeBehavior
|
|
29
|
+
selectable: boolean
|
|
30
|
+
panelKey?: string
|
|
31
|
+
source: string
|
|
32
|
+
provenance: Readonly<Record<string, CalendarEventTypeProvenance>>
|
|
33
|
+
historical: boolean
|
|
34
|
+
fallbackReason?: 'tombstoned' | 'module-unavailable' | 'unknown'
|
|
35
|
+
adminConfigurable: boolean
|
|
36
|
+
isInherited: boolean
|
|
37
|
+
isLocalOverride: boolean
|
|
38
|
+
updatedAt: string | null
|
|
39
|
+
missingCustomFieldsetIds: readonly string[]
|
|
40
|
+
inactiveDictionaryOverride?: boolean
|
|
41
|
+
}>
|
|
42
|
+
|
|
43
|
+
export type CalendarEventTypeCatalog = Readonly<{
|
|
44
|
+
items: readonly ScopedCalendarEventType[]
|
|
45
|
+
fallbackKey: 'meeting'
|
|
46
|
+
}>
|
|
47
|
+
|
|
48
|
+
type ResolveCalendarEventTypesInput = {
|
|
49
|
+
em: EntityManager
|
|
50
|
+
tenantId: string
|
|
51
|
+
organizationId: string
|
|
52
|
+
readableOrganizationIds: readonly string[]
|
|
53
|
+
availableCustomFieldsetIdsFromEntitiesBoundary?: readonly string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function deepFreeze<T>(value: T): T {
|
|
57
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
58
|
+
Object.freeze(value)
|
|
59
|
+
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child)
|
|
60
|
+
}
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function serializeStaticType(type: EffectiveCalendarEventType): ScopedCalendarEventType {
|
|
65
|
+
return deepFreeze({
|
|
66
|
+
key: type.key,
|
|
67
|
+
label: type.label,
|
|
68
|
+
...(type.labelKey ? { labelKey: type.labelKey } : {}),
|
|
69
|
+
icon: type.icon ?? null,
|
|
70
|
+
color: type.color ?? null,
|
|
71
|
+
behavior: type.behavior,
|
|
72
|
+
selectable: type.behavior.selectable,
|
|
73
|
+
...(type.panelKey ? { panelKey: type.panelKey } : {}),
|
|
74
|
+
source: type.provenance.key?.moduleId ?? 'customers',
|
|
75
|
+
provenance: type.provenance,
|
|
76
|
+
historical: type.historical,
|
|
77
|
+
...(type.fallbackReason ? { fallbackReason: type.fallbackReason } : {}),
|
|
78
|
+
adminConfigurable: type.adminConfigurable !== false,
|
|
79
|
+
isInherited: false,
|
|
80
|
+
isLocalOverride: false,
|
|
81
|
+
updatedAt: null,
|
|
82
|
+
missingCustomFieldsetIds: [],
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveBaselineCalendarEventTypes(): CalendarEventTypeCatalog {
|
|
87
|
+
const types: readonly EffectiveCalendarEventType[] = getCalendarEventTypes()
|
|
88
|
+
return deepFreeze({ items: types.map(serializeStaticType), fallbackKey: 'meeting' })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let loadedWidgetVersion = -1
|
|
92
|
+
let loadingWidgets: Promise<void> | null = null
|
|
93
|
+
|
|
94
|
+
async function ensureCalendarEventTypeWidgetsLoaded(): Promise<void> {
|
|
95
|
+
await waitForAsyncRegistration()
|
|
96
|
+
const version = getInjectionRegistryVersion()
|
|
97
|
+
if (loadedWidgetVersion === version) return
|
|
98
|
+
if (loadingWidgets) return loadingWidgets
|
|
99
|
+
const pending = (async () => {
|
|
100
|
+
const widgets = await loadInjectionWidgetsForSpot(extensionPoints.hosts.calendarEventTypes.spotId)
|
|
101
|
+
registerWidgetCalendarEventTypeContributions(widgets.map((widget) => {
|
|
102
|
+
return {
|
|
103
|
+
moduleId: widget.moduleId,
|
|
104
|
+
widgetId: widget.metadata.id,
|
|
105
|
+
priority: typeof widget.placement?.priority === 'number' ? widget.placement.priority : 0,
|
|
106
|
+
definitions: 'eventTypes' in widget ? widget.eventTypes : null,
|
|
107
|
+
overrides: 'eventTypeOverrides' in widget ? widget.eventTypeOverrides : undefined,
|
|
108
|
+
patches: 'eventTypePatches' in widget ? widget.eventTypePatches : undefined,
|
|
109
|
+
}
|
|
110
|
+
}))
|
|
111
|
+
loadedWidgetVersion = version
|
|
112
|
+
})()
|
|
113
|
+
loadingWidgets = pending
|
|
114
|
+
try {
|
|
115
|
+
await pending
|
|
116
|
+
} finally {
|
|
117
|
+
if (loadingWidgets === pending) loadingWidgets = null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function selectDictionaryRows(
|
|
122
|
+
rows: readonly CustomerDictionaryEntry[],
|
|
123
|
+
organizationId: string,
|
|
124
|
+
readableOrganizationIds: readonly string[],
|
|
125
|
+
): Map<string, CustomerDictionaryEntry> {
|
|
126
|
+
const orderedOrganizationIds = Array.from(new Set([organizationId, ...readableOrganizationIds]))
|
|
127
|
+
const rank = new Map(orderedOrganizationIds.map((id, index) => [id, index]))
|
|
128
|
+
const sorted = [...rows].sort((left, right) => {
|
|
129
|
+
const rankDelta = (rank.get(left.organizationId) ?? Number.MAX_SAFE_INTEGER) -
|
|
130
|
+
(rank.get(right.organizationId) ?? Number.MAX_SAFE_INTEGER)
|
|
131
|
+
return rankDelta || left.normalizedValue.localeCompare(right.normalizedValue)
|
|
132
|
+
})
|
|
133
|
+
const selected = new Map<string, CustomerDictionaryEntry>()
|
|
134
|
+
for (const row of sorted) {
|
|
135
|
+
const key = row.normalizedValue.trim() || row.value.trim().toLowerCase()
|
|
136
|
+
if (key && !selected.has(key)) selected.set(key, row)
|
|
137
|
+
}
|
|
138
|
+
return selected
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isUnchangedShippedDictionaryRow(row: CustomerDictionaryEntry, key: string): boolean {
|
|
142
|
+
const shipped = calendarEventTypes.find((entry) => entry.key === key)
|
|
143
|
+
return !!shipped && !row.activityTypeBehavior && row.label === shipped.label &&
|
|
144
|
+
row.icon === shipped.icon && row.color === shipped.color
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function resolveScopedCalendarEventTypes(
|
|
148
|
+
input: ResolveCalendarEventTypesInput,
|
|
149
|
+
): Promise<CalendarEventTypeCatalog> {
|
|
150
|
+
await ensureCalendarEventTypeWidgetsLoaded()
|
|
151
|
+
const scopeOrganizationIds = Array.from(new Set([input.organizationId, ...input.readableOrganizationIds]))
|
|
152
|
+
const rows = await findWithDecryption(
|
|
153
|
+
input.em,
|
|
154
|
+
CustomerDictionaryEntry,
|
|
155
|
+
{
|
|
156
|
+
tenantId: input.tenantId,
|
|
157
|
+
organizationId: { $in: scopeOrganizationIds },
|
|
158
|
+
kind: 'activity_type',
|
|
159
|
+
},
|
|
160
|
+
{ orderBy: { label: 'asc' } },
|
|
161
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId },
|
|
162
|
+
)
|
|
163
|
+
const dictionaryRows = selectDictionaryRows(rows, input.organizationId, scopeOrganizationIds)
|
|
164
|
+
for (const [key, row] of dictionaryRows) {
|
|
165
|
+
if (isUnchangedShippedDictionaryRow(row, key)) dictionaryRows.delete(key)
|
|
166
|
+
}
|
|
167
|
+
const staticCatalog = resolveBaselineCalendarEventTypes()
|
|
168
|
+
const availableFieldsets = input.availableCustomFieldsetIdsFromEntitiesBoundary
|
|
169
|
+
? new Set(input.availableCustomFieldsetIdsFromEntitiesBoundary)
|
|
170
|
+
: null
|
|
171
|
+
const staticByKey = new Map(staticCatalog.items.map((item) => [item.key, item]))
|
|
172
|
+
const tombstonedKeys = new Set(getCalendarEventTypeTombstones().map((entry) => entry.key))
|
|
173
|
+
const keys = new Set([...staticByKey.keys(), ...[...dictionaryRows.keys()].filter((key) => !tombstonedKeys.has(key))])
|
|
174
|
+
const fallbackBehavior = calendarEventTypes[0]!.behavior
|
|
175
|
+
const items = [...keys].map((key): ScopedCalendarEventType => {
|
|
176
|
+
const base = staticByKey.get(key)
|
|
177
|
+
const row = dictionaryRows.get(key)
|
|
178
|
+
if (!row) {
|
|
179
|
+
if (!availableFieldsets) return base!
|
|
180
|
+
return deepFreeze({
|
|
181
|
+
...base!,
|
|
182
|
+
missingCustomFieldsetIds: base!.behavior.customFieldsetIds.filter((id) => !availableFieldsets.has(id)),
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
if (base?.adminConfigurable === false) {
|
|
186
|
+
return deepFreeze({
|
|
187
|
+
...base,
|
|
188
|
+
inactiveDictionaryOverride: true,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
const parsedBehavior = row.activityTypeBehavior
|
|
192
|
+
? calendarEventTypeBehaviorSchema.safeParse(row.activityTypeBehavior)
|
|
193
|
+
: null
|
|
194
|
+
const behavior = parsedBehavior?.success
|
|
195
|
+
? parsedBehavior.data
|
|
196
|
+
: base?.behavior ?? { ...fallbackBehavior, selectable: true }
|
|
197
|
+
const isLocalOverride = row.organizationId === input.organizationId
|
|
198
|
+
const defaultLabel = calendarEventTypes.find((entry) => entry.key === key)?.label ?? base?.label
|
|
199
|
+
const hasCustomLabel = !!row.label && row.label !== defaultLabel
|
|
200
|
+
return deepFreeze({
|
|
201
|
+
key,
|
|
202
|
+
label: hasCustomLabel ? row.label : base?.label || row.label || row.value,
|
|
203
|
+
...(!hasCustomLabel && base?.labelKey ? { labelKey: base.labelKey } : {}),
|
|
204
|
+
icon: row.icon ?? base?.icon ?? null,
|
|
205
|
+
color: row.color ?? base?.color ?? null,
|
|
206
|
+
behavior,
|
|
207
|
+
selectable: behavior.selectable,
|
|
208
|
+
...(base?.panelKey ? { panelKey: base.panelKey } : {}),
|
|
209
|
+
source: base?.source ?? 'dictionary',
|
|
210
|
+
provenance: base?.provenance ?? {},
|
|
211
|
+
historical: false,
|
|
212
|
+
adminConfigurable: true,
|
|
213
|
+
isInherited: !isLocalOverride,
|
|
214
|
+
isLocalOverride,
|
|
215
|
+
updatedAt: row.updatedAt instanceof Date ? row.updatedAt.toISOString() : null,
|
|
216
|
+
missingCustomFieldsetIds: availableFieldsets
|
|
217
|
+
? behavior.customFieldsetIds.filter((id) => !availableFieldsets.has(id))
|
|
218
|
+
: [],
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
items.sort((left, right) => left.behavior.order - right.behavior.order || left.key.localeCompare(right.key))
|
|
222
|
+
return deepFreeze({ items, fallbackKey: 'meeting' })
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function resolveCatalogEventType(
|
|
226
|
+
catalog: CalendarEventTypeCatalog,
|
|
227
|
+
key: string,
|
|
228
|
+
options: { includeHistorical?: boolean } = {},
|
|
229
|
+
): ScopedCalendarEventType | undefined {
|
|
230
|
+
const normalizedKey = key.trim().toLowerCase()
|
|
231
|
+
const item = catalog.items.find((entry) => entry.key === normalizedKey)
|
|
232
|
+
if (item || !options.includeHistorical) return item
|
|
233
|
+
const registryHistorical = resolveCalendarEventType(key, { includeHistorical: true })
|
|
234
|
+
if (registryHistorical) {
|
|
235
|
+
const serialized = serializeStaticType(registryHistorical)
|
|
236
|
+
return deepFreeze({
|
|
237
|
+
...serialized,
|
|
238
|
+
selectable: false,
|
|
239
|
+
behavior: { ...serialized.behavior, selectable: false },
|
|
240
|
+
historical: true,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
const fallback = catalog.items.find((entry) => entry.key === catalog.fallbackKey)
|
|
244
|
+
if (!fallback) return undefined
|
|
245
|
+
return deepFreeze({
|
|
246
|
+
...fallback,
|
|
247
|
+
key,
|
|
248
|
+
label: key,
|
|
249
|
+
labelKey: undefined,
|
|
250
|
+
behavior: { ...fallback.behavior, selectable: false },
|
|
251
|
+
selectable: false,
|
|
252
|
+
historical: true,
|
|
253
|
+
source: 'historical-fallback',
|
|
254
|
+
isInherited: false,
|
|
255
|
+
isLocalOverride: false,
|
|
256
|
+
updatedAt: null,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { CalendarEventTypeBehavior } from '../../calendar-event-types'
|
|
2
|
+
|
|
3
|
+
export type InteractionCoreValues = Readonly<Record<string, unknown>>
|
|
4
|
+
|
|
5
|
+
const CORE_FIELD_RULES: readonly [keyof CalendarEventTypeBehavior['fields'], readonly string[]][] = [
|
|
6
|
+
['endTime', ['durationMinutes']],
|
|
7
|
+
['allDay', ['allDay']],
|
|
8
|
+
['recurrence', ['recurrenceRule', 'recurrenceEnd']],
|
|
9
|
+
['priority', ['priority']],
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
function hasValue(value: unknown): boolean {
|
|
13
|
+
if (value === null || value === undefined || value === '' || value === false) return false
|
|
14
|
+
if (Array.isArray(value)) return value.length > 0
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hasResourceLinks(value: unknown): boolean {
|
|
19
|
+
return Array.isArray(value) && value.some((link) =>
|
|
20
|
+
link !== null && typeof link === 'object' && 'type' in link && link.type === 'resource')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function findInapplicableCoreFields(
|
|
24
|
+
behavior: CalendarEventTypeBehavior,
|
|
25
|
+
values: InteractionCoreValues,
|
|
26
|
+
): string[] {
|
|
27
|
+
const fields: string[] = []
|
|
28
|
+
for (const [rule, names] of CORE_FIELD_RULES) {
|
|
29
|
+
if (behavior.fields[rule]) continue
|
|
30
|
+
for (const name of names) if (hasValue(values[name])) fields.push(name)
|
|
31
|
+
}
|
|
32
|
+
if (behavior.fields.location === 'none' && hasValue(values.location)) fields.push('location')
|
|
33
|
+
if (behavior.fields.people === 'none' && hasValue(values.participants)) fields.push('participants')
|
|
34
|
+
if (!behavior.fields.resources && hasResourceLinks(values.linkedEntities)) fields.push('linkedEntities')
|
|
35
|
+
return fields
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function clearInapplicableCoreFields(
|
|
39
|
+
behavior: CalendarEventTypeBehavior,
|
|
40
|
+
values: InteractionCoreValues,
|
|
41
|
+
): Record<string, unknown> {
|
|
42
|
+
const cleared: Record<string, unknown> = {}
|
|
43
|
+
for (const field of findInapplicableCoreFields(behavior, values)) {
|
|
44
|
+
if (field === 'linkedEntities') {
|
|
45
|
+
cleared.linkedEntities = (values.linkedEntities as readonly Record<string, unknown>[])
|
|
46
|
+
.filter((link) => link.type !== 'resource')
|
|
47
|
+
} else {
|
|
48
|
+
cleared[field] = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return cleared
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Under the selectable-type opt-in an explicit `null` for a field the type hides
|
|
56
|
+
* means "not shown", not "clear it". Dropping it lets a type change judge the
|
|
57
|
+
* stored value, so the destructive clear is confirmed instead of silent.
|
|
58
|
+
*/
|
|
59
|
+
export function omitHiddenCoreClears(
|
|
60
|
+
behavior: CalendarEventTypeBehavior,
|
|
61
|
+
patch: InteractionCoreValues,
|
|
62
|
+
): Record<string, unknown> {
|
|
63
|
+
const result = { ...patch }
|
|
64
|
+
for (const [rule, names] of CORE_FIELD_RULES) {
|
|
65
|
+
if (behavior.fields[rule]) continue
|
|
66
|
+
for (const name of names) if (result[name] === null) delete result[name]
|
|
67
|
+
}
|
|
68
|
+
if (behavior.fields.location === 'none' && result.location === null) delete result.location
|
|
69
|
+
if (behavior.fields.people === 'none' && result.participants === null) delete result.participants
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `ignoreHiddenClears` belongs to the selectable-type opt-in: only a caller that
|
|
75
|
+
* declares `enforceSelectableType` has its explicit `null` clears of fields the
|
|
76
|
+
* type hides dropped. Retaining stored resource links is unconditional — it
|
|
77
|
+
* protects bookings a partial payload never meant to release.
|
|
78
|
+
*/
|
|
79
|
+
export function preserveHiddenCoreValuesOnSameTypeEdit(
|
|
80
|
+
behavior: CalendarEventTypeBehavior,
|
|
81
|
+
current: InteractionCoreValues,
|
|
82
|
+
patch: InteractionCoreValues,
|
|
83
|
+
options: { ignoreHiddenClears?: boolean } = { ignoreHiddenClears: true },
|
|
84
|
+
): Record<string, unknown> {
|
|
85
|
+
const result = options.ignoreHiddenClears !== false ? omitHiddenCoreClears(behavior, patch) : { ...patch }
|
|
86
|
+
if (!behavior.fields.resources && result.linkedEntities !== undefined) {
|
|
87
|
+
const existingLinks = Array.isArray(current.linkedEntities) ? current.linkedEntities : []
|
|
88
|
+
const retainedResources = existingLinks.filter((link) =>
|
|
89
|
+
link !== null && typeof link === 'object' && 'type' in link && link.type === 'resource')
|
|
90
|
+
const nextLinks = Array.isArray(result.linkedEntities) ? result.linkedEntities : []
|
|
91
|
+
const sentResourceIds = new Set(nextLinks.flatMap((link) =>
|
|
92
|
+
link !== null && typeof link === 'object' && 'type' in link && link.type === 'resource' && 'id' in link ? [link.id] : []))
|
|
93
|
+
result.linkedEntities = [
|
|
94
|
+
...nextLinks,
|
|
95
|
+
...retainedResources.filter((link) => !('id' in link) || !sentResourceIds.has(link.id)),
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
} from '../../components/calendar/types'
|
|
12
12
|
import { categoryOf } from './categories'
|
|
13
13
|
import { participantActorKey } from './participantIdentity'
|
|
14
|
+
import { calendarDayEndInstant, calendarDayStartInstant, calendarInstantToWallTime, isCalendarTimezone } from './timezone'
|
|
14
15
|
|
|
15
16
|
const DEFAULT_DURATION_MINUTES = 30
|
|
16
17
|
|
|
@@ -70,8 +71,11 @@ export function mapInteractionToCalendarItem(
|
|
|
70
71
|
|
|
71
72
|
const allDay = payload.allDay === true
|
|
72
73
|
const durationMinutes = payload.durationMinutes ?? DEFAULT_DURATION_MINUTES
|
|
73
|
-
const
|
|
74
|
-
const
|
|
74
|
+
const eventDate = allDay && isCalendarTimezone(payload.timezone) ? calendarInstantToWallTime(parsedStart, payload.timezone).date : null
|
|
75
|
+
const start = eventDate ? calendarDayStartInstant(eventDate, payload.timezone as string) : allDay ? startOfDay(parsedStart) : parsedStart
|
|
76
|
+
const end = eventDate ? calendarDayEndInstant(eventDate, payload.timezone as string) : allDay ? endOfDay(parsedStart) : addMinutes(parsedStart, durationMinutes)
|
|
77
|
+
|
|
78
|
+
if (!start || !end) return null
|
|
75
79
|
|
|
76
80
|
const location = payload.location ?? null
|
|
77
81
|
const platform = detectPlatform(location)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { addDays } from 'date-fns/addDays'
|
|
2
|
+
import { calendarDayEndInstant, calendarDayStartInstant, calendarInstantToWallTime, calendarRecurrenceWallTimeToInstant, isCalendarTimezone } from './timezone'
|
|
2
3
|
import type { CalendarItem, CalendarRange } from '../../components/calendar/types'
|
|
3
4
|
|
|
4
5
|
const MAX_OCCURRENCES_PER_WINDOW = 100
|
|
6
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
5
7
|
|
|
6
8
|
const WEEKDAY_TOKENS = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'] as const
|
|
7
9
|
|
|
@@ -92,15 +94,70 @@ function parseRecurrenceEndTime(rawRecurrenceEnd: string | null | undefined): nu
|
|
|
92
94
|
|
|
93
95
|
function occursOn(date: Date, rule: ParsedRecurrenceRule, seriesStartWeekday: number): boolean {
|
|
94
96
|
if (rule.freq === 'DAILY') return true
|
|
95
|
-
const allowedWeekdays = rule.byDay ?? [seriesStartWeekday]
|
|
97
|
+
const allowedWeekdays = [...new Set(rule.byDay ?? [seriesStartWeekday])]
|
|
96
98
|
return allowedWeekdays.includes(date.getDay())
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
function countMatchingDays(startDay: number, endDay: number, rule: ParsedRecurrenceRule, seriesStartWeekday: number): number {
|
|
102
|
+
const dayCount = Math.max(0, endDay - startDay)
|
|
103
|
+
if (rule.freq === 'DAILY') return dayCount
|
|
104
|
+
const allowedWeekdays = [...new Set(rule.byDay ?? [seriesStartWeekday])]
|
|
105
|
+
const fullWeeks = Math.floor(dayCount / 7)
|
|
106
|
+
let matches = fullWeeks * allowedWeekdays.length
|
|
107
|
+
const remainder = dayCount % 7
|
|
108
|
+
const startWeekday = new Date(startDay * DAY_MS).getUTCDay()
|
|
109
|
+
for (let offset = 0; offset < remainder; offset += 1) {
|
|
110
|
+
if (allowedWeekdays.includes((startWeekday + offset) % 7)) matches += 1
|
|
111
|
+
}
|
|
112
|
+
return matches
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function utcDay(date: Date): number {
|
|
116
|
+
return Math.floor(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / DAY_MS)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function expandZonedOccurrences(item: CalendarItem, range: CalendarRange, rule: ParsedRecurrenceRule, timezone: string): CalendarItem[] {
|
|
120
|
+
const startWall = calendarInstantToWallTime(item.start, timezone)
|
|
121
|
+
const cursor = new Date(`${startWall.date}T00:00:00Z`)
|
|
122
|
+
const seriesStartDay = Math.floor(cursor.getTime() / DAY_MS)
|
|
123
|
+
const earliestRelevant = new Date(range.from.getTime() - Math.max(0, item.end.getTime() - item.start.getTime()))
|
|
124
|
+
const earliestWallDate = calendarInstantToWallTime(earliestRelevant, timezone).date
|
|
125
|
+
const earliestDay = Math.floor(new Date(`${earliestWallDate}T00:00:00Z`).getTime() / DAY_MS)
|
|
126
|
+
const finalDate = calendarInstantToWallTime(range.to, timezone).date
|
|
127
|
+
const seriesWeekday = cursor.getUTCDay()
|
|
128
|
+
const duration = item.end.getTime() - item.start.getTime()
|
|
129
|
+
const recurrenceEnd = typeof item.raw.recurrenceEnd === 'string' ? new Date(item.raw.recurrenceEnd).getTime() : null
|
|
130
|
+
const occurrences: CalendarItem[] = []
|
|
131
|
+
let occurrenceIndex = 0
|
|
132
|
+
if (earliestDay > seriesStartDay) {
|
|
133
|
+
occurrenceIndex = countMatchingDays(seriesStartDay, earliestDay, rule, seriesWeekday)
|
|
134
|
+
cursor.setUTCDate(cursor.getUTCDate() + earliestDay - seriesStartDay)
|
|
135
|
+
}
|
|
136
|
+
while (cursor.toISOString().slice(0, 10) <= finalDate) {
|
|
137
|
+
const date = cursor.toISOString().slice(0, 10)
|
|
138
|
+
const start = item.allDay ? calendarDayStartInstant(date, timezone) : calendarRecurrenceWallTimeToInstant(date, startWall.time, timezone)
|
|
139
|
+
if (start && ((rule.until && start > rule.until) || (recurrenceEnd !== null && start.getTime() > recurrenceEnd))) break
|
|
140
|
+
const matches = rule.freq === 'DAILY' || (rule.byDay ?? [seriesWeekday]).includes(cursor.getUTCDay())
|
|
141
|
+
if (start && matches) {
|
|
142
|
+
if (rule.count !== null && occurrenceIndex >= rule.count) break
|
|
143
|
+
const end = item.allDay ? calendarDayEndInstant(date, timezone) : new Date(start.getTime() + duration)
|
|
144
|
+
if (end && start <= range.to && end > range.from) {
|
|
145
|
+
occurrences.push({ ...item, id: `${item.id}:${occurrenceIndex}`, start, end, isRecurringOccurrence: true })
|
|
146
|
+
if (occurrences.length >= MAX_OCCURRENCES_PER_WINDOW) break
|
|
147
|
+
}
|
|
148
|
+
occurrenceIndex += 1
|
|
149
|
+
}
|
|
150
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1)
|
|
151
|
+
}
|
|
152
|
+
return occurrences
|
|
153
|
+
}
|
|
154
|
+
|
|
99
155
|
export function expandOccurrences(item: CalendarItem, range: CalendarRange): CalendarItem[] {
|
|
100
156
|
const rawRule = item.raw.recurrenceRule
|
|
101
157
|
if (typeof rawRule !== 'string' || rawRule.trim().length === 0) return [item]
|
|
102
158
|
const rule = parseRecurrenceRule(rawRule)
|
|
103
159
|
if (!rule) return [item]
|
|
160
|
+
if (isCalendarTimezone(item.raw.timezone)) return expandZonedOccurrences(item, range, rule, item.raw.timezone)
|
|
104
161
|
|
|
105
162
|
const durationMs = item.end.getTime() - item.start.getTime()
|
|
106
163
|
const recurrenceEndTime = parseRecurrenceEndTime(item.raw.recurrenceEnd)
|
|
@@ -110,6 +167,13 @@ export function expandOccurrences(item: CalendarItem, range: CalendarRange): Cal
|
|
|
110
167
|
const occurrences: CalendarItem[] = []
|
|
111
168
|
let occurrenceIndex = 0
|
|
112
169
|
let cursor = new Date(item.start)
|
|
170
|
+
const seriesStartDay = utcDay(cursor)
|
|
171
|
+
const earliestRelevant = new Date(range.from.getTime() - Math.max(0, durationMs))
|
|
172
|
+
const earliestDay = utcDay(earliestRelevant)
|
|
173
|
+
if (earliestDay > seriesStartDay) {
|
|
174
|
+
occurrenceIndex = countMatchingDays(seriesStartDay, earliestDay, rule, seriesStartWeekday)
|
|
175
|
+
cursor = addDays(cursor, earliestDay - seriesStartDay)
|
|
176
|
+
}
|
|
113
177
|
|
|
114
178
|
while (cursor.getTime() <= range.to.getTime()) {
|
|
115
179
|
if (untilTime !== null && cursor.getTime() > untilTime) break
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const formatters = new Map<string, Intl.DateTimeFormat>()
|
|
2
|
+
|
|
3
|
+
function parseWallTime(date: string, time: string): Date | null {
|
|
4
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !/^\d{2}:\d{2}$/.test(time)) return null
|
|
5
|
+
const wall = new Date(`${date}T${time}:00Z`)
|
|
6
|
+
if (!Number.isFinite(wall.getTime()) || wall.toISOString().slice(0, 16) !== `${date}T${time}`) return null
|
|
7
|
+
return wall
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function calendarTimezoneOffsets(wall: Date, timezone: string): number[] {
|
|
11
|
+
const offsets = new Set<number>()
|
|
12
|
+
for (const hour of [-36, -12, 0, 12, 36]) {
|
|
13
|
+
const sample = new Date(wall.getTime() + hour * 3600000)
|
|
14
|
+
const local = calendarInstantToWallTime(sample, timezone)
|
|
15
|
+
offsets.add(new Date(`${local.date}T${local.time}:00Z`).getTime() - sample.getTime())
|
|
16
|
+
}
|
|
17
|
+
return [...offsets]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isCalendarTimezone(value: unknown): value is string {
|
|
21
|
+
if (typeof value !== 'string' || !value.trim() || value.length > 120 || /^[+-]/.test(value)) return false
|
|
22
|
+
try {
|
|
23
|
+
formatter(value).format()
|
|
24
|
+
return true
|
|
25
|
+
} catch {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* IANA zone ids are case-insensitive, so `europe/warsaw` and `Europe/Warsaw` are
|
|
32
|
+
* the same zone and would otherwise be stored — and listed — as two options.
|
|
33
|
+
* Canonicalize on write so the stored value matches the catalog spelling.
|
|
34
|
+
*/
|
|
35
|
+
export function canonicalCalendarTimezone(value: unknown): string | null {
|
|
36
|
+
if (!isCalendarTimezone(value)) return null
|
|
37
|
+
const resolved = formatter(value).resolvedOptions().timeZone
|
|
38
|
+
return isCalendarTimezone(resolved) ? resolved : value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function defaultCalendarTimezone(): string {
|
|
42
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
43
|
+
return isCalendarTimezone(timezone) ? timezone : 'UTC'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatter(timezone: string): Intl.DateTimeFormat {
|
|
47
|
+
let result = formatters.get(timezone)
|
|
48
|
+
if (!result) {
|
|
49
|
+
result = new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' })
|
|
50
|
+
if (formatters.size >= 64) formatters.delete(formatters.keys().next().value!)
|
|
51
|
+
formatters.set(timezone, result)
|
|
52
|
+
}
|
|
53
|
+
return result
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function calendarInstantToWallTime(instant: Date, timezone: string): { date: string; time: string } {
|
|
57
|
+
const parts = formatter(timezone).formatToParts(instant)
|
|
58
|
+
const part = (name: string) => parts.find((entry) => entry.type === name)!.value
|
|
59
|
+
return { date: `${part('year')}-${part('month')}-${part('day')}`, time: `${part('hour')}:${part('minute')}` }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function calendarWallTimeToInstant(date: string, time: string, timezone: string): Date | null {
|
|
63
|
+
if (!isCalendarTimezone(timezone)) return null
|
|
64
|
+
const wall = parseWallTime(date, time)
|
|
65
|
+
if (!wall) return null
|
|
66
|
+
const candidates = calendarTimezoneOffsets(wall, timezone).map((offset) => new Date(wall.getTime() - offset))
|
|
67
|
+
.filter((instant) => {
|
|
68
|
+
const local = calendarInstantToWallTime(instant, timezone)
|
|
69
|
+
return local.date === date && local.time === time
|
|
70
|
+
}).sort((left, right) => left.getTime() - right.getTime())
|
|
71
|
+
return candidates[0] ?? null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function calendarRecurrenceWallTimeToInstant(date: string, time: string, timezone: string): Date | null {
|
|
75
|
+
const exact = calendarWallTimeToInstant(date, time, timezone)
|
|
76
|
+
if (exact) return exact
|
|
77
|
+
if (!isCalendarTimezone(timezone) || !calendarDayStartInstant(date, timezone)) return null
|
|
78
|
+
const wall = parseWallTime(date, time)
|
|
79
|
+
if (!wall) return null
|
|
80
|
+
const offsets = calendarTimezoneOffsets(wall, timezone)
|
|
81
|
+
if (offsets.length < 2) return null
|
|
82
|
+
return new Date(wall.getTime() - Math.min(...offsets))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function calendarDayStartInstant(date: string, timezone: string): Date | null {
|
|
86
|
+
if (!isCalendarTimezone(timezone) || !/^\d{4}-\d{2}-\d{2}$/.test(date)) return null
|
|
87
|
+
const anchor = new Date(`${date}T00:00:00Z`)
|
|
88
|
+
if (!Number.isFinite(anchor.getTime()) || anchor.toISOString().slice(0, 10) !== date) return null
|
|
89
|
+
const midnight = calendarWallTimeToInstant(date, '00:00', timezone)
|
|
90
|
+
if (midnight) return midnight
|
|
91
|
+
let lower = anchor.getTime() - 36 * 3600000
|
|
92
|
+
let upper = anchor.getTime() + 36 * 3600000
|
|
93
|
+
while (lower < upper) {
|
|
94
|
+
const middle = lower + Math.floor((upper - lower) / 2)
|
|
95
|
+
if (calendarInstantToWallTime(new Date(middle), timezone).date < date) lower = middle + 1
|
|
96
|
+
else upper = middle
|
|
97
|
+
}
|
|
98
|
+
const boundary = new Date(lower)
|
|
99
|
+
return calendarInstantToWallTime(boundary, timezone).date === date ? boundary : null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function calendarDayEndInstant(date: string, timezone: string): Date | null {
|
|
103
|
+
if (!calendarDayStartInstant(date, timezone)) return null
|
|
104
|
+
const nextDate = new Date(`${date}T00:00:00Z`)
|
|
105
|
+
for (let skippedDays = 0; skippedDays < 3; skippedDays += 1) {
|
|
106
|
+
nextDate.setUTCDate(nextDate.getUTCDate() + 1)
|
|
107
|
+
const nextBoundary = calendarDayStartInstant(nextDate.toISOString().slice(0, 10), timezone)
|
|
108
|
+
if (nextBoundary) return new Date(nextBoundary.getTime() - 1)
|
|
109
|
+
}
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function calendarTimezoneOptions(storedTimezone?: string | null): string[] {
|
|
114
|
+
const intl = Intl as typeof Intl & { supportedValuesOf?: (key: 'timeZone') => string[] }
|
|
115
|
+
const stored = isCalendarTimezone(storedTimezone) ? [storedTimezone] : []
|
|
116
|
+
return [...new Set(['UTC', defaultCalendarTimezone(), ...stored, ...(intl.supportedValuesOf?.('timeZone') ?? [])])]
|
|
117
|
+
}
|
|
@@ -189,6 +189,7 @@ export async function hydrateCanonicalInteractions({
|
|
|
189
189
|
appearanceColor: interaction.appearanceColor ?? null,
|
|
190
190
|
source: interaction.source ?? null,
|
|
191
191
|
duration: interaction.durationMinutes ?? null,
|
|
192
|
+
timezone: interaction.timezone ?? null,
|
|
192
193
|
location: interaction.location ?? null,
|
|
193
194
|
allDay: interaction.allDay ?? null,
|
|
194
195
|
recurrenceRule: interaction.recurrenceRule ?? null,
|
|
@@ -28,6 +28,7 @@ export type CustomerTodoRow = {
|
|
|
28
28
|
todoSeverity?: string | null
|
|
29
29
|
todoDescription?: string | null
|
|
30
30
|
todoDueAt?: string | null
|
|
31
|
+
todoTimezone?: string | null
|
|
31
32
|
todoCustomValues?: Record<string, unknown> | null
|
|
32
33
|
todoOrganizationId: string | null
|
|
33
34
|
todoUpdatedAt?: string | null
|
|
@@ -640,6 +641,7 @@ export function mapInteractionRecordToTodoRow(
|
|
|
640
641
|
: null,
|
|
641
642
|
todoDescription: interaction.body ?? null,
|
|
642
643
|
todoDueAt: interaction.scheduledAt ?? null,
|
|
644
|
+
todoTimezone: interaction.timezone ?? null,
|
|
643
645
|
todoCustomValues: Object.keys(customValues).length > 0 ? customValues : null,
|
|
644
646
|
todoOrganizationId: interaction.organizationId ?? null,
|
|
645
647
|
todoUpdatedAt: interaction.updatedAt ?? null,
|
|
@@ -2528,6 +2528,23 @@
|
|
|
2528
2528
|
"name": "customer_dictionary_entries",
|
|
2529
2529
|
"schema": "public",
|
|
2530
2530
|
"columns": {
|
|
2531
|
+
"activity_type_behavior": {
|
|
2532
|
+
"name": "activity_type_behavior",
|
|
2533
|
+
"type": "jsonb",
|
|
2534
|
+
"unsigned": false,
|
|
2535
|
+
"autoincrement": false,
|
|
2536
|
+
"primary": false,
|
|
2537
|
+
"nullable": true,
|
|
2538
|
+
"unique": false,
|
|
2539
|
+
"length": null,
|
|
2540
|
+
"precision": null,
|
|
2541
|
+
"scale": null,
|
|
2542
|
+
"default": null,
|
|
2543
|
+
"comment": null,
|
|
2544
|
+
"collation": null,
|
|
2545
|
+
"enumItems": [],
|
|
2546
|
+
"mappedType": "json"
|
|
2547
|
+
},
|
|
2531
2548
|
"color": {
|
|
2532
2549
|
"name": "color",
|
|
2533
2550
|
"type": "text",
|
|
@@ -4435,6 +4452,23 @@
|
|
|
4435
4452
|
"collation": null,
|
|
4436
4453
|
"enumItems": [],
|
|
4437
4454
|
"mappedType": "text"
|
|
4455
|
+
},
|
|
4456
|
+
"timezone": {
|
|
4457
|
+
"name": "timezone",
|
|
4458
|
+
"type": "text",
|
|
4459
|
+
"unsigned": false,
|
|
4460
|
+
"autoincrement": false,
|
|
4461
|
+
"primary": false,
|
|
4462
|
+
"nullable": true,
|
|
4463
|
+
"unique": false,
|
|
4464
|
+
"length": null,
|
|
4465
|
+
"precision": null,
|
|
4466
|
+
"scale": null,
|
|
4467
|
+
"default": null,
|
|
4468
|
+
"comment": null,
|
|
4469
|
+
"collation": null,
|
|
4470
|
+
"enumItems": [],
|
|
4471
|
+
"mappedType": "text"
|
|
4438
4472
|
}
|
|
4439
4473
|
},
|
|
4440
4474
|
"indexes": [
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260928175058_customers extends Migration {
|
|
4
|
+
|
|
5
|
+
override name = 'Migration20260928175058_customers';
|
|
6
|
+
|
|
7
|
+
override up(): void | Promise<void> {
|
|
8
|
+
this.addSql(`alter table "customer_dictionary_entries" add "activity_type_behavior" jsonb null;`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
override down(): void | Promise<void> {
|
|
12
|
+
this.addSql(`alter table "customer_dictionary_entries" drop column "activity_type_behavior";`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
}
|