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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
  3. package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
  4. package/dist/generated/entities/customer_interaction/index.js +2 -0
  5. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  9. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  10. package/dist/modules/customers/api/activity-types/route.js +151 -0
  11. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  12. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  13. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  14. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  15. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  16. package/dist/modules/customers/api/interactions/route.js +3 -0
  17. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  18. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  19. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  20. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  21. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  22. package/dist/modules/customers/calendar-event-types.js +548 -0
  23. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  24. package/dist/modules/customers/commands/dictionaries.js +70 -10
  25. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  26. package/dist/modules/customers/commands/interactions.js +187 -22
  27. package/dist/modules/customers/commands/interactions.js.map +3 -3
  28. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  29. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  30. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  31. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  32. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  33. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  34. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  35. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  36. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  37. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  38. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  39. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  40. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  41. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  42. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  43. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  44. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  45. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  46. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  47. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  48. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  49. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  50. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  51. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  52. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  53. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  54. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  55. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  56. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  57. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  58. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  59. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  60. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  61. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  62. package/dist/modules/customers/components/calendar/types.js +1 -0
  63. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  64. package/dist/modules/customers/data/entities.js +6 -0
  65. package/dist/modules/customers/data/entities.js.map +2 -2
  66. package/dist/modules/customers/data/validators.js +39 -10
  67. package/dist/modules/customers/data/validators.js.map +2 -2
  68. package/dist/modules/customers/di.js +20 -1
  69. package/dist/modules/customers/di.js.map +2 -2
  70. package/dist/modules/customers/extension-points.js +13 -0
  71. package/dist/modules/customers/extension-points.js.map +2 -2
  72. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  73. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  74. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  75. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  76. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  77. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  78. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  79. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  80. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  81. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  82. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  83. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  84. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  85. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  86. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  87. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  88. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  89. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  90. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  91. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  92. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  93. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  94. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  95. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  96. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  97. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  98. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  99. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  100. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  101. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  102. package/generated/entities/customer_interaction/index.ts +1 -0
  103. package/generated/entity-fields-registry.ts +2 -0
  104. package/package.json +7 -7
  105. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  106. package/src/modules/customers/api/activity-types/route.ts +154 -0
  107. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  108. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  109. package/src/modules/customers/api/interactions/route.ts +4 -0
  110. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  111. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  112. package/src/modules/customers/calendar-event-types.ts +674 -0
  113. package/src/modules/customers/commands/dictionaries.ts +84 -8
  114. package/src/modules/customers/commands/interactions.ts +244 -21
  115. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  116. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  117. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  118. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  119. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  120. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  121. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  122. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  123. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  124. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  125. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  126. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  127. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  128. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  129. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  130. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  131. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  132. package/src/modules/customers/components/calendar/types.ts +1 -0
  133. package/src/modules/customers/components/detail/types.ts +1 -0
  134. package/src/modules/customers/data/entities.ts +8 -1
  135. package/src/modules/customers/data/validators.ts +42 -7
  136. package/src/modules/customers/di.ts +21 -0
  137. package/src/modules/customers/extension-points.ts +13 -0
  138. package/src/modules/customers/i18n/de.json +83 -0
  139. package/src/modules/customers/i18n/en.json +83 -0
  140. package/src/modules/customers/i18n/es.json +83 -0
  141. package/src/modules/customers/i18n/ko.json +83 -0
  142. package/src/modules/customers/i18n/pl.json +83 -0
  143. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  144. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  145. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  146. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  147. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  148. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  149. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  150. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  151. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  152. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  153. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  154. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  155. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  156. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  157. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  158. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
