@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,674 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
|
|
3
|
+
|
|
4
|
+
export const calendarEventBaseKindSchema = z.enum(['meeting', 'call', 'email', 'note', 'event', 'task'])
|
|
5
|
+
|
|
6
|
+
export const calendarEventTypeFieldsSchema = z.strictObject({
|
|
7
|
+
endTime: z.boolean(),
|
|
8
|
+
allDay: z.boolean(),
|
|
9
|
+
recurrence: z.boolean(),
|
|
10
|
+
location: z.enum(['none', 'location', 'phoneLink']),
|
|
11
|
+
people: z.enum(['none', 'attendees', 'participants', 'recipients', 'assignee']),
|
|
12
|
+
priority: z.boolean(),
|
|
13
|
+
resources: z.boolean(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export const calendarEventTypeBehaviorSchema = z.strictObject({
|
|
17
|
+
schemaVersion: z.literal(1),
|
|
18
|
+
baseKind: calendarEventBaseKindSchema,
|
|
19
|
+
selectable: z.boolean(),
|
|
20
|
+
order: z.number().int().min(0).max(10_000),
|
|
21
|
+
fields: calendarEventTypeFieldsSchema,
|
|
22
|
+
customFieldsetIds: z.array(z.string().trim().min(1).max(100)).max(32).refine(
|
|
23
|
+
(values) => new Set(values).size === values.length,
|
|
24
|
+
'Custom fieldset ids must be unique',
|
|
25
|
+
),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const calendarEventTypeKeySchema = z.string().trim().min(1).max(100).regex(
|
|
29
|
+
/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/,
|
|
30
|
+
'Calendar event type keys must be lowercase dictionary keys',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
export const calendarEventTypeDefinitionSchema = z.strictObject({
|
|
34
|
+
key: calendarEventTypeKeySchema,
|
|
35
|
+
label: z.string().trim().min(1).max(150),
|
|
36
|
+
labelKey: z.string().trim().min(1).max(200).optional(),
|
|
37
|
+
icon: z.string().trim().max(100).nullable().optional(),
|
|
38
|
+
color: z.string().trim().max(100).nullable().optional(),
|
|
39
|
+
behavior: calendarEventTypeBehaviorSchema,
|
|
40
|
+
adminConfigurable: z.boolean().optional(),
|
|
41
|
+
panelKey: z.string().trim().min(1).max(150).optional(),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export const calendarEventTypeBehaviorOverrideSchema = z.strictObject({
|
|
45
|
+
schemaVersion: z.literal(1).optional(),
|
|
46
|
+
baseKind: calendarEventBaseKindSchema.optional(),
|
|
47
|
+
selectable: z.boolean().optional(),
|
|
48
|
+
order: z.number().int().min(0).max(10_000).optional(),
|
|
49
|
+
fields: calendarEventTypeFieldsSchema.partial().strict().optional(),
|
|
50
|
+
customFieldsetIds: z.array(z.string().trim().min(1).max(100)).max(32).refine(
|
|
51
|
+
(values) => new Set(values).size === values.length,
|
|
52
|
+
'Custom fieldset ids must be unique',
|
|
53
|
+
).optional(),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export const calendarEventTypeDefinitionOverrideSchema = z.strictObject({
|
|
57
|
+
label: z.string().trim().min(1).max(150).optional(),
|
|
58
|
+
labelKey: z.string().trim().min(1).max(200).optional(),
|
|
59
|
+
icon: z.string().trim().max(100).nullable().optional(),
|
|
60
|
+
color: z.string().trim().max(100).nullable().optional(),
|
|
61
|
+
behavior: calendarEventTypeBehaviorOverrideSchema.optional(),
|
|
62
|
+
adminConfigurable: z.boolean().optional(),
|
|
63
|
+
panelKey: z.string().trim().min(1).max(150).optional(),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const calendarEventTypePatchSchema = z.strictObject({
|
|
67
|
+
targetEventTypeKey: calendarEventTypeKeySchema,
|
|
68
|
+
replaceLabel: z.string().trim().min(1).max(150).optional(),
|
|
69
|
+
replaceLabelKey: z.string().trim().min(1).max(200).nullable().optional(),
|
|
70
|
+
replaceIcon: z.string().trim().max(100).nullable().optional(),
|
|
71
|
+
replaceColor: z.string().trim().max(100).nullable().optional(),
|
|
72
|
+
replaceBaseKind: calendarEventBaseKindSchema.optional(),
|
|
73
|
+
replaceSelectable: z.boolean().optional(),
|
|
74
|
+
replaceOrder: z.number().int().min(0).max(10_000).optional(),
|
|
75
|
+
replaceFields: calendarEventTypeFieldsSchema.partial().strict().optional(),
|
|
76
|
+
replaceAdminConfigurable: z.boolean().optional(),
|
|
77
|
+
replacePanelKey: z.string().trim().min(1).max(150).nullable().optional(),
|
|
78
|
+
replaceCustomFieldsetIds: calendarEventTypeBehaviorSchema.shape.customFieldsetIds.optional(),
|
|
79
|
+
deleteCustomFieldsetIds: calendarEventTypeBehaviorSchema.shape.customFieldsetIds.optional(),
|
|
80
|
+
appendCustomFieldsetIds: calendarEventTypeBehaviorSchema.shape.customFieldsetIds.optional(),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
export const calendarEventTypeProvenanceSchema = z.strictObject({
|
|
84
|
+
moduleId: z.string().trim().min(1).max(100),
|
|
85
|
+
sourcePath: z.string().trim().min(1).max(500),
|
|
86
|
+
moduleOrder: z.number().int().min(0),
|
|
87
|
+
phase: z.enum(['core', 'widget', 'module', 'programmatic', 'fallback']),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
export const calendarEventTypeTombstoneSchema = z.strictObject({
|
|
91
|
+
key: calendarEventTypeKeySchema,
|
|
92
|
+
provenance: calendarEventTypeProvenanceSchema,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
export const calendarEventTypeHistoricalFallbackSchema = z.strictObject({
|
|
96
|
+
key: calendarEventTypeKeySchema,
|
|
97
|
+
reason: z.enum(['tombstoned', 'module-unavailable', 'unknown']),
|
|
98
|
+
definition: calendarEventTypeDefinitionSchema,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export type CalendarEventTypeWidget = InjectionWidgetModule & {
|
|
102
|
+
eventTypes: readonly CalendarEventTypeDefinition[]
|
|
103
|
+
eventTypeOverrides?: Readonly<Record<string, CalendarEventTypeDefinition | null>>
|
|
104
|
+
eventTypePatches?: readonly CalendarEventTypePatch[]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type CalendarEventBaseKind = z.infer<typeof calendarEventBaseKindSchema>
|
|
108
|
+
export type CalendarEventTypeFields = z.infer<typeof calendarEventTypeFieldsSchema>
|
|
109
|
+
export type CalendarEventTypeBehavior = z.infer<typeof calendarEventTypeBehaviorSchema>
|
|
110
|
+
export type CalendarEventTypeDefinition = z.infer<typeof calendarEventTypeDefinitionSchema>
|
|
111
|
+
export type CalendarEventTypeBehaviorOverride = z.infer<typeof calendarEventTypeBehaviorOverrideSchema>
|
|
112
|
+
export type CalendarEventTypeDefinitionOverride = z.infer<typeof calendarEventTypeDefinitionOverrideSchema>
|
|
113
|
+
export type CalendarEventTypePatch = z.infer<typeof calendarEventTypePatchSchema>
|
|
114
|
+
export type CalendarEventTypeProvenance = z.infer<typeof calendarEventTypeProvenanceSchema>
|
|
115
|
+
export type CalendarEventTypeTombstone = z.infer<typeof calendarEventTypeTombstoneSchema>
|
|
116
|
+
export type CalendarEventTypeHistoricalFallback = z.infer<typeof calendarEventTypeHistoricalFallbackSchema>
|
|
117
|
+
|
|
118
|
+
export type EffectiveCalendarEventType = Readonly<CalendarEventTypeDefinition & {
|
|
119
|
+
provenance: Readonly<Record<string, CalendarEventTypeProvenance>>
|
|
120
|
+
historical: boolean
|
|
121
|
+
fallbackReason?: CalendarEventTypeHistoricalFallback['reason']
|
|
122
|
+
}>
|
|
123
|
+
|
|
124
|
+
export type CalendarEventPanelCapabilities = Readonly<{
|
|
125
|
+
resourcesEnabled: boolean
|
|
126
|
+
staffEnabled: boolean
|
|
127
|
+
}>
|
|
128
|
+
|
|
129
|
+
export type CalendarEventTypePanelProps = {
|
|
130
|
+
definition: EffectiveCalendarEventType
|
|
131
|
+
panelKey?: string
|
|
132
|
+
mode: 'create' | 'edit'
|
|
133
|
+
values: Readonly<Record<string, unknown>>
|
|
134
|
+
errors: Readonly<Record<string, string | undefined>>
|
|
135
|
+
disabled: boolean
|
|
136
|
+
capabilities: CalendarEventPanelCapabilities
|
|
137
|
+
setValue: (fieldId: string, value: unknown) => void
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type CalendarEventTypeDiagnostic = Readonly<{
|
|
141
|
+
code: 'invalid-source' | 'duplicate-definition' | 'unknown-override' | 'unknown-patch'
|
|
142
|
+
key?: string
|
|
143
|
+
moduleId: string
|
|
144
|
+
widgetId?: string
|
|
145
|
+
owner?: string
|
|
146
|
+
}>
|
|
147
|
+
|
|
148
|
+
const behavior = (
|
|
149
|
+
baseKind: CalendarEventBaseKind,
|
|
150
|
+
order: number,
|
|
151
|
+
fields: CalendarEventTypeFields,
|
|
152
|
+
): CalendarEventTypeBehavior => ({
|
|
153
|
+
schemaVersion: 1,
|
|
154
|
+
baseKind,
|
|
155
|
+
selectable: true,
|
|
156
|
+
order,
|
|
157
|
+
fields,
|
|
158
|
+
customFieldsetIds: [],
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const resources = true
|
|
162
|
+
|
|
163
|
+
export const calendarEventTypes: readonly CalendarEventTypeDefinition[] = deepFreeze([
|
|
164
|
+
{ key: 'meeting', label: 'Meeting', labelKey: 'customers.calendar.editor.types.meeting', icon: 'lucide:users', color: '#f59e0b', behavior: behavior('meeting', 0, { endTime: true, allDay: true, recurrence: true, location: 'location', people: 'attendees', priority: false, resources }) },
|
|
165
|
+
{ key: 'call', label: 'Call', labelKey: 'customers.calendar.editor.types.call', icon: 'lucide:phone-call', color: '#2563eb', behavior: behavior('call', 100, { endTime: true, allDay: true, recurrence: true, location: 'phoneLink', people: 'participants', priority: false, resources }) },
|
|
166
|
+
{ key: 'email', label: 'Email', labelKey: 'customers.calendar.editor.types.email', icon: 'lucide:mail', color: '#16a34a', behavior: behavior('email', 200, { endTime: false, allDay: true, recurrence: true, location: 'none', people: 'recipients', priority: false, resources }) },
|
|
167
|
+
{ key: 'note', label: 'Note', labelKey: 'customers.calendar.editor.types.note', icon: 'lucide:notebook', color: '#a855f7', behavior: behavior('note', 300, { endTime: false, allDay: false, recurrence: false, location: 'none', people: 'none', priority: false, resources }) },
|
|
168
|
+
{ key: 'event', label: 'Event', labelKey: 'customers.calendar.editor.types.event', icon: 'lucide:calendar', color: '#6366f1', behavior: behavior('event', 400, { endTime: true, allDay: true, recurrence: true, location: 'location', people: 'attendees', priority: false, resources }) },
|
|
169
|
+
{ key: 'task', label: 'Task', labelKey: 'customers.calendar.editor.types.task', icon: 'lucide:check-square', color: '#ef4444', behavior: behavior('task', 500, { endTime: true, allDay: true, recurrence: true, location: 'none', people: 'assignee', priority: true, resources }) },
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
export const calendarEventTypeOverrides: Readonly<Record<string, CalendarEventTypeDefinitionOverride | null>> = Object.freeze({})
|
|
173
|
+
|
|
174
|
+
export type CalendarEventTypeWidgetContribution = Readonly<{
|
|
175
|
+
moduleId: string
|
|
176
|
+
widgetId: string
|
|
177
|
+
priority?: number
|
|
178
|
+
definitions?: unknown
|
|
179
|
+
overrides?: unknown
|
|
180
|
+
patches?: unknown
|
|
181
|
+
}>
|
|
182
|
+
|
|
183
|
+
export type CalendarOverrides = Readonly<{
|
|
184
|
+
eventTypes?: Readonly<Record<string, CalendarEventTypeDefinition | null>>
|
|
185
|
+
patches?: readonly CalendarEventTypePatch[]
|
|
186
|
+
}>
|
|
187
|
+
|
|
188
|
+
export type CalendarModuleOverrideEntry = Readonly<{
|
|
189
|
+
moduleId: string
|
|
190
|
+
overrides: CalendarOverrides
|
|
191
|
+
}>
|
|
192
|
+
|
|
193
|
+
type ProgrammaticSource = {
|
|
194
|
+
definitions: Map<string, CalendarEventTypeDefinition>
|
|
195
|
+
overrides: Map<string, CalendarEventTypeDefinition | null>
|
|
196
|
+
patches: Map<string, CalendarEventTypePatch>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type RegistryState = {
|
|
200
|
+
widgets: readonly ParsedWidgetContribution[]
|
|
201
|
+
modules: readonly CalendarModuleOverrideEntry[]
|
|
202
|
+
sources: Map<string, ProgrammaticSource>
|
|
203
|
+
historicalDefinitions: Map<string, CalendarEventTypeDefinition>
|
|
204
|
+
version: number
|
|
205
|
+
diagnostics: readonly CalendarEventTypeDiagnostic[]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const REGISTRY_KEY = Symbol.for('open-mercato.customers.calendar-event-types.v1')
|
|
209
|
+
|
|
210
|
+
function getRegistryState(): RegistryState {
|
|
211
|
+
const globalRegistry = globalThis as typeof globalThis & { [REGISTRY_KEY]?: RegistryState }
|
|
212
|
+
globalRegistry[REGISTRY_KEY] ??= {
|
|
213
|
+
widgets: Object.freeze([]),
|
|
214
|
+
modules: Object.freeze([]),
|
|
215
|
+
sources: new Map(),
|
|
216
|
+
historicalDefinitions: new Map(),
|
|
217
|
+
version: 0,
|
|
218
|
+
diagnostics: Object.freeze([]),
|
|
219
|
+
}
|
|
220
|
+
return globalRegistry[REGISTRY_KEY]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function deepFreeze<T>(value: T): T {
|
|
224
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
225
|
+
Object.freeze(value)
|
|
226
|
+
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child)
|
|
227
|
+
}
|
|
228
|
+
return value
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function sourceOf(moduleId: string, sourcePath: string, moduleOrder: number, phase: CalendarEventTypeProvenance['phase']): CalendarEventTypeProvenance {
|
|
232
|
+
return Object.freeze({ moduleId, sourcePath, moduleOrder, phase })
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function allPropertyPaths(definition: CalendarEventTypeDefinition): string[] {
|
|
236
|
+
const paths = ['key', 'label', 'behavior.schemaVersion', 'behavior.baseKind', 'behavior.selectable', 'behavior.order']
|
|
237
|
+
if (definition.labelKey !== undefined) paths.push('labelKey')
|
|
238
|
+
if (definition.icon !== undefined) paths.push('icon')
|
|
239
|
+
if (definition.color !== undefined) paths.push('color')
|
|
240
|
+
if (definition.adminConfigurable !== undefined) paths.push('adminConfigurable')
|
|
241
|
+
if (definition.panelKey !== undefined) paths.push('panelKey')
|
|
242
|
+
for (const key of Object.keys(definition.behavior.fields)) paths.push(`behavior.fields.${key}`)
|
|
243
|
+
paths.push('behavior.customFieldsetIds')
|
|
244
|
+
return paths
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const widgetContributionSchema = z.strictObject({
|
|
248
|
+
moduleId: z.string().trim().min(1).max(100),
|
|
249
|
+
widgetId: z.string().trim().min(1).max(200),
|
|
250
|
+
priority: z.number().int().optional(),
|
|
251
|
+
definitions: z.array(calendarEventTypeDefinitionSchema).optional(),
|
|
252
|
+
overrides: z.record(calendarEventTypeKeySchema, calendarEventTypeDefinitionSchema.nullable()).optional(),
|
|
253
|
+
patches: z.array(calendarEventTypePatchSchema).optional(),
|
|
254
|
+
}).superRefine((entry, context) => {
|
|
255
|
+
const keys = new Set<string>()
|
|
256
|
+
for (const definition of entry.definitions ?? []) {
|
|
257
|
+
if (keys.has(definition.key)) {
|
|
258
|
+
context.addIssue({ code: 'custom', message: 'Duplicate source definition', path: ['definitions'] })
|
|
259
|
+
}
|
|
260
|
+
keys.add(definition.key)
|
|
261
|
+
}
|
|
262
|
+
for (const [key, definition] of Object.entries(entry.overrides ?? {})) {
|
|
263
|
+
if (definition !== null && definition.key !== key) {
|
|
264
|
+
context.addIssue({ code: 'custom', message: 'Replacement key must match target key', path: ['overrides', key] })
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
type ParsedWidgetContribution = z.infer<typeof widgetContributionSchema>
|
|
270
|
+
|
|
271
|
+
const moduleOverrideEntrySchema = z.strictObject({
|
|
272
|
+
moduleId: z.string().trim().min(1).max(100),
|
|
273
|
+
overrides: z.strictObject({
|
|
274
|
+
eventTypes: z.record(calendarEventTypeKeySchema, calendarEventTypeDefinitionSchema.nullable()).optional(),
|
|
275
|
+
patches: z.array(calendarEventTypePatchSchema).optional(),
|
|
276
|
+
}),
|
|
277
|
+
}).superRefine((entry, context) => {
|
|
278
|
+
for (const [key, definition] of Object.entries(entry.overrides.eventTypes ?? {})) {
|
|
279
|
+
if (definition !== null && definition.key !== key) {
|
|
280
|
+
context.addIssue({ code: 'custom', message: 'Replacement key must match target key', path: ['overrides', 'eventTypes', key] })
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
function copyDefinition(definition: CalendarEventTypeDefinition): CalendarEventTypeDefinition {
|
|
286
|
+
return calendarEventTypeDefinitionSchema.parse(definition)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function copyPatch(patch: CalendarEventTypePatch): CalendarEventTypePatch {
|
|
290
|
+
return calendarEventTypePatchSchema.parse(patch)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function programmaticSource(sourceId: string): ProgrammaticSource {
|
|
294
|
+
return getRegistryState().sources.get(sourceId) ?? {
|
|
295
|
+
definitions: new Map(),
|
|
296
|
+
overrides: new Map(),
|
|
297
|
+
patches: new Map(),
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function replaceProgrammaticSource(sourceId: string, source: ProgrammaticSource): void {
|
|
302
|
+
const state = getRegistryState()
|
|
303
|
+
archiveCurrentDefinitions()
|
|
304
|
+
state.sources.set(sourceId, source)
|
|
305
|
+
state.version += 1
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function markDefinitionProvenance(
|
|
309
|
+
definition: CalendarEventTypeDefinition,
|
|
310
|
+
source: CalendarEventTypeProvenance,
|
|
311
|
+
): Record<string, CalendarEventTypeProvenance> {
|
|
312
|
+
return Object.fromEntries(allPropertyPaths(definition).map((path) => [path, source]))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function applyPatch(
|
|
316
|
+
definition: CalendarEventTypeDefinition,
|
|
317
|
+
patch: CalendarEventTypePatch,
|
|
318
|
+
provenance: Record<string, CalendarEventTypeProvenance>,
|
|
319
|
+
source: CalendarEventTypeProvenance,
|
|
320
|
+
): CalendarEventTypeDefinition {
|
|
321
|
+
const next: CalendarEventTypeDefinition = {
|
|
322
|
+
...definition,
|
|
323
|
+
behavior: { ...definition.behavior, fields: { ...definition.behavior.fields }, customFieldsetIds: [...definition.behavior.customFieldsetIds] },
|
|
324
|
+
}
|
|
325
|
+
if (patch.replaceLabel !== undefined) { next.label = patch.replaceLabel; provenance.label = source }
|
|
326
|
+
if (patch.replaceLabelKey !== undefined) { next.labelKey = patch.replaceLabelKey ?? undefined; provenance.labelKey = source }
|
|
327
|
+
if (patch.replaceIcon !== undefined) { next.icon = patch.replaceIcon; provenance.icon = source }
|
|
328
|
+
if (patch.replaceColor !== undefined) { next.color = patch.replaceColor; provenance.color = source }
|
|
329
|
+
if (patch.replaceAdminConfigurable !== undefined) { next.adminConfigurable = patch.replaceAdminConfigurable; provenance.adminConfigurable = source }
|
|
330
|
+
if (patch.replacePanelKey !== undefined) { next.panelKey = patch.replacePanelKey ?? undefined; provenance.panelKey = source }
|
|
331
|
+
if (patch.replaceBaseKind !== undefined) { next.behavior.baseKind = patch.replaceBaseKind; provenance['behavior.baseKind'] = source }
|
|
332
|
+
if (patch.replaceSelectable !== undefined) { next.behavior.selectable = patch.replaceSelectable; provenance['behavior.selectable'] = source }
|
|
333
|
+
if (patch.replaceOrder !== undefined) { next.behavior.order = patch.replaceOrder; provenance['behavior.order'] = source }
|
|
334
|
+
if (patch.replaceFields) {
|
|
335
|
+
next.behavior.fields = { ...next.behavior.fields, ...patch.replaceFields }
|
|
336
|
+
for (const field of Object.keys(patch.replaceFields)) provenance[`behavior.fields.${field}`] = source
|
|
337
|
+
}
|
|
338
|
+
if (patch.replaceCustomFieldsetIds || patch.deleteCustomFieldsetIds || patch.appendCustomFieldsetIds) {
|
|
339
|
+
const fieldsets = patch.replaceCustomFieldsetIds ?? next.behavior.customFieldsetIds
|
|
340
|
+
const deleted = new Set(patch.deleteCustomFieldsetIds ?? [])
|
|
341
|
+
next.behavior.customFieldsetIds = [...new Set([...fieldsets.filter((id) => !deleted.has(id)), ...(patch.appendCustomFieldsetIds ?? [])])]
|
|
342
|
+
provenance['behavior.customFieldsetIds'] = source
|
|
343
|
+
}
|
|
344
|
+
return calendarEventTypeDefinitionSchema.parse(next)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function compose(excludedSources: ReadonlySet<object> = new Set(), rejectedSources: readonly CalendarEventTypeDiagnostic[] = []): {
|
|
348
|
+
items: readonly EffectiveCalendarEventType[]
|
|
349
|
+
tombstones: readonly CalendarEventTypeTombstone[]
|
|
350
|
+
diagnostics: readonly CalendarEventTypeDiagnostic[]
|
|
351
|
+
} {
|
|
352
|
+
const definitions = new Map<string, CalendarEventTypeDefinition>()
|
|
353
|
+
const owners = new Map<string, CalendarEventTypeProvenance>()
|
|
354
|
+
const provenance = new Map<string, Record<string, CalendarEventTypeProvenance>>()
|
|
355
|
+
const tombstones = new Map<string, CalendarEventTypeTombstone>()
|
|
356
|
+
const diagnostics: CalendarEventTypeDiagnostic[] = [...getRegistryState().diagnostics, ...rejectedSources]
|
|
357
|
+
const coreSource = Object.freeze({ moduleId: 'customers', sourcePath: 'calendar-event-types.ts', moduleOrder: 0, phase: 'core' as const })
|
|
358
|
+
|
|
359
|
+
for (const definition of calendarEventTypes) {
|
|
360
|
+
definitions.set(definition.key, definition)
|
|
361
|
+
owners.set(definition.key, coreSource)
|
|
362
|
+
provenance.set(definition.key, markDefinitionProvenance(definition, coreSource))
|
|
363
|
+
}
|
|
364
|
+
const state = getRegistryState()
|
|
365
|
+
const widgets = state.widgets.map((entry, moduleOrder) => ({ entry, moduleOrder })).filter(({ entry }) => !excludedSources.has(entry))
|
|
366
|
+
const modules = state.modules.map((entry, moduleOrder) => ({ entry, moduleOrder })).filter(({ entry }) => !excludedSources.has(entry))
|
|
367
|
+
const programmatic = [...state.sources.entries()].filter(([, contribution]) => !excludedSources.has(contribution))
|
|
368
|
+
|
|
369
|
+
for (const { entry, moduleOrder } of widgets) {
|
|
370
|
+
const source = sourceOf(entry.moduleId, `widgets/injection/${entry.widgetId}/widget.ts`, moduleOrder, 'widget')
|
|
371
|
+
for (const definition of entry.definitions ?? []) {
|
|
372
|
+
const existing = owners.get(definition.key)
|
|
373
|
+
if (existing) {
|
|
374
|
+
diagnostics.push({ code: 'duplicate-definition', key: definition.key, moduleId: entry.moduleId, widgetId: entry.widgetId, owner: existing.moduleId })
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
definitions.set(definition.key, definition)
|
|
378
|
+
owners.set(definition.key, source)
|
|
379
|
+
provenance.set(definition.key, markDefinitionProvenance(definition, source))
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const [sourceId, contribution] of programmatic) {
|
|
383
|
+
const source = sourceOf(sourceId, `di:${sourceId}`, 0, 'programmatic')
|
|
384
|
+
for (const definition of contribution.definitions.values()) {
|
|
385
|
+
const existing = owners.get(definition.key)
|
|
386
|
+
if (existing && existing.moduleId !== sourceId) {
|
|
387
|
+
diagnostics.push({ code: 'duplicate-definition', key: definition.key, moduleId: sourceId, owner: existing.moduleId })
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
definitions.set(definition.key, definition)
|
|
391
|
+
owners.set(definition.key, source)
|
|
392
|
+
provenance.set(definition.key, markDefinitionProvenance(definition, source))
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const applyOverrides = (
|
|
397
|
+
overrides: Readonly<Record<string, CalendarEventTypeDefinition | null>>,
|
|
398
|
+
source: CalendarEventTypeProvenance,
|
|
399
|
+
widgetId?: string,
|
|
400
|
+
): void => {
|
|
401
|
+
for (const [key, override] of Object.entries(overrides)) {
|
|
402
|
+
if (!definitions.has(key)) {
|
|
403
|
+
if (override === null) {
|
|
404
|
+
diagnostics.push({ code: 'unknown-override', key, moduleId: source.moduleId, widgetId })
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
diagnostics.push({ code: 'unknown-override', key, moduleId: source.moduleId, widgetId })
|
|
408
|
+
}
|
|
409
|
+
if (override === null) {
|
|
410
|
+
tombstones.set(key, deepFreeze({ key, provenance: source }))
|
|
411
|
+
} else {
|
|
412
|
+
definitions.set(key, override)
|
|
413
|
+
owners.set(key, source)
|
|
414
|
+
provenance.set(key, markDefinitionProvenance(override, source))
|
|
415
|
+
tombstones.delete(key)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const { entry, moduleOrder } of widgets) {
|
|
420
|
+
applyOverrides(entry.overrides ?? {}, sourceOf(entry.moduleId, `widgets/injection/${entry.widgetId}/widget.ts`, moduleOrder, 'widget'), entry.widgetId)
|
|
421
|
+
}
|
|
422
|
+
for (const { entry, moduleOrder } of modules) {
|
|
423
|
+
applyOverrides(entry.overrides.eventTypes ?? {}, sourceOf(entry.moduleId, 'modules.ts', moduleOrder, 'module'))
|
|
424
|
+
}
|
|
425
|
+
for (const [sourceId, contribution] of programmatic) {
|
|
426
|
+
applyOverrides(Object.fromEntries(contribution.overrides), sourceOf(sourceId, `di:${sourceId}`, 0, 'programmatic'))
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const applyPatches = (patches: readonly CalendarEventTypePatch[], source: CalendarEventTypeProvenance, widgetId?: string): CalendarEventTypeDiagnostic | null => {
|
|
430
|
+
for (const patch of patches) {
|
|
431
|
+
const key = patch.targetEventTypeKey
|
|
432
|
+
const definition = definitions.get(key)
|
|
433
|
+
if (!definition || tombstones.has(key)) {
|
|
434
|
+
diagnostics.push({ code: 'unknown-patch', key, moduleId: source.moduleId, widgetId })
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
const propertyProvenance = { ...provenance.get(key) }
|
|
438
|
+
try {
|
|
439
|
+
const patched = applyPatch(definition, patch, propertyProvenance, source)
|
|
440
|
+
definitions.set(key, patched)
|
|
441
|
+
provenance.set(key, propertyProvenance)
|
|
442
|
+
} catch {
|
|
443
|
+
return { code: 'invalid-source', key, moduleId: source.moduleId, widgetId }
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return null
|
|
447
|
+
}
|
|
448
|
+
for (const { entry, moduleOrder } of widgets) {
|
|
449
|
+
const failure = applyPatches(entry.patches ?? [], sourceOf(entry.moduleId, `widgets/injection/${entry.widgetId}/widget.ts`, moduleOrder, 'widget'), entry.widgetId)
|
|
450
|
+
if (failure) return compose(new Set([...excludedSources, entry]), [...rejectedSources, failure])
|
|
451
|
+
}
|
|
452
|
+
for (const { entry, moduleOrder } of modules) {
|
|
453
|
+
const failure = applyPatches(entry.overrides.patches ?? [], sourceOf(entry.moduleId, 'modules.ts', moduleOrder, 'module'))
|
|
454
|
+
if (failure) return compose(new Set([...excludedSources, entry]), [...rejectedSources, failure])
|
|
455
|
+
}
|
|
456
|
+
for (const [sourceId, contribution] of programmatic) {
|
|
457
|
+
const failure = applyPatches([...contribution.patches.values()], sourceOf(sourceId, `di:${sourceId}`, 0, 'programmatic'))
|
|
458
|
+
if (failure) return compose(new Set([...excludedSources, contribution]), [...rejectedSources, failure])
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const items = [...definitions.values()]
|
|
462
|
+
.filter((definition) => !tombstones.has(definition.key))
|
|
463
|
+
.map((definition) => deepFreeze({
|
|
464
|
+
...definition,
|
|
465
|
+
provenance: Object.freeze({ ...(provenance.get(definition.key) ?? {}) }),
|
|
466
|
+
historical: false,
|
|
467
|
+
}))
|
|
468
|
+
.sort((left, right) => left.behavior.order - right.behavior.order || left.key.localeCompare(right.key))
|
|
469
|
+
return deepFreeze({ items, tombstones: [...tombstones.values()], diagnostics })
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function archiveCurrentDefinitions(): void {
|
|
473
|
+
const state = getRegistryState()
|
|
474
|
+
for (const item of compose().items) {
|
|
475
|
+
const { provenance: _provenance, historical: _historical, fallbackReason: _fallbackReason, ...definition } = item
|
|
476
|
+
state.historicalDefinitions.set(item.key, deepFreeze(definition))
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function registerWidgetCalendarEventTypeContributions(entries: readonly CalendarEventTypeWidgetContribution[]): void {
|
|
481
|
+
const state = getRegistryState()
|
|
482
|
+
const diagnostics: CalendarEventTypeDiagnostic[] = []
|
|
483
|
+
const parsed: ParsedWidgetContribution[] = []
|
|
484
|
+
for (const entry of entries) {
|
|
485
|
+
const result = widgetContributionSchema.safeParse(entry)
|
|
486
|
+
if (result.success) parsed.push(result.data)
|
|
487
|
+
else diagnostics.push({ code: 'invalid-source', moduleId: entry.moduleId, widgetId: entry.widgetId })
|
|
488
|
+
}
|
|
489
|
+
const moduleOrder = new Map<string, number>()
|
|
490
|
+
entries.forEach((entry) => { if (!moduleOrder.has(entry.moduleId)) moduleOrder.set(entry.moduleId, moduleOrder.size) })
|
|
491
|
+
parsed.sort((left, right) => (moduleOrder.get(left.moduleId) ?? 0) - (moduleOrder.get(right.moduleId) ?? 0)
|
|
492
|
+
|| (left.priority ?? 0) - (right.priority ?? 0) || left.widgetId.localeCompare(right.widgetId))
|
|
493
|
+
const moduleDiagnostics = state.diagnostics.filter((diagnostic) => !diagnostic.widgetId)
|
|
494
|
+
if (JSON.stringify(state.widgets) === JSON.stringify(parsed)
|
|
495
|
+
&& JSON.stringify(state.diagnostics.filter((diagnostic) => diagnostic.widgetId)) === JSON.stringify(diagnostics)) return
|
|
496
|
+
archiveCurrentDefinitions()
|
|
497
|
+
state.widgets = deepFreeze(parsed)
|
|
498
|
+
state.diagnostics = deepFreeze([...diagnostics, ...moduleDiagnostics])
|
|
499
|
+
state.version += 1
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function registerCalendarModuleOverrides(entries: readonly CalendarModuleOverrideEntry[]): void {
|
|
503
|
+
const state = getRegistryState()
|
|
504
|
+
const diagnostics: CalendarEventTypeDiagnostic[] = []
|
|
505
|
+
const parsed: CalendarModuleOverrideEntry[] = []
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
const result = moduleOverrideEntrySchema.safeParse(entry)
|
|
508
|
+
if (result.success) parsed.push(result.data)
|
|
509
|
+
else diagnostics.push({ code: 'invalid-source', moduleId: entry.moduleId })
|
|
510
|
+
}
|
|
511
|
+
const widgetDiagnostics = state.diagnostics.filter((diagnostic) => diagnostic.widgetId)
|
|
512
|
+
if (JSON.stringify(state.modules) === JSON.stringify(parsed)
|
|
513
|
+
&& JSON.stringify(state.diagnostics.filter((diagnostic) => !diagnostic.widgetId)) === JSON.stringify(diagnostics)) return
|
|
514
|
+
archiveCurrentDefinitions()
|
|
515
|
+
state.modules = deepFreeze(parsed)
|
|
516
|
+
state.diagnostics = deepFreeze([...widgetDiagnostics, ...diagnostics])
|
|
517
|
+
state.version += 1
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export type CalendarEventTypeRegistrySnapshot = Readonly<{
|
|
521
|
+
version: number
|
|
522
|
+
items: readonly EffectiveCalendarEventType[]
|
|
523
|
+
tombstones: readonly CalendarEventTypeTombstone[]
|
|
524
|
+
diagnostics: readonly CalendarEventTypeDiagnostic[]
|
|
525
|
+
}>
|
|
526
|
+
|
|
527
|
+
export interface CalendarEventTypeRegistry {
|
|
528
|
+
upsert(sourceId: string, definition: CalendarEventTypeDefinition): void
|
|
529
|
+
replace(sourceId: string, key: string, definition: CalendarEventTypeDefinition | null): void
|
|
530
|
+
patch(sourceId: string, patch: CalendarEventTypePatch): void
|
|
531
|
+
remove(sourceId: string, key: string): void
|
|
532
|
+
removeSource(sourceId: string): void
|
|
533
|
+
snapshot(): CalendarEventTypeRegistrySnapshot
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function validatedSourceId(sourceId: string): string {
|
|
537
|
+
return z.string().trim().min(1).max(100).parse(sourceId)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function baseOwnerOf(key: string): string | undefined {
|
|
541
|
+
if (calendarEventTypes.some((definition) => definition.key === key)) return 'customers'
|
|
542
|
+
const invalidSources = compose().diagnostics.filter((diagnostic) => diagnostic.code === 'invalid-source')
|
|
543
|
+
for (const widget of getRegistryState().widgets) {
|
|
544
|
+
if (invalidSources.some((diagnostic) => diagnostic.moduleId === widget.moduleId && diagnostic.widgetId === widget.widgetId)) continue
|
|
545
|
+
if (widget.definitions?.some((definition) => definition.key === key)) return widget.moduleId
|
|
546
|
+
}
|
|
547
|
+
for (const [sourceId, contribution] of getRegistryState().sources) {
|
|
548
|
+
if (invalidSources.some((diagnostic) => diagnostic.moduleId === sourceId && !diagnostic.widgetId)) continue
|
|
549
|
+
if (contribution.definitions.has(key)) return sourceId
|
|
550
|
+
}
|
|
551
|
+
return compose().items.find((definition) => definition.key === key)?.provenance.key?.moduleId
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function createCalendarEventTypeRegistry(): CalendarEventTypeRegistry {
|
|
555
|
+
return {
|
|
556
|
+
upsert(sourceId, definition) {
|
|
557
|
+
const id = validatedSourceId(sourceId)
|
|
558
|
+
const nextDefinition = copyDefinition(definition)
|
|
559
|
+
const owner = baseOwnerOf(nextDefinition.key)
|
|
560
|
+
if (owner && owner !== id) {
|
|
561
|
+
throw new Error(`[internal] Calendar event type "${nextDefinition.key}" is owned by "${owner}"`)
|
|
562
|
+
}
|
|
563
|
+
const current = programmaticSource(id)
|
|
564
|
+
if (JSON.stringify(current.definitions.get(nextDefinition.key)) === JSON.stringify(nextDefinition)) return
|
|
565
|
+
replaceProgrammaticSource(id, { ...current, definitions: new Map(current.definitions).set(nextDefinition.key, nextDefinition) })
|
|
566
|
+
},
|
|
567
|
+
replace(sourceId, key, definition) {
|
|
568
|
+
const id = validatedSourceId(sourceId)
|
|
569
|
+
const parsedKey = calendarEventTypeKeySchema.parse(key)
|
|
570
|
+
const nextDefinition = definition === null ? null : copyDefinition(definition)
|
|
571
|
+
if (nextDefinition && nextDefinition.key !== parsedKey) throw new Error('[internal] Replacement key must match target key')
|
|
572
|
+
const current = programmaticSource(id)
|
|
573
|
+
if (JSON.stringify(current.overrides.get(parsedKey)) === JSON.stringify(nextDefinition)) return
|
|
574
|
+
replaceProgrammaticSource(id, { ...current, overrides: new Map(current.overrides).set(parsedKey, nextDefinition) })
|
|
575
|
+
},
|
|
576
|
+
patch(sourceId, patch) {
|
|
577
|
+
const id = validatedSourceId(sourceId)
|
|
578
|
+
const nextPatch = copyPatch(patch)
|
|
579
|
+
const current = programmaticSource(id)
|
|
580
|
+
if (JSON.stringify(current.patches.get(nextPatch.targetEventTypeKey)) === JSON.stringify(nextPatch)) return
|
|
581
|
+
replaceProgrammaticSource(id, { ...current, patches: new Map(current.patches).set(nextPatch.targetEventTypeKey, nextPatch) })
|
|
582
|
+
},
|
|
583
|
+
remove(sourceId, key) {
|
|
584
|
+
const id = validatedSourceId(sourceId)
|
|
585
|
+
const parsedKey = calendarEventTypeKeySchema.parse(key)
|
|
586
|
+
const current = getRegistryState().sources.get(id)
|
|
587
|
+
if (!current || !(current.definitions.has(parsedKey) || current.overrides.has(parsedKey) || current.patches.has(parsedKey))) return
|
|
588
|
+
const next = { definitions: new Map(current.definitions), overrides: new Map(current.overrides), patches: new Map(current.patches) }
|
|
589
|
+
next.definitions.delete(parsedKey)
|
|
590
|
+
next.overrides.delete(parsedKey)
|
|
591
|
+
next.patches.delete(parsedKey)
|
|
592
|
+
replaceProgrammaticSource(id, next)
|
|
593
|
+
},
|
|
594
|
+
removeSource(sourceId) {
|
|
595
|
+
const id = validatedSourceId(sourceId)
|
|
596
|
+
const state = getRegistryState()
|
|
597
|
+
if (!state.sources.has(id)) return
|
|
598
|
+
archiveCurrentDefinitions()
|
|
599
|
+
state.sources.delete(id)
|
|
600
|
+
state.version += 1
|
|
601
|
+
},
|
|
602
|
+
snapshot() {
|
|
603
|
+
const state = getRegistryState()
|
|
604
|
+
return deepFreeze({ version: state.version, ...compose() })
|
|
605
|
+
},
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function getCalendarEventTypeRegistryVersion(): number {
|
|
610
|
+
return getRegistryState().version
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function getCalendarEventTypes(): readonly EffectiveCalendarEventType[] {
|
|
614
|
+
return compose().items
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function resolveCalendarEventTypes(): readonly EffectiveCalendarEventType[] {
|
|
618
|
+
return getCalendarEventTypes()
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function getCalendarEventTypeTombstones(): readonly CalendarEventTypeTombstone[] {
|
|
622
|
+
return compose().tombstones
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function getCalendarEventTypeDiagnostics(): readonly CalendarEventTypeDiagnostic[] {
|
|
626
|
+
return compose().diagnostics
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function resolveCalendarEventType(
|
|
630
|
+
rawKey: string,
|
|
631
|
+
options: { includeHistorical?: boolean } = {},
|
|
632
|
+
): EffectiveCalendarEventType | undefined {
|
|
633
|
+
const parsedKey = calendarEventTypeKeySchema.safeParse(rawKey.trim().toLowerCase())
|
|
634
|
+
if (!parsedKey.success) return undefined
|
|
635
|
+
const key = parsedKey.data
|
|
636
|
+
const current = compose()
|
|
637
|
+
const resolved = current.items.find((item) => item.key === key)
|
|
638
|
+
if (resolved || !options.includeHistorical) return resolved
|
|
639
|
+
|
|
640
|
+
const stored = getRegistryState().historicalDefinitions.get(key)
|
|
641
|
+
const reason: CalendarEventTypeHistoricalFallback['reason'] = current.tombstones.some((entry) => entry.key === key)
|
|
642
|
+
? 'tombstoned'
|
|
643
|
+
: stored
|
|
644
|
+
? 'module-unavailable'
|
|
645
|
+
: 'unknown'
|
|
646
|
+
const definition = stored ?? {
|
|
647
|
+
...calendarEventTypes[0],
|
|
648
|
+
key,
|
|
649
|
+
label: rawKey,
|
|
650
|
+
labelKey: undefined,
|
|
651
|
+
behavior: { ...calendarEventTypes[0]!.behavior, selectable: false },
|
|
652
|
+
}
|
|
653
|
+
const fallbackSource = Object.freeze({ moduleId: 'customers', sourcePath: 'calendar-event-types.ts', moduleOrder: 0, phase: 'fallback' as const })
|
|
654
|
+
const historicalDefinition = calendarEventTypeDefinitionSchema.parse({
|
|
655
|
+
...definition,
|
|
656
|
+
behavior: { ...definition.behavior, selectable: false },
|
|
657
|
+
})
|
|
658
|
+
return deepFreeze({
|
|
659
|
+
...historicalDefinition,
|
|
660
|
+
provenance: Object.fromEntries(allPropertyPaths(historicalDefinition).map((path) => [path, fallbackSource])),
|
|
661
|
+
historical: true,
|
|
662
|
+
fallbackReason: reason,
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function resetCalendarEventTypeRegistryForTests(): void {
|
|
667
|
+
const state = getRegistryState()
|
|
668
|
+
state.widgets = Object.freeze([])
|
|
669
|
+
state.modules = Object.freeze([])
|
|
670
|
+
state.sources.clear()
|
|
671
|
+
state.historicalDefinitions.clear()
|
|
672
|
+
state.version = 0
|
|
673
|
+
state.diagnostics = Object.freeze([])
|
|
674
|
+
}
|