@open-mercato/core 0.8.1-develop.7454.1.010095490f → 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.
- 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/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/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/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/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/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/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/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 +7 -7
- package/src/modules/customers/api/activity-types/route.ts +154 -0
- 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/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/types.ts +1 -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 +83 -0
- package/src/modules/customers/i18n/en.json +83 -0
- package/src/modules/customers/i18n/es.json +83 -0
- package/src/modules/customers/i18n/ko.json +83 -0
- package/src/modules/customers/i18n/pl.json +83 -0
- 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/planner/lib/availabilityMerge.ts +106 -41
- package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
4
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
5
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
6
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
7
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
8
|
+
import { resolveDictionaryRouteContext } from "../dictionaries/context.js";
|
|
9
|
+
import { createDictionaryCacheTags, DICTIONARY_CACHE_TTL_MS } from "../dictionaries/cache.js";
|
|
10
|
+
import { getInjectionRegistryVersion } from "@open-mercato/shared/modules/widgets/injection-loader";
|
|
11
|
+
import {
|
|
12
|
+
resolveScopedCalendarEventTypes
|
|
13
|
+
} from "../../lib/calendar/eventTypeResolver.js";
|
|
14
|
+
import { calendarEventTypeBehaviorSchema } from "../../calendar-event-types.js";
|
|
15
|
+
import { getCalendarEventTypeRegistryVersion } from "../../calendar-event-types.js";
|
|
16
|
+
const logger = createLogger("customers");
|
|
17
|
+
function reportActivityTypeError(error, code, attributes) {
|
|
18
|
+
try {
|
|
19
|
+
getTelemetryRuntime()?.reportError(error, { module: "customers", code, attributes });
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const querySchema = z.object({ organizationId: z.string().uuid().optional() });
|
|
24
|
+
const responseSchema = z.object({
|
|
25
|
+
fallbackKey: z.literal("meeting"),
|
|
26
|
+
items: z.array(z.object({
|
|
27
|
+
key: z.string(),
|
|
28
|
+
label: z.string(),
|
|
29
|
+
labelKey: z.string().optional(),
|
|
30
|
+
icon: z.string().nullable().optional(),
|
|
31
|
+
color: z.string().nullable().optional(),
|
|
32
|
+
behavior: calendarEventTypeBehaviorSchema,
|
|
33
|
+
selectable: z.boolean(),
|
|
34
|
+
panelKey: z.string().optional(),
|
|
35
|
+
source: z.string(),
|
|
36
|
+
provenance: z.record(z.string(), z.unknown()),
|
|
37
|
+
historical: z.boolean(),
|
|
38
|
+
fallbackReason: z.enum(["tombstoned", "module-unavailable", "unknown"]).optional(),
|
|
39
|
+
adminConfigurable: z.boolean(),
|
|
40
|
+
isInherited: z.boolean(),
|
|
41
|
+
isLocalOverride: z.boolean(),
|
|
42
|
+
updatedAt: z.string().nullable(),
|
|
43
|
+
missingCustomFieldsetIds: z.array(z.string()),
|
|
44
|
+
inactiveDictionaryOverride: z.boolean().optional()
|
|
45
|
+
}))
|
|
46
|
+
});
|
|
47
|
+
const metadata = {
|
|
48
|
+
GET: { requireAuth: true }
|
|
49
|
+
};
|
|
50
|
+
async function GET(req) {
|
|
51
|
+
try {
|
|
52
|
+
const url = new URL(req.url);
|
|
53
|
+
const query = querySchema.parse({ organizationId: url.searchParams.get("organizationId") ?? void 0 });
|
|
54
|
+
const context = await resolveDictionaryRouteContext(req, { selectedId: query.organizationId });
|
|
55
|
+
if (!context.organizationId) {
|
|
56
|
+
return NextResponse.json({ error: context.translate("customers.errors.organization_required", "Organization context is required") }, { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
const organizationId = context.organizationId;
|
|
59
|
+
const rbac = context.container.resolve("rbacService");
|
|
60
|
+
const actorId = context.auth?.sub;
|
|
61
|
+
if (!rbac || !actorId) {
|
|
62
|
+
throw new CrudHttpError(403, { error: context.translate("customers.calendar.activityTypes.errors.forbidden", "You do not have permission to view activity types.") });
|
|
63
|
+
}
|
|
64
|
+
const permissionScope = { tenantId: context.tenantId, organizationId };
|
|
65
|
+
const canViewInteractions = await rbac.userHasAllFeatures(actorId, ["customers.interactions.view"], permissionScope);
|
|
66
|
+
const canManageSettings = canViewInteractions || await rbac.userHasAllFeatures(actorId, ["customers.settings.manage"], permissionScope);
|
|
67
|
+
if (!canManageSettings) {
|
|
68
|
+
throw new CrudHttpError(403, { error: context.translate("customers.calendar.activityTypes.errors.forbidden", "You do not have permission to view activity types.") });
|
|
69
|
+
}
|
|
70
|
+
const hierarchy = context.container.resolve("organizationHierarchyService");
|
|
71
|
+
const ancestors = await hierarchy.resolveAncestorIds({ tenantId: context.tenantId, organizationId });
|
|
72
|
+
if (ancestors === null) {
|
|
73
|
+
throw new CrudHttpError(403, { error: context.translate("customers.errors.organization_forbidden", "Organization not accessible") });
|
|
74
|
+
}
|
|
75
|
+
const scopeIds = Array.from(/* @__PURE__ */ new Set([organizationId, ...ancestors.slice().reverse()]));
|
|
76
|
+
const cacheKey = `customers:activity-types:${context.tenantId}:org=${organizationId}:scope=${scopeIds.join("|")}:widgets=${getInjectionRegistryVersion()}:registry=${getCalendarEventTypeRegistryVersion()}`;
|
|
77
|
+
const cached = await context.cache?.get(cacheKey);
|
|
78
|
+
if (cached) return NextResponse.json(cached);
|
|
79
|
+
let catalog;
|
|
80
|
+
try {
|
|
81
|
+
catalog = await resolveScopedCalendarEventTypes({
|
|
82
|
+
em: context.em,
|
|
83
|
+
tenantId: context.tenantId,
|
|
84
|
+
organizationId,
|
|
85
|
+
readableOrganizationIds: scopeIds
|
|
86
|
+
});
|
|
87
|
+
} catch (err) {
|
|
88
|
+
logger.error("customers.activity_types.resolve failed", {
|
|
89
|
+
err,
|
|
90
|
+
tenantId: context.tenantId,
|
|
91
|
+
organizationId
|
|
92
|
+
});
|
|
93
|
+
reportActivityTypeError(err, "customers.activity_type_catalog_resolution_failed", {
|
|
94
|
+
tenantId: context.tenantId,
|
|
95
|
+
organizationId
|
|
96
|
+
});
|
|
97
|
+
return NextResponse.json({
|
|
98
|
+
error: context.translate("customers.calendar.activityTypes.errors.loadFailed", "Failed to load activity types"),
|
|
99
|
+
code: "activity_type_catalog_unavailable"
|
|
100
|
+
}, { status: 503 });
|
|
101
|
+
}
|
|
102
|
+
if (context.cache) {
|
|
103
|
+
await context.cache.set(cacheKey, catalog, {
|
|
104
|
+
ttl: DICTIONARY_CACHE_TTL_MS,
|
|
105
|
+
tags: createDictionaryCacheTags({
|
|
106
|
+
tenantId: context.tenantId,
|
|
107
|
+
mappedKind: "activity_type",
|
|
108
|
+
organizationIds: scopeIds
|
|
109
|
+
})
|
|
110
|
+
}).catch((err) => {
|
|
111
|
+
logger.warn("customers.activity_types cache write failed", { err });
|
|
112
|
+
reportActivityTypeError(err, "customers.activity_type_catalog_cache_write_failed", {
|
|
113
|
+
tenantId: context.tenantId,
|
|
114
|
+
organizationId
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return NextResponse.json(catalog);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
|
|
121
|
+
logger.error("customers.activity_types request failed", { err });
|
|
122
|
+
reportActivityTypeError(err, "customers.activity_type_catalog_request_failed");
|
|
123
|
+
const { translate } = await resolveTranslations();
|
|
124
|
+
return NextResponse.json({
|
|
125
|
+
error: translate("customers.calendar.activityTypes.errors.loadFailed", "Failed to load activity types")
|
|
126
|
+
}, { status: err instanceof z.ZodError ? 400 : 500 });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const openApi = {
|
|
130
|
+
tag: "Customers",
|
|
131
|
+
summary: "Effective calendar activity types",
|
|
132
|
+
methods: {
|
|
133
|
+
GET: {
|
|
134
|
+
summary: "List effective calendar activity types",
|
|
135
|
+
description: "Returns the tenant and organization scoped calendar activity-type catalog. Requires customers.interactions.view or customers.settings.manage.",
|
|
136
|
+
responses: [{ status: 200, description: "Effective activity-type catalog", schema: responseSchema }],
|
|
137
|
+
errors: [
|
|
138
|
+
{ status: 400, description: "Organization scope is required", schema: z.object({ error: z.string() }) },
|
|
139
|
+
{ status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) },
|
|
140
|
+
{ status: 403, description: "Forbidden", schema: z.object({ error: z.string() }) },
|
|
141
|
+
{ status: 500, description: "Unexpected server error", schema: z.object({ error: z.string() }) }
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
export {
|
|
147
|
+
GET,
|
|
148
|
+
metadata,
|
|
149
|
+
openApi
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/customers/api/activity-types/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveDictionaryRouteContext } from '../dictionaries/context'\nimport { createDictionaryCacheTags, DICTIONARY_CACHE_TTL_MS } from '../dictionaries/cache'\nimport { getInjectionRegistryVersion } from '@open-mercato/shared/modules/widgets/injection-loader'\nimport {\n resolveScopedCalendarEventTypes,\n} from '../../lib/calendar/eventTypeResolver'\nimport { calendarEventTypeBehaviorSchema } from '../../calendar-event-types'\nimport { getCalendarEventTypeRegistryVersion } from '../../calendar-event-types'\n\nconst logger = createLogger('customers')\n\nfunction reportActivityTypeError(error: unknown, code: string, attributes?: Record<string, string>): void {\n try {\n getTelemetryRuntime()?.reportError(error, { module: 'customers', code, attributes })\n } catch {}\n}\n\nconst querySchema = z.object({ organizationId: z.string().uuid().optional() })\nconst responseSchema = z.object({\n fallbackKey: z.literal('meeting'),\n items: z.array(z.object({\n key: z.string(),\n label: z.string(),\n labelKey: z.string().optional(),\n icon: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n behavior: calendarEventTypeBehaviorSchema,\n selectable: z.boolean(),\n panelKey: z.string().optional(),\n source: z.string(),\n provenance: z.record(z.string(), z.unknown()),\n historical: z.boolean(),\n fallbackReason: z.enum(['tombstoned', 'module-unavailable', 'unknown']).optional(),\n adminConfigurable: z.boolean(),\n isInherited: z.boolean(),\n isLocalOverride: z.boolean(),\n updatedAt: z.string().nullable(),\n missingCustomFieldsetIds: z.array(z.string()),\n inactiveDictionaryOverride: z.boolean().optional(),\n })),\n})\n\nexport const metadata = {\n GET: { requireAuth: true },\n}\n\nexport async function GET(req: Request) {\n try {\n const url = new URL(req.url)\n const query = querySchema.parse({ organizationId: url.searchParams.get('organizationId') ?? undefined })\n const context = await resolveDictionaryRouteContext(req, { selectedId: query.organizationId })\n if (!context.organizationId) {\n return NextResponse.json({ error: context.translate('customers.errors.organization_required', 'Organization context is required') }, { status: 400 })\n }\n const organizationId = context.organizationId\n const rbac = context.container.resolve('rbacService') as RbacService | undefined\n const actorId = context.auth?.sub\n if (!rbac || !actorId) {\n throw new CrudHttpError(403, { error: context.translate('customers.calendar.activityTypes.errors.forbidden', 'You do not have permission to view activity types.') })\n }\n const permissionScope = { tenantId: context.tenantId, organizationId }\n const canViewInteractions = await rbac.userHasAllFeatures(actorId, ['customers.interactions.view'], permissionScope)\n const canManageSettings = canViewInteractions || await rbac.userHasAllFeatures(actorId, ['customers.settings.manage'], permissionScope)\n if (!canManageSettings) {\n throw new CrudHttpError(403, { error: context.translate('customers.calendar.activityTypes.errors.forbidden', 'You do not have permission to view activity types.') })\n }\n const hierarchy = context.container.resolve('organizationHierarchyService') as OrganizationHierarchyService\n const ancestors = await hierarchy.resolveAncestorIds({ tenantId: context.tenantId, organizationId })\n if (ancestors === null) {\n throw new CrudHttpError(403, { error: context.translate('customers.errors.organization_forbidden', 'Organization not accessible') })\n }\n const scopeIds = Array.from(new Set([organizationId, ...ancestors.slice().reverse()]))\n const cacheKey = `customers:activity-types:${context.tenantId}:org=${organizationId}:scope=${scopeIds.join('|')}:widgets=${getInjectionRegistryVersion()}:registry=${getCalendarEventTypeRegistryVersion()}`\n const cached = await context.cache?.get(cacheKey)\n if (cached) return NextResponse.json(cached)\n\n let catalog\n try {\n catalog = await resolveScopedCalendarEventTypes({\n em: context.em,\n tenantId: context.tenantId,\n organizationId,\n readableOrganizationIds: scopeIds,\n })\n } catch (err) {\n logger.error('customers.activity_types.resolve failed', {\n err,\n tenantId: context.tenantId,\n organizationId,\n })\n reportActivityTypeError(err, 'customers.activity_type_catalog_resolution_failed', {\n tenantId: context.tenantId,\n organizationId,\n })\n return NextResponse.json({\n error: context.translate('customers.calendar.activityTypes.errors.loadFailed', 'Failed to load activity types'),\n code: 'activity_type_catalog_unavailable',\n }, { status: 503 })\n }\n if (context.cache) {\n await context.cache.set(cacheKey, catalog, {\n ttl: DICTIONARY_CACHE_TTL_MS,\n tags: createDictionaryCacheTags({\n tenantId: context.tenantId,\n mappedKind: 'activity_type',\n organizationIds: scopeIds,\n }),\n }).catch((err) => {\n logger.warn('customers.activity_types cache write failed', { err })\n reportActivityTypeError(err, 'customers.activity_type_catalog_cache_write_failed', {\n tenantId: context.tenantId,\n organizationId,\n })\n })\n }\n return NextResponse.json(catalog)\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n logger.error('customers.activity_types request failed', { err })\n reportActivityTypeError(err, 'customers.activity_type_catalog_request_failed')\n const { translate } = await resolveTranslations()\n return NextResponse.json({\n error: translate('customers.calendar.activityTypes.errors.loadFailed', 'Failed to load activity types'),\n }, { status: err instanceof z.ZodError ? 400 : 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Effective calendar activity types',\n methods: {\n GET: {\n summary: 'List effective calendar activity types',\n description: 'Returns the tenant and organization scoped calendar activity-type catalog. Requires customers.interactions.view or customers.settings.manage.',\n responses: [{ status: 200, description: 'Effective activity-type catalog', schema: responseSchema }],\n errors: [\n { status: 400, description: 'Organization scope is required', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 500, description: 'Unexpected server error', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
15
15
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
16
16
|
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
17
|
+
import { calendarEventTypeBehaviorSchema } from "../../../../calendar-event-types.js";
|
|
17
18
|
const logger = createLogger("customers");
|
|
18
19
|
const paramsSchema = z.object({
|
|
19
20
|
id: z.string().uuid()
|
|
@@ -22,8 +23,9 @@ const patchSchema = z.object({
|
|
|
22
23
|
value: z.string().trim().min(1).max(150).optional(),
|
|
23
24
|
label: z.string().trim().max(150).optional(),
|
|
24
25
|
color: z.union([z.string().trim(), z.null()]).optional(),
|
|
25
|
-
icon: z.union([z.string().trim(), z.null()]).optional()
|
|
26
|
-
|
|
26
|
+
icon: z.union([z.string().trim(), z.null()]).optional(),
|
|
27
|
+
behavior: calendarEventTypeBehaviorSchema.nullable().optional()
|
|
28
|
+
}).refine((input) => input.value !== void 0 || input.label !== void 0 || input.color !== void 0 || input.icon !== void 0 || input.behavior !== void 0, {
|
|
27
29
|
message: "No changes provided"
|
|
28
30
|
});
|
|
29
31
|
const metadata = {
|
|
@@ -69,7 +71,8 @@ async function PATCH(req, ctx) {
|
|
|
69
71
|
value: payload.value,
|
|
70
72
|
label: payload.label,
|
|
71
73
|
color: payload.color,
|
|
72
|
-
icon: payload.icon
|
|
74
|
+
icon: payload.icon,
|
|
75
|
+
behavior: payload.behavior
|
|
73
76
|
},
|
|
74
77
|
ctx: routeContext.ctx
|
|
75
78
|
});
|
|
@@ -92,6 +95,7 @@ async function PATCH(req, ctx) {
|
|
|
92
95
|
)
|
|
93
96
|
});
|
|
94
97
|
}
|
|
98
|
+
if (typeof err.body.code === "string") throw err;
|
|
95
99
|
throw new CrudHttpError(409, { error: routeContext.translate("customers.config.dictionaries.errors.duplicate", "An entry with this value already exists") });
|
|
96
100
|
}
|
|
97
101
|
if (err.status === 400) {
|
|
@@ -127,8 +131,10 @@ async function PATCH(req, ctx) {
|
|
|
127
131
|
label: entry.label,
|
|
128
132
|
color: entry.color,
|
|
129
133
|
icon: entry.icon,
|
|
134
|
+
...mappedKind === "activity_type" ? { behavior: entry.activityTypeBehavior ?? null } : {},
|
|
130
135
|
organizationId: entry.organizationId,
|
|
131
|
-
isInherited: false
|
|
136
|
+
isInherited: false,
|
|
137
|
+
updatedAt: entry.updatedAt
|
|
132
138
|
});
|
|
133
139
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
134
140
|
response.headers.set(
|
|
@@ -279,8 +285,10 @@ const dictionaryEntrySchema = z.object({
|
|
|
279
285
|
label: z.string().nullable().optional(),
|
|
280
286
|
color: z.string().nullable().optional(),
|
|
281
287
|
icon: z.string().nullable().optional(),
|
|
288
|
+
behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
|
|
282
289
|
organizationId: z.string().uuid().nullable().optional(),
|
|
283
|
-
isInherited: z.boolean().optional()
|
|
290
|
+
isInherited: z.boolean().optional(),
|
|
291
|
+
updatedAt: z.date().or(z.string()).optional()
|
|
284
292
|
});
|
|
285
293
|
const dictionaryDeleteResponseSchema = z.object({
|
|
286
294
|
success: z.literal(true)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customers/api/dictionaries/%5Bkind%5D/%5Bid%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandExecuteResult } from '@open-mercato/shared/lib/commands/types'\nimport { CustomerDictionaryEntry } from '../../../../data/entities'\nimport { mapDictionaryKind, resolveDictionaryActorId, resolveDictionaryRouteContext } from '../../context'\nimport { invalidateDictionaryCache } from '../../cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst patchSchema = z\n .object({\n value: z.string().trim().min(1).max(150).optional(),\n label: z.string().trim().max(150).optional(),\n color: z.union([z.string().trim(), z.null()]).optional(),\n icon: z.union([z.string().trim(), z.null()]).optional(),\n })\n .refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined, {\n message: 'No changes provided',\n })\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\nfunction formatCountMessage(template: string, count: number) {\n return template.replace('{{count}}', String(count))\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { kind?: string; id?: string } }) {\n try {\n const routeContext = await resolveDictionaryRouteContext(req)\n if (!routeContext.organizationId) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const payload = patchSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(routeContext.auth)\n const guardResult = await validateCrudMutationGuard(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (routeContext.container.resolve('commandBus') as CommandBus)\n let commandResult: CommandExecuteResult<{ entryId: string; changed: boolean }>\n try {\n commandResult = (await commandBus.execute('customers.dictionaryEntries.update', {\n input: {\n id,\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n kind: mappedKind,\n value: payload.value,\n label: payload.label,\n color: payload.color,\n icon: payload.icon,\n },\n ctx: routeContext.ctx,\n })) as CommandExecuteResult<{ entryId: string; changed: boolean }>\n } catch (err) {\n if (isCrudHttpError(err)) {\n if (err.status === 404) {\n throw notFound(routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found'))\n }\n if (err.status === 409) {\n if ((err.body as Record<string, unknown> | undefined)?.code === 'role_type_in_use') {\n const usageCount = Number((err.body as Record<string, unknown> | undefined)?.usageCount ?? 0)\n throw new CrudHttpError(409, {\n ...(err.body as Record<string, unknown> | undefined),\n error: formatCountMessage(\n routeContext.translate(\n 'customers.config.dictionaries.errors.roleTypeValueInUse',\n 'This role type is assigned to {{count}} records. Remove or replace those assignments before changing its value.',\n ),\n usageCount,\n ),\n })\n }\n throw new CrudHttpError(409, { error: routeContext.translate('customers.config.dictionaries.errors.duplicate', 'An entry with this value already exists') })\n }\n if (err.status === 400) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') })\n }\n }\n throw err\n }\n const { result, logEntry } = commandResult\n\n const entry = await findOneWithDecryption(\n routeContext.em.fork(),\n CustomerDictionaryEntry,\n { id },\n {},\n {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n },\n )\n if (!entry) {\n throw notFound(routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found'))\n }\n\n if (result.changed) {\n await invalidateDictionaryCache(routeContext.cache, {\n tenantId: routeContext.tenantId,\n mappedKind,\n organizationIds: [routeContext.organizationId],\n })\n }\n\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n organizationId: entry.organizationId,\n isInherited: false,\n })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: entry.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.update failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') }, { status: 400 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { kind?: string; id?: string } }) {\n try {\n const routeContext = await resolveDictionaryRouteContext(req)\n if (!routeContext.organizationId) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const guardUserId = resolveDictionaryActorId(routeContext.auth)\n const guardResult = await validateCrudMutationGuard(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (routeContext.container.resolve('commandBus') as CommandBus)\n let deleteResult: CommandExecuteResult<{ entryId: string }>\n try {\n deleteResult = (await commandBus.execute('customers.dictionaryEntries.delete', {\n input: {\n id,\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n kind: mappedKind,\n },\n ctx: routeContext.ctx,\n })) as CommandExecuteResult<{ entryId: string }>\n } catch (err) {\n if (isCrudHttpError(err) && err.status === 404) {\n throw notFound(routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found'))\n }\n if (isCrudHttpError(err) && err.status === 409 && (err.body as Record<string, unknown> | undefined)?.code === 'role_type_in_use') {\n const usageCount = Number((err.body as Record<string, unknown> | undefined)?.usageCount ?? 0)\n throw new CrudHttpError(409, {\n ...(err.body as Record<string, unknown> | undefined),\n error: formatCountMessage(\n routeContext.translate(\n 'customers.config.dictionaries.errors.roleTypeDeleteInUse',\n 'This role type is assigned to {{count}} records. Remove or replace those assignments before deleting it.',\n ),\n usageCount,\n ),\n })\n }\n throw err\n }\n const { logEntry } = deleteResult\n\n await invalidateDictionaryCache(routeContext.cache, {\n tenantId: routeContext.tenantId,\n mappedKind,\n organizationIds: [routeContext.organizationId],\n })\n\n const response = NextResponse.json({ success: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.delete failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to delete dictionary entry') }, { status: 400 })\n }\n}\n\nconst dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n icon: z.string().nullable().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n isInherited: z.boolean().optional(),\n})\n\nconst dictionaryDeleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst dictionaryErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer dictionary entry',\n methods: {\n PATCH: {\n summary: 'Update dictionary entry',\n description: 'Updates value, label, color, or icon for an existing customer dictionary entry.',\n requestBody: {\n contentType: 'application/json',\n schema: patchSchema,\n },\n responses: [\n { status: 200, description: 'Updated dictionary entry', schema: dictionaryEntrySchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: dictionaryErrorSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 404, description: 'Entry not found', schema: dictionaryErrorSchema },\n { status: 409, description: 'Duplicate value conflict', schema: dictionaryErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete dictionary entry',\n description: 'Removes a customer dictionary entry by identifier.',\n responses: [\n { status: 200, description: 'Entry deleted', schema: dictionaryDeleteResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 404, description: 'Entry not found', schema: dictionaryErrorSchema },\n { status: 409, description: 'Entry is in use and cannot be deleted', schema: dictionaryErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,2BAA2B;AACpC,SAAS,eAAe,iBAAiB,gBAAgB;AAEzD,SAAS,kCAAkC;AAE3C,SAAS,+BAA+B;AACxC,SAAS,mBAAmB,0BAA0B,qCAAqC;AAC3F,SAAS,iCAAiC;AAE1C,SAAS,oBAAoB;AAC7B,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,0CAA0C;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandExecuteResult } from '@open-mercato/shared/lib/commands/types'\nimport { CustomerDictionaryEntry } from '../../../../data/entities'\nimport { mapDictionaryKind, resolveDictionaryActorId, resolveDictionaryRouteContext } from '../../context'\nimport { invalidateDictionaryCache } from '../../cache'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { calendarEventTypeBehaviorSchema } from '../../../../calendar-event-types'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst patchSchema = z\n .object({\n value: z.string().trim().min(1).max(150).optional(),\n label: z.string().trim().max(150).optional(),\n color: z.union([z.string().trim(), z.null()]).optional(),\n icon: z.union([z.string().trim(), z.null()]).optional(),\n behavior: calendarEventTypeBehaviorSchema.nullable().optional(),\n })\n .refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined || input.behavior !== undefined, {\n message: 'No changes provided',\n })\n\nexport const metadata = {\n PATCH: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\nfunction formatCountMessage(template: string, count: number) {\n return template.replace('{{count}}', String(count))\n}\n\nexport async function PATCH(req: Request, ctx: { params?: { kind?: string; id?: string } }) {\n try {\n const routeContext = await resolveDictionaryRouteContext(req)\n if (!routeContext.organizationId) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const payload = patchSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(routeContext.auth)\n const guardResult = await validateCrudMutationGuard(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: payload,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (routeContext.container.resolve('commandBus') as CommandBus)\n let commandResult: CommandExecuteResult<{ entryId: string; changed: boolean }>\n try {\n commandResult = (await commandBus.execute('customers.dictionaryEntries.update', {\n input: {\n id,\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n kind: mappedKind,\n value: payload.value,\n label: payload.label,\n color: payload.color,\n icon: payload.icon,\n behavior: payload.behavior,\n },\n ctx: routeContext.ctx,\n })) as CommandExecuteResult<{ entryId: string; changed: boolean }>\n } catch (err) {\n if (isCrudHttpError(err)) {\n if (err.status === 404) {\n throw notFound(routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found'))\n }\n if (err.status === 409) {\n if ((err.body as Record<string, unknown> | undefined)?.code === 'role_type_in_use') {\n const usageCount = Number((err.body as Record<string, unknown> | undefined)?.usageCount ?? 0)\n throw new CrudHttpError(409, {\n ...(err.body as Record<string, unknown> | undefined),\n error: formatCountMessage(\n routeContext.translate(\n 'customers.config.dictionaries.errors.roleTypeValueInUse',\n 'This role type is assigned to {{count}} records. Remove or replace those assignments before changing its value.',\n ),\n usageCount,\n ),\n })\n }\n if (typeof err.body.code === 'string') throw err\n throw new CrudHttpError(409, { error: routeContext.translate('customers.config.dictionaries.errors.duplicate', 'An entry with this value already exists') })\n }\n if (err.status === 400) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') })\n }\n }\n throw err\n }\n const { result, logEntry } = commandResult\n\n const entry = await findOneWithDecryption(\n routeContext.em.fork(),\n CustomerDictionaryEntry,\n { id },\n {},\n {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n },\n )\n if (!entry) {\n throw notFound(routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found'))\n }\n\n if (result.changed) {\n await invalidateDictionaryCache(routeContext.cache, {\n tenantId: routeContext.tenantId,\n mappedKind,\n organizationIds: [routeContext.organizationId],\n })\n }\n\n const response = NextResponse.json({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n ...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),\n organizationId: entry.organizationId,\n isInherited: false,\n updatedAt: entry.updatedAt,\n })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: entry.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.update failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') }, { status: 400 })\n }\n}\n\nexport async function DELETE(req: Request, ctx: { params?: { kind?: string; id?: string } }) {\n try {\n const routeContext = await resolveDictionaryRouteContext(req)\n if (!routeContext.organizationId) {\n throw new CrudHttpError(400, { error: routeContext.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const guardUserId = resolveDictionaryActorId(routeContext.auth)\n const guardResult = await validateCrudMutationGuard(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: null,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (routeContext.container.resolve('commandBus') as CommandBus)\n let deleteResult: CommandExecuteResult<{ entryId: string }>\n try {\n deleteResult = (await commandBus.execute('customers.dictionaryEntries.delete', {\n input: {\n id,\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n kind: mappedKind,\n },\n ctx: routeContext.ctx,\n })) as CommandExecuteResult<{ entryId: string }>\n } catch (err) {\n if (isCrudHttpError(err) && err.status === 404) {\n throw notFound(routeContext.translate('customers.errors.lookup_failed', 'Dictionary entry not found'))\n }\n if (isCrudHttpError(err) && err.status === 409 && (err.body as Record<string, unknown> | undefined)?.code === 'role_type_in_use') {\n const usageCount = Number((err.body as Record<string, unknown> | undefined)?.usageCount ?? 0)\n throw new CrudHttpError(409, {\n ...(err.body as Record<string, unknown> | undefined),\n error: formatCountMessage(\n routeContext.translate(\n 'customers.config.dictionaries.errors.roleTypeDeleteInUse',\n 'This role type is assigned to {{count}} records. Remove or replace those assignments before deleting it.',\n ),\n usageCount,\n ),\n })\n }\n throw err\n }\n const { logEntry } = deleteResult\n\n await invalidateDictionaryCache(routeContext.cache, {\n tenantId: routeContext.tenantId,\n mappedKind,\n organizationIds: [routeContext.organizationId],\n })\n\n const response = NextResponse.json({ success: true })\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(routeContext.container, {\n tenantId: routeContext.tenantId,\n organizationId: routeContext.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: id,\n operation: 'delete',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.delete failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to delete dictionary entry') }, { status: 400 })\n }\n}\n\nconst dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n icon: z.string().nullable().optional(),\n behavior: calendarEventTypeBehaviorSchema.nullable().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n isInherited: z.boolean().optional(),\n updatedAt: z.date().or(z.string()).optional(),\n})\n\nconst dictionaryDeleteResponseSchema = z.object({\n success: z.literal(true),\n})\n\nconst dictionaryErrorSchema = z.object({\n error: z.string(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer dictionary entry',\n methods: {\n PATCH: {\n summary: 'Update dictionary entry',\n description: 'Updates value, label, color, or icon for an existing customer dictionary entry.',\n requestBody: {\n contentType: 'application/json',\n schema: patchSchema,\n },\n responses: [\n { status: 200, description: 'Updated dictionary entry', schema: dictionaryEntrySchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: dictionaryErrorSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 404, description: 'Entry not found', schema: dictionaryErrorSchema },\n { status: 409, description: 'Duplicate value conflict', schema: dictionaryErrorSchema },\n ],\n },\n DELETE: {\n summary: 'Delete dictionary entry',\n description: 'Removes a customer dictionary entry by identifier.',\n responses: [\n { status: 200, description: 'Entry deleted', schema: dictionaryDeleteResponseSchema },\n ],\n errors: [\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 404, description: 'Entry not found', schema: dictionaryErrorSchema },\n { status: 409, description: 'Entry is in use and cannot be deleted', schema: dictionaryErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -21,6 +21,7 @@ import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
|
21
21
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
22
22
|
import { CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE } from "../../../lib/dictionaries.js";
|
|
23
23
|
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
24
|
+
import { calendarEventTypeBehaviorSchema } from "../../../calendar-event-types.js";
|
|
24
25
|
const logger = createLogger("customers");
|
|
25
26
|
const colorSchema = z.string().trim().regex(/^#([0-9A-Fa-f]{6})$/, "Invalid color hex");
|
|
26
27
|
const iconSchema = z.string().trim().min(1).max(48);
|
|
@@ -28,7 +29,8 @@ const postSchema = z.object({
|
|
|
28
29
|
value: z.string().trim().min(1).max(150),
|
|
29
30
|
label: z.string().trim().max(150).optional(),
|
|
30
31
|
color: colorSchema.or(z.null()).optional(),
|
|
31
|
-
icon: iconSchema.or(z.null()).optional()
|
|
32
|
+
icon: iconSchema.or(z.null()).optional(),
|
|
33
|
+
behavior: calendarEventTypeBehaviorSchema.nullable().optional()
|
|
32
34
|
});
|
|
33
35
|
const querySchema = z.object({
|
|
34
36
|
organizationId: z.string().uuid().optional()
|
|
@@ -43,7 +45,7 @@ async function GET(req, ctx) {
|
|
|
43
45
|
const query = querySchema.parse({
|
|
44
46
|
organizationId: url.searchParams.get("organizationId") ?? void 0
|
|
45
47
|
});
|
|
46
|
-
const { translate, em, organizationId, readableOrganizationIds, tenantId, cache } = await resolveDictionaryRouteContext(req, {
|
|
48
|
+
const { translate, em, organizationId, readableOrganizationIds, tenantId, cache, container } = await resolveDictionaryRouteContext(req, {
|
|
47
49
|
selectedId: query.organizationId ?? void 0
|
|
48
50
|
});
|
|
49
51
|
const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind);
|
|
@@ -55,7 +57,15 @@ async function GET(req, ctx) {
|
|
|
55
57
|
}
|
|
56
58
|
const settings = await loadCustomerSettings(em, { tenantId, organizationId });
|
|
57
59
|
const sortMode = resolveDictionaryEntrySortMode(settings?.dictionarySortModes?.[kind]);
|
|
58
|
-
|
|
60
|
+
let scopedOrganizationIds = readableOrganizationIds.length > 0 ? readableOrganizationIds : [organizationId];
|
|
61
|
+
if (mappedKind === "activity_type") {
|
|
62
|
+
const hierarchy = container.resolve("organizationHierarchyService");
|
|
63
|
+
const ancestors = await hierarchy.resolveAncestorIds({ tenantId, organizationId });
|
|
64
|
+
if (ancestors === null) {
|
|
65
|
+
throw new CrudHttpError(403, { error: translate("customers.errors.organization_forbidden", "Organization not accessible") });
|
|
66
|
+
}
|
|
67
|
+
scopedOrganizationIds = Array.from(/* @__PURE__ */ new Set([organizationId, ...ancestors.slice().reverse()]));
|
|
68
|
+
}
|
|
59
69
|
const canUseCache = Boolean(cache) && mappedKind !== "person_company_role";
|
|
60
70
|
let cacheKey = null;
|
|
61
71
|
if (canUseCache && cache) {
|
|
@@ -108,6 +118,7 @@ async function GET(req, ctx) {
|
|
|
108
118
|
label: entry.label,
|
|
109
119
|
color: entry.color,
|
|
110
120
|
icon: entry.icon,
|
|
121
|
+
...mappedKind === "activity_type" ? { behavior: entry.activityTypeBehavior ?? null } : {},
|
|
111
122
|
organizationId: entry.organizationId,
|
|
112
123
|
isInherited: entry.organizationId !== organizationId,
|
|
113
124
|
createdAt: entry.createdAt,
|
|
@@ -179,7 +190,8 @@ async function POST(req, ctx) {
|
|
|
179
190
|
value: body.value,
|
|
180
191
|
label: body.label,
|
|
181
192
|
color: body.color,
|
|
182
|
-
icon: body.icon
|
|
193
|
+
icon: body.icon,
|
|
194
|
+
behavior: body.behavior
|
|
183
195
|
},
|
|
184
196
|
ctx: context.ctx
|
|
185
197
|
});
|
|
@@ -212,8 +224,10 @@ async function POST(req, ctx) {
|
|
|
212
224
|
label: entry.label,
|
|
213
225
|
color: entry.color,
|
|
214
226
|
icon: entry.icon,
|
|
227
|
+
...mappedKind === "activity_type" ? { behavior: entry.activityTypeBehavior ?? null } : {},
|
|
215
228
|
organizationId: entry.organizationId,
|
|
216
|
-
isInherited: false
|
|
229
|
+
isInherited: false,
|
|
230
|
+
updatedAt: entry.updatedAt
|
|
217
231
|
},
|
|
218
232
|
{ status: result.mode === "created" ? 201 : 200 }
|
|
219
233
|
);
|
|
@@ -251,6 +265,7 @@ const dictionaryEntrySchema = z.object({
|
|
|
251
265
|
label: z.string().nullable().optional(),
|
|
252
266
|
color: z.string().nullable().optional(),
|
|
253
267
|
icon: z.string().nullable().optional(),
|
|
268
|
+
behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
|
|
254
269
|
organizationId: z.string().uuid().nullable().optional(),
|
|
255
270
|
isInherited: z.boolean().optional(),
|
|
256
271
|
createdAt: z.date().or(z.string()).optional(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/customers/api/dictionaries/%5Bkind%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { CommandExecuteResult } from '@open-mercato/shared/lib/commands/types'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { CustomerDictionaryEntry } from '../../../data/entities'\nimport { mapDictionaryKind, resolveDictionaryActorId, resolveDictionaryRouteContext } from '../context'\nimport { createDictionaryCacheKey, createDictionaryCacheTags, invalidateDictionaryCache, DICTIONARY_CACHE_TTL_MS } from '../cache'\nimport { loadCustomerSettings } from '../../../commands/settings'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { loadRoleTypeUsageMap, resolveRoleTypeUsageKey } from '../../../lib/roleTypeUsage'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE } from '../../../lib/dictionaries'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\n\nconst logger = createLogger('customers')\n\nconst colorSchema = z.string().trim().regex(/^#([0-9A-Fa-f]{6})$/, 'Invalid color hex')\nconst iconSchema = z.string().trim().min(1).max(48)\n\nconst postSchema = z.object({\n value: z.string().trim().min(1).max(150),\n label: z.string().trim().max(150).optional(),\n color: colorSchema.or(z.null()).optional(),\n icon: iconSchema.or(z.null()).optional(),\n})\n\nconst querySchema = z.object({\n organizationId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { kind?: string } }) {\n try {\n const url = new URL(req.url)\n const query = querySchema.parse({\n organizationId: url.searchParams.get('organizationId') ?? undefined,\n })\n const { translate, em, organizationId, readableOrganizationIds, tenantId, cache } = await resolveDictionaryRouteContext(req, {\n selectedId: query.organizationId ?? undefined,\n })\n const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind)\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('customers.errors.organization_required', 'Organization context is required'),\n code: CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE,\n })\n }\n const settings = await loadCustomerSettings(em, { tenantId, organizationId })\n const sortMode = resolveDictionaryEntrySortMode(settings?.dictionarySortModes?.[kind])\n const scopedOrganizationIds = readableOrganizationIds.length > 0 ? readableOrganizationIds : [organizationId]\n const canUseCache = Boolean(cache) && mappedKind !== 'person_company_role'\n\n let cacheKey: string | null = null\n if (canUseCache && cache) {\n cacheKey = createDictionaryCacheKey({\n tenantId,\n organizationId,\n mappedKind,\n sortMode,\n readableOrganizationIds: scopedOrganizationIds,\n })\n const cached = await cache.get(cacheKey)\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const entries = await findWithDecryption(\n em,\n CustomerDictionaryEntry,\n { tenantId, kind: mappedKind, organizationId: { $in: scopedOrganizationIds } } as any,\n { orderBy: { label: 'asc' } },\n { tenantId, organizationId },\n )\n\n const inheritedPriority = new Map(scopedOrganizationIds.map((id, index) => [id, index]))\n const sortByLabel = (left: CustomerDictionaryEntry, right: CustomerDictionaryEntry) =>\n left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n\n const localEntries = entries\n .filter((entry) => entry.organizationId === organizationId)\n .sort(sortByLabel)\n const inheritedEntries = entries\n .filter((entry) => entry.organizationId !== organizationId)\n .sort((left, right) => {\n const leftPriority = inheritedPriority.get(left.organizationId) ?? Number.MAX_SAFE_INTEGER\n const rightPriority = inheritedPriority.get(right.organizationId) ?? Number.MAX_SAFE_INTEGER\n if (leftPriority !== rightPriority) return leftPriority - rightPriority\n return sortByLabel(left, right)\n })\n\n const preferredEntries = new Map<string, CustomerDictionaryEntry>()\n for (const entry of [...localEntries, ...inheritedEntries]) {\n const normalizedValue = entry.normalizedValue?.trim() || entry.value.trim().toLowerCase()\n if (!normalizedValue || preferredEntries.has(normalizedValue)) continue\n preferredEntries.set(normalizedValue, entry)\n }\n\n const preferredEntryList = Array.from(preferredEntries.values())\n const usageByEntryKey =\n mappedKind === 'person_company_role'\n ? await loadRoleTypeUsageMap(em, {\n tenantId,\n entries: preferredEntryList.map((entry) => ({\n organizationId: entry.organizationId,\n value: entry.value,\n })),\n })\n : new Map()\n\n const items = sortDictionaryEntries(\n preferredEntryList\n .map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n organizationId: entry.organizationId,\n isInherited: entry.organizationId !== organizationId,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n ...(mappedKind === 'person_company_role'\n ? {\n usageCount:\n usageByEntryKey.get(resolveRoleTypeUsageKey(entry.organizationId, entry.value))?.total ?? 0,\n }\n : {}),\n })),\n sortMode,\n )\n\n const responseBody = {\n sortMode,\n items,\n }\n\n if (canUseCache && cache && cacheKey) {\n const tags = createDictionaryCacheTags({\n tenantId,\n mappedKind,\n organizationIds: scopedOrganizationIds,\n })\n try {\n await cache.set(cacheKey, responseBody, {\n ttl: DICTIONARY_CACHE_TTL_MS,\n tags,\n })\n } catch (err) {\n logger.warn('Failed to set cache entry', { component: 'dictionaries.cache', err })\n }\n }\n\n return NextResponse.json(responseBody)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.list failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to load dictionary entries') }, { status: 400 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { kind?: string } }) {\n try {\n const context = await resolveDictionaryRouteContext(req)\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const body = postSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: '',\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } =\n (await commandBus.execute('customers.dictionaryEntries.create', {\n input: {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n kind: mappedKind,\n value: body.value,\n label: body.label,\n color: body.color,\n icon: body.icon,\n },\n ctx: context.ctx,\n })) as CommandExecuteResult<{ entryId: string; mode: 'created' | 'updated' | 'unchanged' }>\n const entry = await findOneWithDecryption(context.em.fork(), CustomerDictionaryEntry, result.entryId, {}, { tenantId: context.tenantId, organizationId: context.organizationId })\n if (!entry) {\n throw new CrudHttpError(400, { error: context.translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') })\n }\n\n await invalidateDictionaryCache(context.cache, {\n tenantId: context.tenantId,\n mappedKind,\n organizationIds: [entry.organizationId],\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: entry.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = NextResponse.json(\n {\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n organizationId: entry.organizationId,\n isInherited: false,\n },\n { status: result.mode === 'created' ? 201 : 200 }\n )\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.create failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') }, { status: 400 })\n }\n}\n\nconst dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n icon: z.string().nullable().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n isInherited: z.boolean().optional(),\n createdAt: z.date().or(z.string()).optional(),\n updatedAt: z.date().or(z.string()).optional(),\n})\n\nconst dictionaryListResponseSchema = z.object({\n sortMode: z.string().optional(),\n items: z.array(dictionaryEntrySchema),\n})\n\nconst dictionaryErrorSchema = z.object({\n error: z.string(),\n code: z.string().optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer dictionary entries',\n methods: {\n GET: {\n summary: 'List dictionary entries',\n description: 'Returns dictionary entries for the requested kind within the currently selected organization.',\n responses: [\n { status: 200, description: 'Dictionary entries', schema: dictionaryListResponseSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 400, description: 'Failed to resolve dictionary context', schema: dictionaryErrorSchema },\n ],\n },\n POST: {\n summary: 'Create or override dictionary entry',\n description: 'Creates a dictionary entry (or updates the existing entry for the same value) within the current organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: postSchema,\n },\n responses: [\n { status: 201, description: 'Dictionary entry created', schema: dictionaryEntrySchema },\n { status: 200, description: 'Dictionary entry updated', schema: dictionaryEntrySchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: dictionaryErrorSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 409, description: 'Duplicate value conflict', schema: dictionaryErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,eAAe,uBAAuB;AAG/C,SAAS,kCAAkC;AAC3C,SAAS,+BAA+B;AACxC,SAAS,mBAAmB,0BAA0B,qCAAqC;AAC3F,SAAS,0BAA0B,2BAA2B,2BAA2B,+BAA+B;AACxH,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS;AAElB,SAAS,sBAAsB,+BAA+B;AAC9D;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB,6BAA6B;AAC1D,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,sDAAsD;AAC/D,SAAS,0CAA0C;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport type { CommandExecuteResult } from '@open-mercato/shared/lib/commands/types'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport { CustomerDictionaryEntry } from '../../../data/entities'\nimport { mapDictionaryKind, resolveDictionaryActorId, resolveDictionaryRouteContext } from '../context'\nimport { createDictionaryCacheKey, createDictionaryCacheTags, invalidateDictionaryCache, DICTIONARY_CACHE_TTL_MS } from '../cache'\nimport { loadCustomerSettings } from '../../../commands/settings'\nimport {\n resolveDictionaryEntrySortMode,\n sortDictionaryEntries,\n} from '@open-mercato/core/modules/dictionaries/lib/entrySort'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { loadRoleTypeUsageMap, resolveRoleTypeUsageKey } from '../../../lib/roleTypeUsage'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE } from '../../../lib/dictionaries'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { calendarEventTypeBehaviorSchema } from '../../../calendar-event-types'\nimport type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'\n\nconst logger = createLogger('customers')\n\nconst colorSchema = z.string().trim().regex(/^#([0-9A-Fa-f]{6})$/, 'Invalid color hex')\nconst iconSchema = z.string().trim().min(1).max(48)\n\nconst postSchema = z.object({\n value: z.string().trim().min(1).max(150),\n label: z.string().trim().max(150).optional(),\n color: colorSchema.or(z.null()).optional(),\n icon: iconSchema.or(z.null()).optional(),\n behavior: calendarEventTypeBehaviorSchema.nullable().optional(),\n})\n\nconst querySchema = z.object({\n organizationId: z.string().uuid().optional(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.settings.manage'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { kind?: string } }) {\n try {\n const url = new URL(req.url)\n const query = querySchema.parse({\n organizationId: url.searchParams.get('organizationId') ?? undefined,\n })\n const { translate, em, organizationId, readableOrganizationIds, tenantId, cache, container } = await resolveDictionaryRouteContext(req, {\n selectedId: query.organizationId ?? undefined,\n })\n const { kind, mappedKind } = mapDictionaryKind(ctx.params?.kind)\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('customers.errors.organization_required', 'Organization context is required'),\n code: CUSTOMER_DICTIONARY_ORGANIZATION_REQUIRED_CODE,\n })\n }\n const settings = await loadCustomerSettings(em, { tenantId, organizationId })\n const sortMode = resolveDictionaryEntrySortMode(settings?.dictionarySortModes?.[kind])\n let scopedOrganizationIds = readableOrganizationIds.length > 0 ? readableOrganizationIds : [organizationId]\n if (mappedKind === 'activity_type') {\n const hierarchy = container.resolve('organizationHierarchyService') as OrganizationHierarchyService\n const ancestors = await hierarchy.resolveAncestorIds({ tenantId, organizationId })\n if (ancestors === null) {\n throw new CrudHttpError(403, { error: translate('customers.errors.organization_forbidden', 'Organization not accessible') })\n }\n scopedOrganizationIds = Array.from(new Set([organizationId, ...ancestors.slice().reverse()]))\n }\n const canUseCache = Boolean(cache) && mappedKind !== 'person_company_role'\n\n let cacheKey: string | null = null\n if (canUseCache && cache) {\n cacheKey = createDictionaryCacheKey({\n tenantId,\n organizationId,\n mappedKind,\n sortMode,\n readableOrganizationIds: scopedOrganizationIds,\n })\n const cached = await cache.get(cacheKey)\n if (cached) {\n return NextResponse.json(cached)\n }\n }\n\n const entries = await findWithDecryption(\n em,\n CustomerDictionaryEntry,\n { tenantId, kind: mappedKind, organizationId: { $in: scopedOrganizationIds } } as any,\n { orderBy: { label: 'asc' } },\n { tenantId, organizationId },\n )\n\n const inheritedPriority = new Map(scopedOrganizationIds.map((id, index) => [id, index]))\n const sortByLabel = (left: CustomerDictionaryEntry, right: CustomerDictionaryEntry) =>\n left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n\n const localEntries = entries\n .filter((entry) => entry.organizationId === organizationId)\n .sort(sortByLabel)\n const inheritedEntries = entries\n .filter((entry) => entry.organizationId !== organizationId)\n .sort((left, right) => {\n const leftPriority = inheritedPriority.get(left.organizationId) ?? Number.MAX_SAFE_INTEGER\n const rightPriority = inheritedPriority.get(right.organizationId) ?? Number.MAX_SAFE_INTEGER\n if (leftPriority !== rightPriority) return leftPriority - rightPriority\n return sortByLabel(left, right)\n })\n\n const preferredEntries = new Map<string, CustomerDictionaryEntry>()\n for (const entry of [...localEntries, ...inheritedEntries]) {\n const normalizedValue = entry.normalizedValue?.trim() || entry.value.trim().toLowerCase()\n if (!normalizedValue || preferredEntries.has(normalizedValue)) continue\n preferredEntries.set(normalizedValue, entry)\n }\n\n const preferredEntryList = Array.from(preferredEntries.values())\n const usageByEntryKey =\n mappedKind === 'person_company_role'\n ? await loadRoleTypeUsageMap(em, {\n tenantId,\n entries: preferredEntryList.map((entry) => ({\n organizationId: entry.organizationId,\n value: entry.value,\n })),\n })\n : new Map()\n\n const items = sortDictionaryEntries(\n preferredEntryList\n .map((entry) => ({\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n ...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),\n organizationId: entry.organizationId,\n isInherited: entry.organizationId !== organizationId,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n ...(mappedKind === 'person_company_role'\n ? {\n usageCount:\n usageByEntryKey.get(resolveRoleTypeUsageKey(entry.organizationId, entry.value))?.total ?? 0,\n }\n : {}),\n })),\n sortMode,\n )\n\n const responseBody = {\n sortMode,\n items,\n }\n\n if (canUseCache && cache && cacheKey) {\n const tags = createDictionaryCacheTags({\n tenantId,\n mappedKind,\n organizationIds: scopedOrganizationIds,\n })\n try {\n await cache.set(cacheKey, responseBody, {\n ttl: DICTIONARY_CACHE_TTL_MS,\n tags,\n })\n } catch (err) {\n logger.warn('Failed to set cache entry', { component: 'dictionaries.cache', err })\n }\n }\n\n return NextResponse.json(responseBody)\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.list failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to load dictionary entries') }, { status: 400 })\n }\n}\n\nexport async function POST(req: Request, ctx: { params?: { kind?: string } }) {\n try {\n const context = await resolveDictionaryRouteContext(req)\n if (!context.organizationId) {\n throw new CrudHttpError(400, { error: context.translate('customers.errors.organization_required', 'Organization context is required') })\n }\n const { mappedKind } = mapDictionaryKind(ctx.params?.kind)\n const body = postSchema.parse(await readJsonSafe(req, {}))\n const guardUserId = resolveDictionaryActorId(context.auth)\n const guardResult = await validateCrudMutationGuard(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: '',\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: body,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n const commandBus = (context.container.resolve('commandBus') as CommandBus)\n const { result, logEntry } =\n (await commandBus.execute('customers.dictionaryEntries.create', {\n input: {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n kind: mappedKind,\n value: body.value,\n label: body.label,\n color: body.color,\n icon: body.icon,\n behavior: body.behavior,\n },\n ctx: context.ctx,\n })) as CommandExecuteResult<{ entryId: string; mode: 'created' | 'updated' | 'unchanged' }>\n const entry = await findOneWithDecryption(context.em.fork(), CustomerDictionaryEntry, result.entryId, {}, { tenantId: context.tenantId, organizationId: context.organizationId })\n if (!entry) {\n throw new CrudHttpError(400, { error: context.translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') })\n }\n\n await invalidateDictionaryCache(context.cache, {\n tenantId: context.tenantId,\n mappedKind,\n organizationIds: [entry.organizationId],\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(context.container, {\n tenantId: context.tenantId,\n organizationId: context.organizationId,\n userId: guardUserId,\n resourceKind: 'customers.dictionary_entry',\n resourceId: entry.id,\n operation: 'create',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n const response = NextResponse.json(\n {\n id: entry.id,\n value: entry.value,\n label: entry.label,\n color: entry.color,\n icon: entry.icon,\n ...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),\n organizationId: entry.organizationId,\n isInherited: false,\n updatedAt: entry.updatedAt,\n },\n { status: result.mode === 'created' ? 201 : 200 }\n )\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n response.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'customers.dictionary_entry',\n resourceId: entry.id,\n executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,\n })\n )\n }\n return response\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('customers.dictionaries.create failed', { err })\n return NextResponse.json({ error: translate('customers.errors.lookup_failed', 'Failed to save dictionary entry') }, { status: 400 })\n }\n}\n\nconst dictionaryEntrySchema = z.object({\n id: z.string().uuid(),\n value: z.string(),\n label: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n icon: z.string().nullable().optional(),\n behavior: calendarEventTypeBehaviorSchema.nullable().optional(),\n organizationId: z.string().uuid().nullable().optional(),\n isInherited: z.boolean().optional(),\n createdAt: z.date().or(z.string()).optional(),\n updatedAt: z.date().or(z.string()).optional(),\n})\n\nconst dictionaryListResponseSchema = z.object({\n sortMode: z.string().optional(),\n items: z.array(dictionaryEntrySchema),\n})\n\nconst dictionaryErrorSchema = z.object({\n error: z.string(),\n code: z.string().optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n summary: 'Customer dictionary entries',\n methods: {\n GET: {\n summary: 'List dictionary entries',\n description: 'Returns dictionary entries for the requested kind within the currently selected organization.',\n responses: [\n { status: 200, description: 'Dictionary entries', schema: dictionaryListResponseSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 400, description: 'Failed to resolve dictionary context', schema: dictionaryErrorSchema },\n ],\n },\n POST: {\n summary: 'Create or override dictionary entry',\n description: 'Creates a dictionary entry (or updates the existing entry for the same value) within the current organization scope.',\n requestBody: {\n contentType: 'application/json',\n schema: postSchema,\n },\n responses: [\n { status: 201, description: 'Dictionary entry created', schema: dictionaryEntrySchema },\n { status: 200, description: 'Dictionary entry updated', schema: dictionaryEntrySchema },\n ],\n errors: [\n { status: 400, description: 'Invalid payload', schema: dictionaryErrorSchema },\n { status: 401, description: 'Unauthorized', schema: dictionaryErrorSchema },\n { status: 409, description: 'Duplicate value conflict', schema: dictionaryErrorSchema },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -226,6 +226,7 @@ const INTERACTION_LIST_COLUMNS = [
|
|
|
226
226
|
"appearance_icon",
|
|
227
227
|
"appearance_color",
|
|
228
228
|
"source",
|
|
229
|
+
"timezone",
|
|
229
230
|
"duration_minutes",
|
|
230
231
|
"location",
|
|
231
232
|
"all_day",
|
|
@@ -520,6 +521,7 @@ async function GET(req) {
|
|
|
520
521
|
source: row.source ?? null,
|
|
521
522
|
duration: row.duration_minutes ?? null,
|
|
522
523
|
durationMinutes: row.duration_minutes ?? null,
|
|
524
|
+
timezone: row.timezone ?? null,
|
|
523
525
|
location: row.location ?? null,
|
|
524
526
|
allDay: row.all_day ?? null,
|
|
525
527
|
recurrenceRule: row.recurrence_rule ?? null,
|
|
@@ -596,6 +598,7 @@ const interactionListItemSchema = z.object({
|
|
|
596
598
|
source: z.string().nullable().optional(),
|
|
597
599
|
duration: z.number().nullable().optional(),
|
|
598
600
|
durationMinutes: z.number().nullable().optional(),
|
|
601
|
+
timezone: z.string().nullable().optional(),
|
|
599
602
|
location: z.string().nullable().optional(),
|
|
600
603
|
allDay: z.boolean().nullable().optional(),
|
|
601
604
|
recurrenceRule: z.string().nullable().optional(),
|