@@ -0,0 +1,154 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
6
+ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
+ import type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'
8
+ import { createLogger } from '@open-mercato/shared/lib/logger'
9
+ import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
10
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
11
+ import { resolveDictionaryRouteContext } from '../dictionaries/context'
12
+ import { createDictionaryCacheTags, DICTIONARY_CACHE_TTL_MS } from '../dictionaries/cache'
13
+ import { getInjectionRegistryVersion } from '@open-mercato/shared/modules/widgets/injection-loader'
14
+ import {
15
+ resolveScopedCalendarEventTypes,
16
+ } from '../../lib/calendar/eventTypeResolver'
17
+ import { calendarEventTypeBehaviorSchema } from '../../calendar-event-types'
18
+ import { getCalendarEventTypeRegistryVersion } from '../../calendar-event-types'
19
+
20
+ const logger = createLogger('customers')
21
+
22
+ function reportActivityTypeError(error: unknown, code: string, attributes?: Record<string, string>): void {
23
+ try {
24
+ getTelemetryRuntime()?.reportError(error, { module: 'customers', code, attributes })
25
+ } catch {}
26
+ }
27
+
28
+ const querySchema = z.object({ organizationId: z.string().uuid().optional() })
29
+ const responseSchema = z.object({
30
+ fallbackKey: z.literal('meeting'),
31
+ items: z.array(z.object({
32
+ key: z.string(),
33
+ label: z.string(),
34
+ labelKey: z.string().optional(),
35
+ icon: z.string().nullable().optional(),
36
+ color: z.string().nullable().optional(),
37
+ behavior: calendarEventTypeBehaviorSchema,
38
+ selectable: z.boolean(),
39
+ panelKey: z.string().optional(),
40
+ source: z.string(),
41
+ provenance: z.record(z.string(), z.unknown()),
42
+ historical: z.boolean(),
43
+ fallbackReason: z.enum(['tombstoned', 'module-unavailable', 'unknown']).optional(),
44
+ adminConfigurable: z.boolean(),
45
+ isInherited: z.boolean(),
46
+ isLocalOverride: z.boolean(),
47
+ updatedAt: z.string().nullable(),
48
+ missingCustomFieldsetIds: z.array(z.string()),
49
+ inactiveDictionaryOverride: z.boolean().optional(),
50
+ })),
51
+ })
52
+
53
+ export const metadata = {
54
+ GET: { requireAuth: true },
55
+ }
56
+
57
+ export async function GET(req: Request) {
58
+ try {
59
+ const url = new URL(req.url)
60
+ const query = querySchema.parse({ organizationId: url.searchParams.get('organizationId') ?? undefined })
61
+ const context = await resolveDictionaryRouteContext(req, { selectedId: query.organizationId })
62
+ if (!context.organizationId) {
63
+ return NextResponse.json({ error: context.translate('customers.errors.organization_required', 'Organization context is required') }, { status: 400 })
64
+ }
65
+ const organizationId = context.organizationId
66
+ const rbac = context.container.resolve('rbacService') as RbacService | undefined
67
+ const actorId = context.auth?.sub
68
+ if (!rbac || !actorId) {
69
+ throw new CrudHttpError(403, { error: context.translate('customers.calendar.activityTypes.errors.forbidden', 'You do not have permission to view activity types.') })
70
+ }
71
+ const permissionScope = { tenantId: context.tenantId, organizationId }
72
+ const canViewInteractions = await rbac.userHasAllFeatures(actorId, ['customers.interactions.view'], permissionScope)
73
+ const canManageSettings = canViewInteractions || await rbac.userHasAllFeatures(actorId, ['customers.settings.manage'], permissionScope)
74
+ if (!canManageSettings) {
75
+ throw new CrudHttpError(403, { error: context.translate('customers.calendar.activityTypes.errors.forbidden', 'You do not have permission to view activity types.') })
76
+ }
77
+ const hierarchy = context.container.resolve('organizationHierarchyService') as OrganizationHierarchyService
78
+ const ancestors = await hierarchy.resolveAncestorIds({ tenantId: context.tenantId, organizationId })
79
+ if (ancestors === null) {
80
+ throw new CrudHttpError(403, { error: context.translate('customers.errors.organization_forbidden', 'Organization not accessible') })
81
+ }
82
+ const scopeIds = Array.from(new Set([organizationId, ...ancestors.slice().reverse()]))
83
+ const cacheKey = `customers:activity-types:${context.tenantId}:org=${organizationId}:scope=${scopeIds.join('|')}:widgets=${getInjectionRegistryVersion()}:registry=${getCalendarEventTypeRegistryVersion()}`
84
+ const cached = await context.cache?.get(cacheKey)
85
+ if (cached) return NextResponse.json(cached)
86
+
87
+ let catalog
88
+ try {
89
+ catalog = await resolveScopedCalendarEventTypes({
90
+ em: context.em,
91
+ tenantId: context.tenantId,
92
+ organizationId,
93
+ readableOrganizationIds: scopeIds,
94
+ })
95
+ } catch (err) {
96
+ logger.error('customers.activity_types.resolve failed', {
97
+ err,
98
+ tenantId: context.tenantId,
99
+ organizationId,
100
+ })
101
+ reportActivityTypeError(err, 'customers.activity_type_catalog_resolution_failed', {
102
+ tenantId: context.tenantId,
103
+ organizationId,
104
+ })
105
+ return NextResponse.json({
106
+ error: context.translate('customers.calendar.activityTypes.errors.loadFailed', 'Failed to load activity types'),
107
+ code: 'activity_type_catalog_unavailable',
108
+ }, { status: 503 })
109
+ }
110
+ if (context.cache) {
111
+ await context.cache.set(cacheKey, catalog, {
112
+ ttl: DICTIONARY_CACHE_TTL_MS,
113
+ tags: createDictionaryCacheTags({
114
+ tenantId: context.tenantId,
115
+ mappedKind: 'activity_type',
116
+ organizationIds: scopeIds,
117
+ }),
118
+ }).catch((err) => {
119
+ logger.warn('customers.activity_types cache write failed', { err })
120
+ reportActivityTypeError(err, 'customers.activity_type_catalog_cache_write_failed', {
121
+ tenantId: context.tenantId,
122
+ organizationId,
123
+ })
124
+ })
125
+ }
126
+ return NextResponse.json(catalog)
127
+ } catch (err) {
128
+ if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
129
+ logger.error('customers.activity_types request failed', { err })
130
+ reportActivityTypeError(err, 'customers.activity_type_catalog_request_failed')
131
+ const { translate } = await resolveTranslations()
132
+ return NextResponse.json({
133
+ error: translate('customers.calendar.activityTypes.errors.loadFailed', 'Failed to load activity types'),
134
+ }, { status: err instanceof z.ZodError ? 400 : 500 })
135
+ }
136
+ }
137
+
138
+ export const openApi: OpenApiRouteDoc = {
139
+ tag: 'Customers',
140
+ summary: 'Effective calendar activity types',
141
+ methods: {
142
+ GET: {
143
+ summary: 'List effective calendar activity types',
144
+ description: 'Returns the tenant and organization scoped calendar activity-type catalog. Requires customers.interactions.view or customers.settings.manage.',
145
+ responses: [{ status: 200, description: 'Effective activity-type catalog', schema: responseSchema }],
146
+ errors: [
147
+ { status: 400, description: 'Organization scope is required', schema: z.object({ error: z.string() }) },
148
+ { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
149
+ { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },
150
+ { status: 500, description: 'Unexpected server error', schema: z.object({ error: z.string() }) },
151
+ ],
152
+ },
153
+ },
154
+ }
@@ -17,6 +17,7 @@ import {
17
17
  } from '@open-mercato/shared/lib/crud/mutation-guard'
18
18
  import { createLogger } from '@open-mercato/shared/lib/logger'
19
19
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
20
+ import { calendarEventTypeBehaviorSchema } from '../../../../calendar-event-types'
20
21
 
21
22
  const logger = createLogger('customers')
22
23
 
@@ -30,8 +31,9 @@ const patchSchema = z
30
31
  label: z.string().trim().max(150).optional(),
31
32
  color: z.union([z.string().trim(), z.null()]).optional(),
32
33
  icon: z.union([z.string().trim(), z.null()]).optional(),
34
+ behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
33
35
  })
