@open-mercato/core 0.9.0 → 0.9.1-develop.7462.1.94c09f158f
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/attachments/api/route.js +29 -3
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/lib/ocrLimits.js +49 -0
- package/dist/modules/attachments/lib/ocrLimits.js.map +7 -0
- package/dist/modules/attachments/lib/ocrQueue.js +76 -2
- package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
- package/dist/modules/attachments/lib/ocrService.js +4 -0
- package/dist/modules/attachments/lib/ocrService.js.map +2 -2
- package/dist/modules/attachments/lib/pdfProcessing.js +14 -2
- package/dist/modules/attachments/lib/pdfProcessing.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +4 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/textExtraction.js +13 -1
- package/dist/modules/attachments/lib/textExtraction.js.map +2 -2
- package/dist/modules/catalog/commands/categories.js +1 -1
- package/dist/modules/catalog/commands/categories.js.map +2 -2
- package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
- package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +52 -17
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/search.js +3 -1
- package/dist/modules/catalog/search.js.map +2 -2
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js +2 -1
- package/dist/modules/communication_channels/api/get/oauth/[provider]/callback/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +10 -7
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
- package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
- package/dist/modules/communication_channels/lib/oauth-result.js +12 -0
- package/dist/modules/communication_channels/lib/oauth-result.js.map +7 -0
- package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
- package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
- package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
- package/dist/modules/customers/api/activity-types/route.js +151 -0
- package/dist/modules/customers/api/activity-types/route.js.map +7 -0
- package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -0
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/calendar-event-types.js +548 -0
- package/dist/modules/customers/calendar-event-types.js.map +7 -0
- package/dist/modules/customers/commands/dictionaries.js +70 -10
- package/dist/modules/customers/commands/dictionaries.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +187 -22
- package/dist/modules/customers/commands/interactions.js.map +3 -3
- package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
- package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
- package/dist/modules/customers/components/DictionarySettings.js +3 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
- package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
- package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
- package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
- package/dist/modules/customers/components/calendar/types.js +1 -0
- package/dist/modules/customers/components/calendar/types.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
- package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +7 -3
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
- package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonCard.js +5 -2
- package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
- package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
- package/dist/modules/customers/data/entities.js +6 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +39 -10
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/di.js +20 -1
- package/dist/modules/customers/di.js.map +2 -2
- package/dist/modules/customers/extension-points.js +13 -0
- package/dist/modules/customers/extension-points.js.map +2 -2
- package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
- package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
- package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
- package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
- package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
- package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
- package/dist/modules/customers/lib/calendar/timezone.js +111 -0
- package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
- package/dist/modules/customers/lib/interactionReadModel.js +1 -0
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +1 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
- package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
- package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
- package/dist/modules/messages/subscribers/message-notification.js +13 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/planner/lib/availabilityMerge.js +101 -38
- package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
- package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/generated/entities/customer_dictionary_entry/index.ts +1 -0
- package/generated/entities/customer_interaction/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +9 -8
- package/src/modules/attachments/api/route.ts +36 -3
- package/src/modules/attachments/lib/ocrLimits.ts +62 -0
- package/src/modules/attachments/lib/ocrQueue.ts +95 -2
- package/src/modules/attachments/lib/ocrService.ts +4 -0
- package/src/modules/attachments/lib/pdfProcessing.ts +14 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +4 -2
- package/src/modules/attachments/lib/textExtraction.ts +14 -1
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/catalog/commands/categories.ts +1 -1
- package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
- package/src/modules/catalog/commands/products.ts +58 -17
- package/src/modules/catalog/search.ts +5 -1
- package/src/modules/communication_channels/api/get/oauth/[provider]/callback/route.ts +2 -1
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +12 -9
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
- package/src/modules/communication_channels/i18n/de.json +4 -1
- package/src/modules/communication_channels/i18n/en.json +4 -1
- package/src/modules/communication_channels/i18n/es.json +4 -1
- package/src/modules/communication_channels/i18n/ko.json +4 -1
- package/src/modules/communication_channels/i18n/pl.json +4 -1
- package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
- package/src/modules/communication_channels/lib/oauth-result.ts +13 -0
- package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
- package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
- package/src/modules/customers/api/activity-types/route.ts +154 -0
- package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
- package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
- package/src/modules/customers/api/interactions/route.ts +4 -0
- package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
- package/src/modules/customers/calendar-event-types.ts +674 -0
- package/src/modules/customers/commands/dictionaries.ts +84 -8
- package/src/modules/customers/commands/interactions.ts +244 -21
- package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
- package/src/modules/customers/components/DictionarySettings.tsx +5 -3
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
- package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
- package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
- package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
- package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
- package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
- package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
- package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
- package/src/modules/customers/components/detail/DealForm.tsx +7 -3
- package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
- package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
- package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
- package/src/modules/customers/components/detail/types.ts +1 -0
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
- package/src/modules/customers/data/entities.ts +8 -1
- package/src/modules/customers/data/validators.ts +42 -7
- package/src/modules/customers/di.ts +21 -0
- package/src/modules/customers/extension-points.ts +13 -0
- package/src/modules/customers/i18n/de.json +104 -8
- package/src/modules/customers/i18n/en.json +104 -8
- package/src/modules/customers/i18n/es.json +104 -8
- package/src/modules/customers/i18n/ko.json +104 -8
- package/src/modules/customers/i18n/pl.json +104 -8
- package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
- package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
- package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
- package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
- package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
- package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
- package/src/modules/customers/lib/calendar/timezone.ts +117 -0
- package/src/modules/customers/lib/interactionReadModel.ts +1 -0
- package/src/modules/customers/lib/todoCompatibility.ts +2 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
- package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
- package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
- package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
- package/src/modules/messages/subscribers/message-notification.ts +13 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
- package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
- package/src/modules/planner/lib/availabilityMerge.ts +106 -41
- package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
- package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
- package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
|
@@ -8,7 +8,11 @@ import { resolveSingleOrganizationIdOrDeny } from "@open-mercato/core/modules/di
|
|
|
8
8
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
9
9
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
10
10
|
import { isOrganizationReadAccessAllowed } from "@open-mercato/core/modules/directory/utils/organizationScopeGuard";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
CustomerDeal,
|
|
13
|
+
CustomerDealPersonLink,
|
|
14
|
+
CustomerPersonProfile
|
|
15
|
+
} from "../../../../data/entities.js";
|
|
12
16
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
13
17
|
const logger = createLogger("customers");
|
|
14
18
|
const paramsSchema = z.object({
|
|
@@ -28,7 +32,17 @@ function normalizeSort(sort) {
|
|
|
28
32
|
function matchesSearch(item, query) {
|
|
29
33
|
const normalized = query.trim().toLowerCase();
|
|
30
34
|
if (!normalized.length) return true;
|
|
31
|
-
return [
|
|
35
|
+
return [
|
|
36
|
+
item.label,
|
|
37
|
+
item.subtitle,
|
|
38
|
+
item.primaryEmail,
|
|
39
|
+
item.primaryPhone,
|
|
40
|
+
item.jobTitle,
|
|
41
|
+
item.department,
|
|
42
|
+
item.status,
|
|
43
|
+
item.lifecycleStage,
|
|
44
|
+
item.source
|
|
45
|
+
].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase().includes(normalized));
|
|
32
46
|
}
|
|
33
47
|
function sortItems(items, sort) {
|
|
34
48
|
if (sort === "recent") {
|
|
@@ -87,16 +101,44 @@ async function GET(req, ctx) {
|
|
|
87
101
|
{ populate: ["person"] },
|
|
88
102
|
entityScope
|
|
89
103
|
);
|
|
104
|
+
const personIds = links.map((link) => link.person?.id).filter((personId) => typeof personId === "string" && personId.length > 0);
|
|
105
|
+
const profiles = personIds.length > 0 ? await findWithDecryption(
|
|
106
|
+
em,
|
|
107
|
+
CustomerPersonProfile,
|
|
108
|
+
{
|
|
109
|
+
entity: { $in: personIds },
|
|
110
|
+
tenantId: deal.tenantId,
|
|
111
|
+
organizationId: deal.organizationId
|
|
112
|
+
},
|
|
113
|
+
{},
|
|
114
|
+
entityScope
|
|
115
|
+
) : [];
|
|
116
|
+
const profileByPersonId = new Map(
|
|
117
|
+
profiles.map((profile) => [profile.entity.id, profile])
|
|
118
|
+
);
|
|
90
119
|
const items = links.map((link) => {
|
|
91
120
|
const person = link.person;
|
|
92
121
|
if (!person?.id) return null;
|
|
122
|
+
const profile = profileByPersonId.get(person.id) ?? null;
|
|
123
|
+
const displayName = person.displayName ?? person.primaryEmail ?? person.id;
|
|
93
124
|
return {
|
|
94
125
|
id: person.id,
|
|
95
|
-
label:
|
|
126
|
+
label: displayName,
|
|
96
127
|
subtitle: person.primaryEmail ?? person.primaryPhone ?? null,
|
|
97
128
|
kind: "person",
|
|
98
129
|
linkedAt: link.createdAt.toISOString(),
|
|
99
|
-
isPrimary: link.isPrimary === true
|
|
130
|
+
isPrimary: link.isPrimary === true,
|
|
131
|
+
displayName,
|
|
132
|
+
primaryEmail: person.primaryEmail ?? null,
|
|
133
|
+
primaryPhone: person.primaryPhone ?? null,
|
|
134
|
+
status: person.status ?? null,
|
|
135
|
+
lifecycleStage: person.lifecycleStage ?? null,
|
|
136
|
+
jobTitle: profile?.jobTitle ?? null,
|
|
137
|
+
department: profile?.department ?? null,
|
|
138
|
+
createdAt: person.createdAt.toISOString(),
|
|
139
|
+
organizationId: person.organizationId,
|
|
140
|
+
temperature: person.temperature ?? null,
|
|
141
|
+
source: person.source ?? null
|
|
100
142
|
};
|
|
101
143
|
}).filter((item) => item !== null);
|
|
102
144
|
const filtered = query.search?.trim().length ? items.filter((item) => matchesSearch(item, query.search ?? "")) : items;
|
|
@@ -138,7 +180,18 @@ const openApi = {
|
|
|
138
180
|
subtitle: z.string().nullable(),
|
|
139
181
|
kind: z.literal("person"),
|
|
140
182
|
linkedAt: z.string(),
|
|
141
|
-
isPrimary: z.boolean()
|
|
183
|
+
isPrimary: z.boolean(),
|
|
184
|
+
displayName: z.string(),
|
|
185
|
+
primaryEmail: z.string().nullable(),
|
|
186
|
+
primaryPhone: z.string().nullable(),
|
|
187
|
+
status: z.string().nullable(),
|
|
188
|
+
lifecycleStage: z.string().nullable(),
|
|
189
|
+
jobTitle: z.string().nullable(),
|
|
190
|
+
department: z.string().nullable(),
|
|
191
|
+
createdAt: z.string(),
|
|
192
|
+
organizationId: z.string().uuid().nullable(),
|
|
193
|
+
temperature: z.string().nullable(),
|
|
194
|
+
source: z.string().nullable()
|
|
142
195
|
})
|
|
143
196
|
),
|
|
144
197
|
total: z.number().int().nonnegative(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customers/api/deals/%5Bid%5D/people/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport { CustomerDeal, CustomerDealPersonLink, CustomerEntity } from '../../../../data/entities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(20),\n search: z.string().optional(),\n sort: z.enum(['label-asc', 'label-desc', 'name-asc', 'name-desc', 'recent']).default('label-asc'),\n})\n\ntype DealLinkedEntitySort = 'label-asc' | 'label-desc' | 'recent'\n\nfunction normalizeSort(sort: z.infer<typeof querySchema>['sort']): DealLinkedEntitySort {\n if (sort === 'name-asc') return 'label-asc'\n if (sort === 'name-desc') return 'label-desc'\n return sort\n}\n\ntype DealPersonItem = {\n id: string\n label: string\n subtitle: string | null\n kind: 'person'\n linkedAt: string\n isPrimary: boolean\n}\n\nfunction matchesSearch(item: DealPersonItem, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized.length) return true\n return [item.label, item.subtitle]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .some((value) => value.toLowerCase().includes(normalized))\n}\n\nfunction sortItems(items: DealPersonItem[], sort: 'label-asc' | 'label-desc' | 'recent'): DealPersonItem[] {\n if (sort === 'recent') {\n return [...items].sort((left, right) => {\n const compare = new Date(right.linkedAt).getTime() - new Date(left.linkedAt).getTime()\n if (compare !== 0) return compare\n return left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n })\n }\n return [...items].sort((left, right) => {\n const compare = left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n return sort === 'label-asc' ? compare : -compare\n })\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { id?: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) throw new CrudHttpError(401, { error: 'Unauthorized' })\n\n const url = new URL(req.url)\n const query = querySchema.parse({\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sort: url.searchParams.get('sort') ?? undefined,\n })\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = (container.resolve('em') as EntityManager).fork()\n const decryptionScope = {\n tenantId: auth.tenantId,\n organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null,\n }\n\n const deal = await findOneWithDecryption(\n em,\n CustomerDeal,\n { id, tenantId: auth.tenantId, deletedAt: null },\n {},\n decryptionScope,\n )\n if (!deal) {\n throw notFound(translate('customers.errors.deal_not_found', 'Deal not found'))\n }\n\n // Existence oracle (#5504): deny a cross-org read as not-found \u2014 identical to\n // the parent-not-found above \u2014 so it cannot reveal that a deal exists in an\n // organization the caller cannot see.\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {\n throw notFound(translate('customers.errors.deal_not_found', 'Deal not found'))\n }\n\n const entityScope = { tenantId: deal.tenantId, organizationId: deal.organizationId }\n const links = await findWithDecryption(\n em,\n CustomerDealPersonLink,\n { deal: deal.id },\n { populate: ['person'] },\n entityScope,\n )\n\n const items = links\n .map((link) => {\n const person = link.person as CustomerEntity | null\n if (!person?.id) return null\n return {\n id: person.id,\n label: person.displayName ?? person.primaryEmail ?? person.id,\n subtitle: person.primaryEmail ?? person.primaryPhone ?? null,\n kind: 'person',\n linkedAt: link.createdAt.toISOString(),\n isPrimary: link.isPrimary === true,\n } satisfies DealPersonItem\n })\n .filter((item): item is DealPersonItem => item !== null)\n\n const filtered = query.search?.trim().length ? items.filter((item) => matchesSearch(item, query.search ?? '')) : items\n const sorted = sortItems(filtered, normalizeSort(query.sort))\n const total = sorted.length\n const totalPages = Math.max(1, Math.ceil(total / query.pageSize))\n const page = Math.min(query.page, totalPages)\n const start = (page - 1) * query.pageSize\n\n return NextResponse.json({\n items: sorted.slice(start, start + query.pageSize),\n total,\n page,\n pageSize: query.pageSize,\n totalPages,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n logger.error('customers.deals.people.GET', { err: error })\n return NextResponse.json({ error: translate('customers.errors.deal_people_load_failed', 'Failed to load linked people') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'List linked people for a deal',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated linked people',\n schema: z.object({\n items: z.array(\n z.object({\n id: z.string().uuid(),\n label: z.string(),\n subtitle: z.string().nullable(),\n kind: z.literal('person'),\n linkedAt: z.string(),\n isPrimary: z.boolean(),\n }),\n ),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n }),\n },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,eAAe,iBAAiB,gBAAgB;AACzD,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,yCAAyC;AAClD,SAAS,2BAA2B;AAEpC,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,uCAAuC;AAChD,
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'\nimport {\n CustomerDeal,\n CustomerDealPersonLink,\n CustomerEntity,\n CustomerPersonProfile,\n} from '../../../../data/entities'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst paramsSchema = z.object({\n id: z.string().uuid(),\n})\n\nconst querySchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(20),\n search: z.string().optional(),\n sort: z.enum(['label-asc', 'label-desc', 'name-asc', 'name-desc', 'recent']).default('label-asc'),\n})\n\ntype DealLinkedEntitySort = 'label-asc' | 'label-desc' | 'recent'\n\nfunction normalizeSort(sort: z.infer<typeof querySchema>['sort']): DealLinkedEntitySort {\n if (sort === 'name-asc') return 'label-asc'\n if (sort === 'name-desc') return 'label-desc'\n return sort\n}\n\ntype DealPersonItem = {\n id: string\n label: string\n subtitle: string | null\n kind: 'person'\n linkedAt: string\n isPrimary: boolean\n // Added for the shared linked-people card, which renders the same shape the company\n // endpoint already returns. Purely additive: every key above keeps its meaning, so\n // `DealLinkedEntitiesTab` and any third-party consumer are unaffected.\n displayName: string\n primaryEmail: string | null\n primaryPhone: string | null\n status: string | null\n lifecycleStage: string | null\n jobTitle: string | null\n department: string | null\n createdAt: string\n organizationId: string\n temperature: string | null\n source: string | null\n}\n\nfunction matchesSearch(item: DealPersonItem, query: string): boolean {\n const normalized = query.trim().toLowerCase()\n if (!normalized.length) return true\n return [\n item.label,\n item.subtitle,\n item.primaryEmail,\n item.primaryPhone,\n item.jobTitle,\n item.department,\n item.status,\n item.lifecycleStage,\n item.source,\n ]\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n .some((value) => value.toLowerCase().includes(normalized))\n}\n\nfunction sortItems(items: DealPersonItem[], sort: 'label-asc' | 'label-desc' | 'recent'): DealPersonItem[] {\n if (sort === 'recent') {\n return [...items].sort((left, right) => {\n const compare = new Date(right.linkedAt).getTime() - new Date(left.linkedAt).getTime()\n if (compare !== 0) return compare\n return left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n })\n }\n return [...items].sort((left, right) => {\n const compare = left.label.localeCompare(right.label, undefined, { sensitivity: 'base' })\n return sort === 'label-asc' ? compare : -compare\n })\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.deals.view'] },\n}\n\nexport async function GET(req: Request, ctx: { params?: { id?: string } }) {\n const { translate } = await resolveTranslations()\n try {\n const { id } = paramsSchema.parse({ id: ctx.params?.id })\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) throw new CrudHttpError(401, { error: 'Unauthorized' })\n\n const url = new URL(req.url)\n const query = querySchema.parse({\n page: url.searchParams.get('page') ?? undefined,\n pageSize: url.searchParams.get('pageSize') ?? undefined,\n search: url.searchParams.get('search') ?? undefined,\n sort: url.searchParams.get('sort') ?? undefined,\n })\n\n const container = await createRequestContainer()\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const em = (container.resolve('em') as EntityManager).fork()\n const decryptionScope = {\n tenantId: auth.tenantId,\n organizationId: resolveSingleOrganizationIdOrDeny(scope, auth) ?? null,\n }\n\n const deal = await findOneWithDecryption(\n em,\n CustomerDeal,\n { id, tenantId: auth.tenantId, deletedAt: null },\n {},\n decryptionScope,\n )\n if (!deal) {\n throw notFound(translate('customers.errors.deal_not_found', 'Deal not found'))\n }\n\n // Existence oracle (#5504): deny a cross-org read as not-found \u2014 identical to\n // the parent-not-found above \u2014 so it cannot reveal that a deal exists in an\n // organization the caller cannot see.\n if (!isOrganizationReadAccessAllowed({ scope, auth, organizationId: deal.organizationId })) {\n throw notFound(translate('customers.errors.deal_not_found', 'Deal not found'))\n }\n\n const entityScope = { tenantId: deal.tenantId, organizationId: deal.organizationId }\n const links = await findWithDecryption(\n em,\n CustomerDealPersonLink,\n { deal: deal.id },\n { populate: ['person'] },\n entityScope,\n )\n\n // One batched read for the profile-only columns, keyed by every linked person \u2014 not one\n // query per row. The handler filters, sorts and paginates in memory below, so the batch\n // covers the whole link set rather than the page; that is fine at the expected scale of\n // tens of contacts per deal, and the in-memory pipeline is what to revisit first if it\n // ever is not.\n const personIds = links\n .map((link) => (link.person as CustomerEntity | null)?.id)\n .filter((personId): personId is string => typeof personId === 'string' && personId.length > 0)\n\n const profiles = personIds.length > 0\n ? await findWithDecryption(\n em,\n CustomerPersonProfile,\n {\n entity: { $in: personIds },\n tenantId: deal.tenantId,\n organizationId: deal.organizationId,\n },\n {},\n entityScope,\n )\n : []\n const profileByPersonId = new Map(\n profiles.map((profile) => [(profile.entity as { id: string }).id, profile]),\n )\n\n const items = links\n .map((link) => {\n const person = link.person as CustomerEntity | null\n if (!person?.id) return null\n const profile = profileByPersonId.get(person.id) ?? null\n const displayName = person.displayName ?? person.primaryEmail ?? person.id\n return {\n id: person.id,\n label: displayName,\n subtitle: person.primaryEmail ?? person.primaryPhone ?? null,\n kind: 'person',\n linkedAt: link.createdAt.toISOString(),\n isPrimary: link.isPrimary === true,\n displayName,\n primaryEmail: person.primaryEmail ?? null,\n primaryPhone: person.primaryPhone ?? null,\n status: person.status ?? null,\n lifecycleStage: person.lifecycleStage ?? null,\n jobTitle: profile?.jobTitle ?? null,\n department: profile?.department ?? null,\n createdAt: person.createdAt.toISOString(),\n organizationId: person.organizationId,\n temperature: person.temperature ?? null,\n source: person.source ?? null,\n } satisfies DealPersonItem\n })\n .filter((item): item is DealPersonItem => item !== null)\n\n const filtered = query.search?.trim().length ? items.filter((item) => matchesSearch(item, query.search ?? '')) : items\n const sorted = sortItems(filtered, normalizeSort(query.sort))\n const total = sorted.length\n const totalPages = Math.max(1, Math.ceil(total / query.pageSize))\n const page = Math.min(query.page, totalPages)\n const start = (page - 1) * query.pageSize\n\n return NextResponse.json({\n items: sorted.slice(start, start + query.pageSize),\n total,\n page,\n pageSize: query.pageSize,\n totalPages,\n })\n } catch (error) {\n if (isCrudHttpError(error)) {\n return NextResponse.json(error.body, { status: error.status })\n }\n logger.error('customers.deals.people.GET', { err: error })\n return NextResponse.json({ error: translate('customers.errors.deal_people_load_failed', 'Failed to load linked people') }, { status: 500 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Customers',\n methods: {\n GET: {\n summary: 'List linked people for a deal',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated linked people',\n schema: z.object({\n items: z.array(\n z.object({\n id: z.string().uuid(),\n label: z.string(),\n subtitle: z.string().nullable(),\n kind: z.literal('person'),\n linkedAt: z.string(),\n isPrimary: z.boolean(),\n displayName: z.string(),\n primaryEmail: z.string().nullable(),\n primaryPhone: z.string().nullable(),\n status: z.string().nullable(),\n lifecycleStage: z.string().nullable(),\n jobTitle: z.string().nullable(),\n department: z.string().nullable(),\n createdAt: z.string(),\n organizationId: z.string().uuid().nullable(),\n temperature: z.string().nullable(),\n source: z.string().nullable(),\n }),\n ),\n total: z.number().int().nonnegative(),\n page: z.number().int().min(1),\n pageSize: z.number().int().min(1),\n totalPages: z.number().int().min(1),\n }),\n },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,eAAe,iBAAiB,gBAAgB;AACzD,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,yCAAyC;AAClD,SAAS,2BAA2B;AAEpC,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,uCAAuC;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AACP,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW;AAEvC,MAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,IAAI,EAAE,OAAO,EAAE,KAAK;AACtB,CAAC;AAED,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,MAAM,EAAE,KAAK,CAAC,aAAa,cAAc,YAAY,aAAa,QAAQ,CAAC,EAAE,QAAQ,WAAW;AAClG,CAAC;AAID,SAAS,cAAc,MAAiE;AACtF,MAAI,SAAS,WAAY,QAAO;AAChC,MAAI,SAAS,YAAa,QAAO;AACjC,SAAO;AACT;AAyBA,SAAS,cAAc,MAAsB,OAAwB;AACnE,QAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,MAAI,CAAC,WAAW,OAAQ,QAAO;AAC/B,SAAO;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP,EACG,OAAO,CAAC,UAA2B,OAAO,UAAU,YAAY,MAAM,SAAS,CAAC,EAChF,KAAK,CAAC,UAAU,MAAM,YAAY,EAAE,SAAS,UAAU,CAAC;AAC7D;AAEA,SAAS,UAAU,OAAyB,MAA+D;AACzG,MAAI,SAAS,UAAU;AACrB,WAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AACtC,YAAM,UAAU,IAAI,KAAK,MAAM,QAAQ,EAAE,QAAQ,IAAI,IAAI,KAAK,KAAK,QAAQ,EAAE,QAAQ;AACrF,UAAI,YAAY,EAAG,QAAO;AAC1B,aAAO,KAAK,MAAM,cAAc,MAAM,OAAO,QAAW,EAAE,aAAa,OAAO,CAAC;AAAA,IACjF,CAAC;AAAA,EACH;AACA,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AACtC,UAAM,UAAU,KAAK,MAAM,cAAc,MAAM,OAAO,QAAW,EAAE,aAAa,OAAO,CAAC;AACxF,WAAO,SAAS,cAAc,UAAU,CAAC;AAAA,EAC3C,CAAC;AACH;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAEA,eAAsB,IAAI,KAAc,KAAmC;AACzE,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,MAAI;AACF,UAAM,EAAE,GAAG,IAAI,aAAa,MAAM,EAAE,IAAI,IAAI,QAAQ,GAAG,CAAC;AACxD,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,MAAM,SAAU,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,eAAe,CAAC;AAE3E,UAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,UAAM,QAAQ,YAAY,MAAM;AAAA,MAC9B,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,MACtC,UAAU,IAAI,aAAa,IAAI,UAAU,KAAK;AAAA,MAC9C,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,MAC1C,MAAM,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,IACxC,CAAC;AAED,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,UAAM,kBAAkB;AAAA,MACtB,UAAU,KAAK;AAAA,MACf,gBAAgB,kCAAkC,OAAO,IAAI,KAAK;AAAA,IACpE;AAEA,UAAM,OAAO,MAAM;AAAA,MACjB;AAAA,MACA;AAAA,MACA,EAAE,IAAI,UAAU,KAAK,UAAU,WAAW,KAAK;AAAA,MAC/C,CAAC;AAAA,MACD;AAAA,IACF;AACA,QAAI,CAAC,MAAM;AACT,YAAM,SAAS,UAAU,mCAAmC,gBAAgB,CAAC;AAAA,IAC/E;AAKA,QAAI,CAAC,gCAAgC,EAAE,OAAO,MAAM,gBAAgB,KAAK,eAAe,CAAC,GAAG;AAC1F,YAAM,SAAS,UAAU,mCAAmC,gBAAgB,CAAC;AAAA,IAC/E;AAEA,UAAM,cAAc,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,eAAe;AACnF,UAAM,QAAQ,MAAM;AAAA,MAClB;AAAA,MACA;AAAA,MACA,EAAE,MAAM,KAAK,GAAG;AAAA,MAChB,EAAE,UAAU,CAAC,QAAQ,EAAE;AAAA,MACvB;AAAA,IACF;AAOA,UAAM,YAAY,MACf,IAAI,CAAC,SAAU,KAAK,QAAkC,EAAE,EACxD,OAAO,CAAC,aAAiC,OAAO,aAAa,YAAY,SAAS,SAAS,CAAC;AAE/F,UAAM,WAAW,UAAU,SAAS,IAChC,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,QACE,QAAQ,EAAE,KAAK,UAAU;AAAA,QACzB,UAAU,KAAK;AAAA,QACf,gBAAgB,KAAK;AAAA,MACvB;AAAA,MACA,CAAC;AAAA,MACD;AAAA,IACF,IACA,CAAC;AACL,UAAM,oBAAoB,IAAI;AAAA,MAC5B,SAAS,IAAI,CAAC,YAAY,CAAE,QAAQ,OAA0B,IAAI,OAAO,CAAC;AAAA,IAC5E;AAEA,UAAM,QAAQ,MACX,IAAI,CAAC,SAAS;AACb,YAAM,SAAS,KAAK;AACpB,UAAI,CAAC,QAAQ,GAAI,QAAO;AACxB,YAAM,UAAU,kBAAkB,IAAI,OAAO,EAAE,KAAK;AACpD,YAAM,cAAc,OAAO,eAAe,OAAO,gBAAgB,OAAO;AACxE,aAAO;AAAA,QACL,IAAI,OAAO;AAAA,QACX,OAAO;AAAA,QACP,UAAU,OAAO,gBAAgB,OAAO,gBAAgB;AAAA,QACxD,MAAM;AAAA,QACN,UAAU,KAAK,UAAU,YAAY;AAAA,QACrC,WAAW,KAAK,cAAc;AAAA,QAC9B;AAAA,QACA,cAAc,OAAO,gBAAgB;AAAA,QACrC,cAAc,OAAO,gBAAgB;AAAA,QACrC,QAAQ,OAAO,UAAU;AAAA,QACzB,gBAAgB,OAAO,kBAAkB;AAAA,QACzC,UAAU,SAAS,YAAY;AAAA,QAC/B,YAAY,SAAS,cAAc;AAAA,QACnC,WAAW,OAAO,UAAU,YAAY;AAAA,QACxC,gBAAgB,OAAO;AAAA,QACvB,aAAa,OAAO,eAAe;AAAA,QACnC,QAAQ,OAAO,UAAU;AAAA,MAC3B;AAAA,IACF,CAAC,EACA,OAAO,CAAC,SAAiC,SAAS,IAAI;AAEzD,UAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,SAAS,MAAM,OAAO,CAAC,SAAS,cAAc,MAAM,MAAM,UAAU,EAAE,CAAC,IAAI;AACjH,UAAM,SAAS,UAAU,UAAU,cAAc,MAAM,IAAI,CAAC;AAC5D,UAAM,QAAQ,OAAO;AACrB,UAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,MAAM,QAAQ,CAAC;AAChE,UAAM,OAAO,KAAK,IAAI,MAAM,MAAM,UAAU;AAC5C,UAAM,SAAS,OAAO,KAAK,MAAM;AAEjC,WAAO,aAAa,KAAK;AAAA,MACvB,OAAO,OAAO,MAAM,OAAO,QAAQ,MAAM,QAAQ;AAAA,MACjD;AAAA,MACA;AAAA,MACA,UAAU,MAAM;AAAA,MAChB;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,QAAI,gBAAgB,KAAK,GAAG;AAC1B,aAAO,aAAa,KAAK,MAAM,MAAM,EAAE,QAAQ,MAAM,OAAO,CAAC;AAAA,IAC/D;AACA,WAAO,MAAM,8BAA8B,EAAE,KAAK,MAAM,CAAC;AACzD,WAAO,aAAa,KAAK,EAAE,OAAO,UAAU,4CAA4C,8BAA8B,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC5I;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,OAAO;AAAA,MACP,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,QAAQ,EAAE,OAAO;AAAA,YACf,OAAO,EAAE;AAAA,cACP,EAAE,OAAO;AAAA,gBACP,IAAI,EAAE,OAAO,EAAE,KAAK;AAAA,gBACpB,OAAO,EAAE,OAAO;AAAA,gBAChB,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,gBAC9B,MAAM,EAAE,QAAQ,QAAQ;AAAA,gBACxB,UAAU,EAAE,OAAO;AAAA,gBACnB,WAAW,EAAE,QAAQ;AAAA,gBACrB,aAAa,EAAE,OAAO;AAAA,gBACtB,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,gBAClC,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,gBAClC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,gBAC5B,gBAAgB,EAAE,OAAO,EAAE,SAAS;AAAA,gBACpC,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,gBAC9B,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,gBAChC,WAAW,EAAE,OAAO;AAAA,gBACpB,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,gBAC3C,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,gBACjC,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,cAC9B,CAAC;AAAA,YACH;AAAA,YACA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,YACpC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,YAC5B,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,YAChC,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,UACpC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
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(),
|