34
- .refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined, {
36
+ .refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined || input.behavior !== undefined, {
35
37
  message: 'No changes provided',
36
38
  })
37
39
 
@@ -81,6 +83,7 @@ export async function PATCH(req: Request, ctx: { params?: { kind?: string; id?:
81
83
  label: payload.label,
82
84
  color: payload.color,
83
85
  icon: payload.icon,
86
+ behavior: payload.behavior,
84
87
  },
85
88
  ctx: routeContext.ctx,
86
89
  })) as CommandExecuteResult<{ entryId: string; changed: boolean }>
@@ -103,6 +106,7 @@ export async function PATCH(req: Request, ctx: { params?: { kind?: string; id?:
103
106
  ),
104
107
  })
105
108
  }
109
+ if (typeof err.body.code === 'string') throw err
106
110
  throw new CrudHttpError(409, { error: routeContext.translate('customers.config.dictionaries.errors.duplicate', 'An entry with this value already exists') })
107
111
  }
108
112
  if (err.status === 400) {
@@ -141,8 +145,10 @@ export async function PATCH(req: Request, ctx: { params?: { kind?: string; id?:
141
145
  label: entry.label,
142
146
  color: entry.color,
143
147
  icon: entry.icon,
148
+ ...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),
144
149
  organizationId: entry.organizationId,
145
150
  isInherited: false,
151
+ updatedAt: entry.updatedAt,
146
152
  })
147
153
  if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
148
154
  response.headers.set(
@@ -297,8 +303,10 @@ const dictionaryEntrySchema = z.object({
297
303
  label: z.string().nullable().optional(),
298
304
  color: z.string().nullable().optional(),
299
305
  icon: z.string().nullable().optional(),
306
+ behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
300
307
  organizationId: z.string().uuid().nullable().optional(),
301
308
  isInherited: z.boolean().optional(),
309
+ updatedAt: z.date().or(z.string()).optional(),
302
310
  })
303
311
 
304
312
  const dictionaryDeleteResponseSchema = z.object({
@@ -24,6 +24,8 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
24
24
  import { createLogger } from '@open-mercato/shared/lib/logger'
25
25
  import { CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE } from '../../../lib/dictionaries'
26
26
  import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
27
+ import { calendarEventTypeBehaviorSchema } from '../../../calendar-event-types'
28
+ import type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'
27
29
 
28
30
  const logger = createLogger('customers')
29
31
 
@@ -35,6 +37,7 @@ const postSchema = z.object({
35
37
  label: z.string().trim().max(150).optional(),
36
38
  color: colorSchema.or(z.null()).optional(),
37
39
  icon: iconSchema.or(z.null()).optional(),
40
+ behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
38
41
  })
39
42
 
40
43
  const querySchema = z.object({
@@ -52,7 +55,7 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
52
55
  const query = querySchema.parse({
53
56
  organizationId: url.searchParams.get('organizationId') ?? undefined,
54
57
  })
55
- const { translate, em, organizationId, readableOrganizationIds, tenantId, cache } = await resolveDictionaryRouteContext(req, {
58
+ const { translate, em, organizationId, readableOrganizationIds, tenantId, cache, container } = await resolveDictionaryRouteContext(req, {
56
59
  selectedId: query.organizationId ?? undefined,
57
60
  })
58
61
  const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind)
@@ -64,7 +67,15 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
64
67
  }
65
68
  const settings = await loadCustomerSettings(em, { tenantId, organizationId })
66
69
  const sortMode = resolveDictionaryEntrySortMode(settings?.dictionarySortModes?.[kind])
67
- const scopedOrganizationIds = readableOrganizationIds.length > 0 ? readableOrganizationIds : [organizationId]
70
+ let scopedOrganizationIds = readableOrganizationIds.length > 0 ? readableOrganizationIds : [organizationId]
71
+ if (mappedKind === 'activity_type') {
72
+ const hierarchy = container.resolve('organizationHierarchyService') as OrganizationHierarchyService
73
+ const ancestors = await hierarchy.resolveAncestorIds({ tenantId, organizationId })
74
+ if (ancestors === null) {
75
+ throw new CrudHttpError(403, { error: translate('customers.errors.organization_forbidden', 'Organization not accessible') })
76
+ }
77
+ scopedOrganizationIds = Array.from(new Set([organizationId, ...ancestors.slice().reverse()]))
78
+ }
68
79
  const canUseCache = Boolean(cache) && mappedKind !== 'person_company_role'
69
80
 
70
81
  let cacheKey: string | null = null
@@ -133,6 +144,7 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
133
144
  label: entry.label,
134
145
  color: entry.color,
135
146
  icon: entry.icon,
147
+ ...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),
136
148
  organizationId: entry.organizationId,
137
149
  isInherited: entry.organizationId !== organizationId,
138
150
  createdAt: entry.createdAt,
@@ -213,6 +225,7 @@ export async function POST(req: Request, ctx: { params?: { kind?: string } }) {
213
225
  label: body.label,
214
226
  color: body.color,
215
227
  icon: body.icon,
228
+ behavior: body.behavior,
216
229
  },
217
230
  ctx: context.ctx,
218
231
  })) as CommandExecuteResult<{ entryId: string; mode: 'created' | 'updated' | 'unchanged' }>
@@ -248,8 +261,10 @@ export async function POST(req: Request, ctx: { params?: { kind?: string } }) {
248
261
  label: entry.label,
249
262
  color: entry.color,
250
263
  icon: entry.icon,
264
+ ...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),
251
265
  organizationId: entry.organizationId,
252
266
  isInherited: false,
267
+ updatedAt: entry.updatedAt,
253
268
  },
254
269
  { status: result.mode === 'created' ? 201 : 200 }
255
270
  )
@@ -288,6 +303,7 @@ const dictionaryEntrySchema = z.object({
288
303
  label: z.string().nullable().optional(),
289
304
  color: z.string().nullable().optional(),
290
305
  icon: z.string().nullable().optional(),
306
+ behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
291
307
  organizationId: z.string().uuid().nullable().optional(),
292
308
  isInherited: z.boolean().optional(),
293
309
  createdAt: z.date().or(z.string()).optional(),
@@ -179,6 +179,7 @@ type InteractionListRow = {
179
179
  appearance_icon: string | null
180
180
  appearance_color: string | null
181
181
  source: string | null
182
+ timezone: string | null
182
183
  duration_minutes: number | null
183
184
  location: string | null
184
185
  all_day: boolean | null
@@ -314,6 +315,7 @@ const INTERACTION_LIST_COLUMNS = [
314
315
  'appearance_icon',
315
316
  'appearance_color',
316
317
  'source',
318
+ 'timezone',
317
319
  'duration_minutes',
318
320
  'location',
319
321
  'all_day',
@@ -713,6 +715,7 @@ export async function GET(req: Request) {
713
715
  source: row.source ?? null,
714
716
  duration: row.duration_minutes ?? null,
715
717
  durationMinutes: row.duration_minutes ?? null,
718
+ timezone: row.timezone ?? null,
716
719
  location: row.location ?? null,
717
720
  allDay: row.all_day ?? null,
718
721
  recurrenceRule: row.recurrence_rule ?? null,
@@ -794,6 +797,7 @@ const interactionListItemSchema = z
794
797
  source: z.string().nullable().optional(),
795
798
  duration: z.number().nullable().optional(),
796
799
  durationMinutes: z.number().nullable().optional(),
800
+ timezone: z.string().nullable().optional(),
797
801
  location: z.string().nullable().optional(),
798
802
  allDay: z.boolean().nullable().optional(),
799
803
  recurrenceRule: z.string().nullable().optional(),
@@ -153,6 +153,7 @@ const todoItemSchema = z.object({
153
153
  todoSeverity: z.string().nullable().optional(),
154
154
  todoDescription: z.string().nullable().optional(),
155
155
  todoDueAt: z.string().nullable().optional(),
156
+ todoTimezone: z.string().nullable().optional(),
156
157
  todoCustomValues: z.record(z.string(), z.unknown()).nullable().optional(),
157
158
  todoOrganizationId: z.string().nullable(),
158
159
  organizationId: z.string(),
@@ -1,3 +1,22 @@
1
+ import React from 'react'
2
+
3
+ const pipelineIcon = React.createElement(
4
+ 'svg',
5
+ {
6
+ width: 16,
7
+ height: 16,
8
+ viewBox: '0 0 24 24',
9
+ fill: 'none',
10
+ stroke: 'currentColor',
11
+ strokeWidth: 2,
12
+ strokeLinecap: 'round',
13
+ strokeLinejoin: 'round',
14
+ },
15
+ React.createElement('rect', { x: 3, y: 4, width: 5, height: 16, rx: 1 }),
16
+ React.createElement('rect', { x: 10, y: 4, width: 5, height: 16, rx: 1 }),
17
+ React.createElement('rect', { x: 17, y: 4, width: 4, height: 16, rx: 1 })
18
+ )
19
+
1
20
  export const metadata = {
2
21
  requireAuth: true,
3
22
  requireFeatures: ['customers.pipelines.manage'],
@@ -6,6 +25,7 @@ export const metadata = {
6
25
  pageGroup: 'Module Configs',
7
26
  pageGroupKey: 'settings.sections.moduleConfigs',
8
27
  pageOrder: 4,
28
+ icon: pipelineIcon,
9
29
  pageContext: 'settings' as const,
10
30
  breadcrumb: [
11
31
  { label: 'Pipeline stages', labelKey: 'customers.config.nav.pipelineStages' },