@open-mercato/core 0.6.7-develop.6795.1.8a3f27921c → 0.6.7-develop.6824.1.21fff90682
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_deal_person_link/index.js +2 -0
- package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
- package/dist/generated/entities/customer_user_invitation/index.js +2 -0
- package/dist/generated/entities/customer_user_invitation/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/auth/lib/backendChrome.js +10 -9
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/tenantAccess.js +7 -0
- package/dist/modules/auth/lib/tenantAccess.js.map +2 -2
- package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
- package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
- package/dist/modules/business_rules/api/rules/route.js +71 -4
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
- package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
- package/dist/modules/business_rules/components/ActionRow.js +106 -16
- package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
- package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
- package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
- package/dist/modules/business_rules/lib/action-executor.js +148 -0
- package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
- package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
- package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
- package/dist/modules/business_rules/lib/rule-engine.js +5 -1
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
- package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
- package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
- package/dist/modules/configs/lib/upgrade-actions.js +12 -0
- package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
- package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/data/entities.js +3 -0
- package/dist/modules/customer_accounts/data/entities.js.map +2 -2
- package/dist/modules/customer_accounts/data/validators.js +1 -0
- package/dist/modules/customer_accounts/data/validators.js.map +2 -2
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
- package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
- package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
- package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
- package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
- package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
- package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
- package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
- package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
- package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +7 -3
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +48 -10
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/people.js +2 -0
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/data/entities.js +8 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +2 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
- package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
- package/dist/modules/directory/api/tenants/route.js +8 -2
- package/dist/modules/directory/api/tenants/route.js.map +2 -2
- package/dist/modules/directory/commands/tenants.js +6 -0
- package/dist/modules/directory/commands/tenants.js.map +2 -2
- package/dist/modules/entities/api/entity-settings.js +59 -9
- package/dist/modules/entities/api/entity-settings.js.map +2 -2
- package/dist/modules/entities/backend/config/settings/page.js +10 -0
- package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
- package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
- package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
- package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
- package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
- package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
- package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
- package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +1 -3
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +4 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +15 -6
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
- package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
- package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
- package/dist/modules/payment_gateways/setup.js +47 -0
- package/dist/modules/payment_gateways/setup.js.map +2 -2
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
- package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
- package/dist/modules/portal/lib/navigation.js +7 -0
- package/dist/modules/portal/lib/navigation.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +43 -0
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/lib/search-tokens.js +15 -5
- package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
- package/generated/entities/customer_deal_person_link/index.ts +1 -0
- package/generated/entities/customer_user_invitation/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/auth/lib/backendChrome.tsx +15 -9
- package/src/modules/auth/lib/tenantAccess.ts +8 -0
- package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
- package/src/modules/business_rules/api/rules/route.ts +92 -4
- package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
- package/src/modules/business_rules/components/ActionRow.tsx +156 -18
- package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
- package/src/modules/business_rules/i18n/de.json +12 -0
- package/src/modules/business_rules/i18n/en.json +12 -0
- package/src/modules/business_rules/i18n/es.json +12 -0
- package/src/modules/business_rules/i18n/pl.json +12 -0
- package/src/modules/business_rules/lib/action-executor.ts +198 -0
- package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
- package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
- package/src/modules/business_rules/lib/rule-engine.ts +4 -0
- package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
- package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
- package/src/modules/configs/lib/upgrade-actions.ts +14 -0
- package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
- package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
- package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
- package/src/modules/customer_accounts/data/entities.ts +3 -0
- package/src/modules/customer_accounts/data/validators.ts +1 -0
- package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
- package/src/modules/customer_accounts/i18n/de.json +6 -0
- package/src/modules/customer_accounts/i18n/en.json +6 -0
- package/src/modules/customer_accounts/i18n/es.json +6 -0
- package/src/modules/customer_accounts/i18n/pl.json +6 -0
- package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
- package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
- package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
- package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
- package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
- package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
- package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
- package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
- package/src/modules/customers/api/deals/[id]/route.ts +10 -2
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
- package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
- package/src/modules/customers/commands/deals.ts +50 -9
- package/src/modules/customers/commands/people.ts +3 -0
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
- package/src/modules/customers/data/entities.ts +9 -1
- package/src/modules/customers/data/validators.ts +1 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
- package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
- package/src/modules/directory/api/tenants/route.ts +9 -1
- package/src/modules/directory/commands/tenants.ts +6 -0
- package/src/modules/entities/api/entity-settings.ts +66 -22
- package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
- package/src/modules/entities/backend/config/settings/page.tsx +12 -0
- package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
- package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
- package/src/modules/entities/i18n/de.json +9 -0
- package/src/modules/entities/i18n/en.json +9 -0
- package/src/modules/entities/i18n/es.json +9 -0
- package/src/modules/entities/i18n/pl.json +9 -0
- package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
- package/src/modules/payment_gateways/api/sessions/route.ts +1 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
- package/src/modules/payment_gateways/data/entities.ts +5 -0
- package/src/modules/payment_gateways/i18n/de.json +5 -1
- package/src/modules/payment_gateways/i18n/en.json +5 -1
- package/src/modules/payment_gateways/i18n/es.json +5 -1
- package/src/modules/payment_gateways/i18n/pl.json +5 -1
- package/src/modules/payment_gateways/lib/gateway-service.ts +17 -8
- package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
- package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
- package/src/modules/payment_gateways/setup.ts +62 -0
- package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
- package/src/modules/portal/i18n/de.json +18 -0
- package/src/modules/portal/i18n/en.json +18 -0
- package/src/modules/portal/i18n/es.json +18 -0
- package/src/modules/portal/i18n/pl.json +18 -0
- package/src/modules/portal/lib/navigation.ts +3 -0
- package/src/modules/query_index/lib/engine.ts +52 -0
- package/src/modules/query_index/lib/search-tokens.ts +16 -4
- package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +11 -1
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
Plus,
|
|
14
14
|
} from 'lucide-react'
|
|
15
15
|
import type { SectionAction } from '@open-mercato/ui/backend/detail'
|
|
16
|
+
import { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
17
|
+
import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
|
|
16
18
|
import { useDealsAccess } from './useDealsAccess'
|
|
17
19
|
|
|
18
20
|
export type CompanyTabId =
|
|
@@ -30,10 +32,13 @@ type TabDef = {
|
|
|
30
32
|
count?: React.ReactNode
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
export const COMPANY_DETAIL_TABS_COMPONENT_ID = 'section:customers.companies.detailTabs'
|
|
36
|
+
|
|
37
|
+
export type CompanyDetailTabsProps = {
|
|
34
38
|
activeTab: CompanyTabId
|
|
35
39
|
onTabChange: (tab: CompanyTabId) => void
|
|
36
40
|
injectedTabs?: Array<{ id: string; label: string; priority?: number }>
|
|
41
|
+
hiddenTabIds?: string[]
|
|
37
42
|
peopleCount?: number
|
|
38
43
|
dealsCount?: number
|
|
39
44
|
activitiesCount?: number
|
|
@@ -63,10 +68,11 @@ function formatTabCount(count: number): string | number | undefined {
|
|
|
63
68
|
return count > 999 ? '999+' : count
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
function DefaultCompanyDetailTabs({
|
|
67
72
|
activeTab,
|
|
68
73
|
onTabChange,
|
|
69
74
|
injectedTabs = [],
|
|
75
|
+
hiddenTabIds = [],
|
|
70
76
|
peopleCount = 0,
|
|
71
77
|
dealsCount = 0,
|
|
72
78
|
activitiesCount = 0,
|
|
@@ -117,16 +123,16 @@ export function CompanyDetailTabs({
|
|
|
117
123
|
[t, canViewDeals, peopleCount, dealsCount, activitiesCount, filesCount],
|
|
118
124
|
)
|
|
119
125
|
|
|
120
|
-
const allTabs: TabDef[] = React.useMemo(
|
|
121
|
-
()
|
|
126
|
+
const allTabs: TabDef[] = React.useMemo(() => {
|
|
127
|
+
const hidden = new Set(hiddenTabIds)
|
|
128
|
+
return [
|
|
122
129
|
...builtInTabs,
|
|
123
130
|
...injectedTabs.map((tab) => ({
|
|
124
131
|
id: tab.id as CompanyTabId,
|
|
125
132
|
label: tab.label,
|
|
126
133
|
})),
|
|
127
|
-
]
|
|
128
|
-
|
|
129
|
-
)
|
|
134
|
+
].filter((tab) => !hidden.has(tab.id))
|
|
135
|
+
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
130
136
|
|
|
131
137
|
return (
|
|
132
138
|
<div>
|
|
@@ -170,3 +176,20 @@ export function CompanyDetailTabs({
|
|
|
170
176
|
</div>
|
|
171
177
|
)
|
|
172
178
|
}
|
|
179
|
+
|
|
180
|
+
registerComponent<CompanyDetailTabsProps>({
|
|
181
|
+
id: COMPANY_DETAIL_TABS_COMPONENT_ID,
|
|
182
|
+
component: DefaultCompanyDetailTabs,
|
|
183
|
+
metadata: {
|
|
184
|
+
module: 'customers',
|
|
185
|
+
description: 'Company detail tab navigation and content.',
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
export function CompanyDetailTabs(props: CompanyDetailTabsProps) {
|
|
190
|
+
const ResolvedTabs = useRegisteredComponent<CompanyDetailTabsProps>(
|
|
191
|
+
COMPANY_DETAIL_TABS_COMPONENT_ID,
|
|
192
|
+
DefaultCompanyDetailTabs,
|
|
193
|
+
)
|
|
194
|
+
return <ResolvedTabs {...props} />
|
|
195
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Activity, Building2, History, NotebookPen, Paperclip, Users } from 'lucide-react'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
7
|
+
import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
|
|
6
8
|
import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
7
9
|
|
|
8
10
|
export type DealTabId =
|
|
@@ -21,10 +23,13 @@ type TabDef = {
|
|
|
21
23
|
count?: React.ReactNode
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
export const DEAL_DETAIL_TABS_COMPONENT_ID = 'section:customers.deals.detailTabs'
|
|
27
|
+
|
|
28
|
+
export type DealDetailTabsProps = {
|
|
25
29
|
activeTab: DealTabId
|
|
26
30
|
onTabChange: (tab: DealTabId) => void
|
|
27
31
|
injectedTabs?: Array<{ id: string; label: string }>
|
|
32
|
+
hiddenTabIds?: string[]
|
|
28
33
|
peopleCount?: number
|
|
29
34
|
companiesCount?: number
|
|
30
35
|
children: React.ReactNode
|
|
@@ -32,9 +37,11 @@ type DealDetailTabsProps = {
|
|
|
32
37
|
|
|
33
38
|
const SUPPORTED_TAB_IDS = new Set<DealTabId>(['activities', 'people', 'companies', 'notes', 'files', 'changelog'])
|
|
34
39
|
|
|
35
|
-
export function resolveLegacyTab(tab: string | null | undefined): DealTabId {
|
|
40
|
+
export function resolveLegacyTab(tab: string | null | undefined, knownTabIds?: Iterable<string>): DealTabId {
|
|
36
41
|
if (!tab) return 'activities'
|
|
37
|
-
|
|
42
|
+
if (SUPPORTED_TAB_IDS.has(tab as DealTabId)) return tab as DealTabId
|
|
43
|
+
if (knownTabIds && new Set(knownTabIds).has(tab)) return tab
|
|
44
|
+
return 'activities'
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
function formatTabCount(count: number): string | number | undefined {
|
|
@@ -42,10 +49,11 @@ function formatTabCount(count: number): string | number | undefined {
|
|
|
42
49
|
return count > 999 ? '999+' : count
|
|
43
50
|
}
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
function DefaultDealDetailTabs({
|
|
46
53
|
activeTab,
|
|
47
54
|
onTabChange,
|
|
48
55
|
injectedTabs = [],
|
|
56
|
+
hiddenTabIds = [],
|
|
49
57
|
peopleCount = 0,
|
|
50
58
|
companiesCount = 0,
|
|
51
59
|
children,
|
|
@@ -91,16 +99,16 @@ export function DealDetailTabs({
|
|
|
91
99
|
[companiesCount, peopleCount, t],
|
|
92
100
|
)
|
|
93
101
|
|
|
94
|
-
const allTabs = React.useMemo<TabDef[]>(
|
|
95
|
-
()
|
|
102
|
+
const allTabs = React.useMemo<TabDef[]>(() => {
|
|
103
|
+
const hidden = new Set(hiddenTabIds)
|
|
104
|
+
return [
|
|
96
105
|
...builtInTabs,
|
|
97
106
|
...injectedTabs.map((tab) => ({
|
|
98
107
|
id: tab.id as DealTabId,
|
|
99
108
|
label: tab.label,
|
|
100
109
|
})),
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
)
|
|
110
|
+
].filter((tab) => !hidden.has(tab.id))
|
|
111
|
+
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
104
112
|
|
|
105
113
|
return (
|
|
106
114
|
<div>
|
|
@@ -127,3 +135,20 @@ export function DealDetailTabs({
|
|
|
127
135
|
</div>
|
|
128
136
|
)
|
|
129
137
|
}
|
|
138
|
+
|
|
139
|
+
registerComponent<DealDetailTabsProps>({
|
|
140
|
+
id: DEAL_DETAIL_TABS_COMPONENT_ID,
|
|
141
|
+
component: DefaultDealDetailTabs,
|
|
142
|
+
metadata: {
|
|
143
|
+
module: 'customers',
|
|
144
|
+
description: 'Deal detail tab navigation and content.',
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
export function DealDetailTabs(props: DealDetailTabsProps) {
|
|
149
|
+
const ResolvedTabs = useRegisteredComponent<DealDetailTabsProps>(
|
|
150
|
+
DEAL_DETAIL_TABS_COMPONENT_ID,
|
|
151
|
+
DefaultDealDetailTabs,
|
|
152
|
+
)
|
|
153
|
+
return <ResolvedTabs {...props} />
|
|
154
|
+
}
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
MapPin,
|
|
17
17
|
} from 'lucide-react'
|
|
18
18
|
import type { SectionAction } from '@open-mercato/ui/backend/detail'
|
|
19
|
+
import { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
20
|
+
import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
|
|
19
21
|
|
|
20
22
|
export type PersonTabId =
|
|
21
23
|
| 'activities'
|
|
@@ -35,10 +37,13 @@ type TabDef = {
|
|
|
35
37
|
count?: React.ReactNode
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
export const PERSON_DETAIL_TABS_COMPONENT_ID = 'section:customers.people.detailTabs'
|
|
41
|
+
|
|
42
|
+
export type PersonDetailTabsProps = {
|
|
39
43
|
activeTab: PersonTabId
|
|
40
44
|
onTabChange: (tab: PersonTabId) => void
|
|
41
45
|
injectedTabs?: Array<{ id: string; label: string }>
|
|
46
|
+
hiddenTabIds?: string[]
|
|
42
47
|
activitiesCount?: number
|
|
43
48
|
dealsCount?: number
|
|
44
49
|
companiesCount?: number
|
|
@@ -51,9 +56,11 @@ type PersonDetailTabsProps = {
|
|
|
51
56
|
|
|
52
57
|
const SUPPORTED_TAB_IDS = new Set<PersonTabId>(['activities', 'emails', 'deals', 'companies', 'addresses', 'tasks', 'changelog', 'files'])
|
|
53
58
|
|
|
54
|
-
export function resolveLegacyTab(tab: string | null | undefined): PersonTabId {
|
|
59
|
+
export function resolveLegacyTab(tab: string | null | undefined, knownTabIds?: Iterable<string>): PersonTabId {
|
|
55
60
|
if (!tab) return 'activities'
|
|
56
|
-
|
|
61
|
+
if (SUPPORTED_TAB_IDS.has(tab as PersonTabId)) return tab as PersonTabId
|
|
62
|
+
if (knownTabIds && new Set(knownTabIds).has(tab)) return tab
|
|
63
|
+
return 'activities'
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
function formatTabCount(count: number): string | number | undefined {
|
|
@@ -61,10 +68,11 @@ function formatTabCount(count: number): string | number | undefined {
|
|
|
61
68
|
return count > 999 ? '999+' : count
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
function DefaultPersonDetailTabs({
|
|
65
72
|
activeTab,
|
|
66
73
|
onTabChange,
|
|
67
74
|
injectedTabs = [],
|
|
75
|
+
hiddenTabIds = [],
|
|
68
76
|
activitiesCount = 0,
|
|
69
77
|
dealsCount = 0,
|
|
70
78
|
companiesCount = 0,
|
|
@@ -129,16 +137,16 @@ export function PersonDetailTabs({
|
|
|
129
137
|
[t, activitiesCount, dealsCount, companiesCount, addressesCount, tasksCount, filesCount],
|
|
130
138
|
)
|
|
131
139
|
|
|
132
|
-
const allTabs: TabDef[] = React.useMemo(
|
|
133
|
-
()
|
|
140
|
+
const allTabs: TabDef[] = React.useMemo(() => {
|
|
141
|
+
const hidden = new Set(hiddenTabIds)
|
|
142
|
+
return [
|
|
134
143
|
...builtInTabs,
|
|
135
144
|
...injectedTabs.map((tab) => ({
|
|
136
145
|
id: tab.id as PersonTabId,
|
|
137
146
|
label: tab.label,
|
|
138
147
|
})),
|
|
139
|
-
]
|
|
140
|
-
|
|
141
|
-
)
|
|
148
|
+
].filter((tab) => !hidden.has(tab.id))
|
|
149
|
+
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
142
150
|
|
|
143
151
|
return (
|
|
144
152
|
<div>
|
|
@@ -182,3 +190,20 @@ export function PersonDetailTabs({
|
|
|
182
190
|
</div>
|
|
183
191
|
)
|
|
184
192
|
}
|
|
193
|
+
|
|
194
|
+
registerComponent<PersonDetailTabsProps>({
|
|
195
|
+
id: PERSON_DETAIL_TABS_COMPONENT_ID,
|
|
196
|
+
component: DefaultPersonDetailTabs,
|
|
197
|
+
metadata: {
|
|
198
|
+
module: 'customers',
|
|
199
|
+
description: 'Person detail tab navigation and content.',
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
export function PersonDetailTabs(props: PersonDetailTabsProps) {
|
|
204
|
+
const ResolvedTabs = useRegisteredComponent<PersonDetailTabsProps>(
|
|
205
|
+
PERSON_DETAIL_TABS_COMPONENT_ID,
|
|
206
|
+
DefaultPersonDetailTabs,
|
|
207
|
+
)
|
|
208
|
+
return <ResolvedTabs {...props} />
|
|
209
|
+
}
|
|
@@ -439,9 +439,14 @@ export class CustomerDealStageTransition {
|
|
|
439
439
|
@Entity({ tableName: 'customer_deal_people' })
|
|
440
440
|
@Index({ name: 'customer_deal_people_deal_idx', properties: ['deal'] })
|
|
441
441
|
@Index({ name: 'customer_deal_people_person_idx', properties: ['person'] })
|
|
442
|
+
@Index({
|
|
443
|
+
name: 'customer_deal_people_primary_uq',
|
|
444
|
+
expression:
|
|
445
|
+
`create unique index "customer_deal_people_primary_uq" on "customer_deal_people" ("deal_id") where "is_primary"`,
|
|
446
|
+
})
|
|
442
447
|
@Unique({ name: 'customer_deal_people_unique', properties: ['deal', 'person'] })
|
|
443
448
|
export class CustomerDealPersonLink {
|
|
444
|
-
[OptionalProps]?: 'createdAt'
|
|
449
|
+
[OptionalProps]?: 'isPrimary' | 'createdAt'
|
|
445
450
|
|
|
446
451
|
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
447
452
|
id!: string
|
|
@@ -449,6 +454,9 @@ export class CustomerDealPersonLink {
|
|
|
449
454
|
@Property({ name: 'role', type: 'text', nullable: true })
|
|
450
455
|
participantRole?: string | null
|
|
451
456
|
|
|
457
|
+
@Property({ name: 'is_primary', type: 'boolean', default: false })
|
|
458
|
+
isPrimary: boolean = false
|
|
459
|
+
|
|
452
460
|
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
453
461
|
createdAt: Date = new Date()
|
|
454
462
|
|
|
@@ -184,6 +184,7 @@ export const dealCreateSchema = scopedSchema.extend({
|
|
|
184
184
|
lossNotes: z.string().max(4000).optional(),
|
|
185
185
|
companyIds: z.array(uuid()).optional(),
|
|
186
186
|
personIds: z.array(uuid()).optional(),
|
|
187
|
+
primaryPersonEntityId: uuid().nullable().optional(),
|
|
187
188
|
})
|
|
188
189
|
|
|
189
190
|
export const dealUpdateSchema = z
|
|
@@ -1617,6 +1617,7 @@
|
|
|
1617
1617
|
"customers.errors.person_not_found": "Person nicht gefunden",
|
|
1618
1618
|
"customers.errors.person_profile_not_found": "Personenprofil nicht gefunden",
|
|
1619
1619
|
"customers.errors.person_required": "Personen-ID ist erforderlich",
|
|
1620
|
+
"customers.errors.primaryPersonMustBeLinked": "Die primäre Person muss mit dem Deal verknüpft sein",
|
|
1620
1621
|
"customers.errors.profile_must_be_object": "Profil muss ein Objekt sein",
|
|
1621
1622
|
"customers.errors.profile_unsupported_field": "Nicht unterstütztes Profilfeld: {{field}}",
|
|
1622
1623
|
"customers.errors.role_not_found": "Rolle nicht gefunden",
|
|
@@ -1617,6 +1617,7 @@
|
|
|
1617
1617
|
"customers.errors.person_not_found": "Person not found",
|
|
1618
1618
|
"customers.errors.person_profile_not_found": "Person profile not found",
|
|
1619
1619
|
"customers.errors.person_required": "Person id is required",
|
|
1620
|
+
"customers.errors.primaryPersonMustBeLinked": "Primary person must be linked to the deal",
|
|
1620
1621
|
"customers.errors.profile_must_be_object": "profile must be an object",
|
|
1621
1622
|
"customers.errors.profile_unsupported_field": "Unsupported profile field: {{field}}",
|
|
1622
1623
|
"customers.errors.role_not_found": "Role not found",
|
|
@@ -1617,6 +1617,7 @@
|
|
|
1617
1617
|
"customers.errors.person_not_found": "Persona no encontrada",
|
|
1618
1618
|
"customers.errors.person_profile_not_found": "Perfil de persona no encontrado",
|
|
1619
1619
|
"customers.errors.person_required": "Se requiere el ID de la persona",
|
|
1620
|
+
"customers.errors.primaryPersonMustBeLinked": "La persona principal debe estar vinculada al deal",
|
|
1620
1621
|
"customers.errors.profile_must_be_object": "el perfil debe ser un objeto",
|
|
1621
1622
|
"customers.errors.profile_unsupported_field": "Campo de perfil no soportado: {{field}}",
|
|
1622
1623
|
"customers.errors.role_not_found": "Rol no encontrado",
|
|
@@ -1617,6 +1617,7 @@
|
|
|
1617
1617
|
"customers.errors.person_not_found": "Nie znaleziono osoby",
|
|
1618
1618
|
"customers.errors.person_profile_not_found": "Nie znaleziono profilu osoby",
|
|
1619
1619
|
"customers.errors.person_required": "Wymagany identyfikator osoby",
|
|
1620
|
+
"customers.errors.primaryPersonMustBeLinked": "Główna osoba musi być powiązana z dealem",
|
|
1620
1621
|
"customers.errors.profile_must_be_object": "profil musi być obiektem",
|
|
1621
1622
|
"customers.errors.profile_unsupported_field": "Nieobsługiwane pole profilu: {{field}}",
|
|
1622
1623
|
"customers.errors.role_not_found": "Nie znaleziono roli",
|
|
@@ -1685,6 +1685,23 @@
|
|
|
1685
1685
|
"enumItems": [],
|
|
1686
1686
|
"mappedType": "uuid"
|
|
1687
1687
|
},
|
|
1688
|
+
"is_primary": {
|
|
1689
|
+
"name": "is_primary",
|
|
1690
|
+
"type": "boolean",
|
|
1691
|
+
"unsigned": false,
|
|
1692
|
+
"autoincrement": false,
|
|
1693
|
+
"primary": false,
|
|
1694
|
+
"nullable": false,
|
|
1695
|
+
"unique": false,
|
|
1696
|
+
"length": null,
|
|
1697
|
+
"precision": null,
|
|
1698
|
+
"scale": null,
|
|
1699
|
+
"default": "false",
|
|
1700
|
+
"comment": null,
|
|
1701
|
+
"collation": null,
|
|
1702
|
+
"enumItems": [],
|
|
1703
|
+
"mappedType": "boolean"
|
|
1704
|
+
},
|
|
1688
1705
|
"person_entity_id": {
|
|
1689
1706
|
"name": "person_entity_id",
|
|
1690
1707
|
"type": "uuid",
|
|
@@ -1741,6 +1758,15 @@
|
|
|
1741
1758
|
"primary": false,
|
|
1742
1759
|
"unique": false
|
|
1743
1760
|
},
|
|
1761
|
+
{
|
|
1762
|
+
"columnNames": [],
|
|
1763
|
+
"composite": false,
|
|
1764
|
+
"constraint": false,
|
|
1765
|
+
"keyName": "customer_deal_people_primary_uq",
|
|
1766
|
+
"primary": false,
|
|
1767
|
+
"unique": false,
|
|
1768
|
+
"expression": "create unique index \"customer_deal_people_primary_uq\" on \"customer_deal_people\" (\"deal_id\") where \"is_primary\""
|
|
1769
|
+
},
|
|
1744
1770
|
{
|
|
1745
1771
|
"columnNames": [
|
|
1746
1772
|
"id"
|
|
@@ -6499,4 +6525,4 @@
|
|
|
6499
6525
|
],
|
|
6500
6526
|
"views": [],
|
|
6501
6527
|
"nativeEnums": {}
|
|
6502
|
-
}
|
|
6528
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260723210000_customers extends Migration {
|
|
4
|
+
|
|
5
|
+
override up(): void | Promise<void> {
|
|
6
|
+
this.addSql(`alter table "customer_deal_people" add "is_primary" boolean not null default false;`);
|
|
7
|
+
this.addSql(`create unique index "customer_deal_people_primary_uq" on "customer_deal_people" ("deal_id") where "is_primary";`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
override down(): void | Promise<void> {
|
|
11
|
+
this.addSql(`drop index "customer_deal_people_primary_uq";`);
|
|
12
|
+
this.addSql(`alter table "customer_deal_people" drop column "is_primary";`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
}
|
|
@@ -2,7 +2,9 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { logCrudAccess, makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
4
4
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
|
+
import { resolveIsSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
|
|
5
6
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
7
|
+
|
|
6
8
|
import { Tenant } from '@open-mercato/core/modules/directory/data/entities'
|
|
7
9
|
import { tenantCreateSchema, tenantUpdateSchema } from '@open-mercato/core/modules/directory/data/validators'
|
|
8
10
|
import { loadCustomFieldValues, buildCustomFieldFiltersFromQuery } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
@@ -94,6 +96,12 @@ export async function GET(req: Request) {
|
|
|
94
96
|
return NextResponse.json({ items: [], total: 0, page: 1, pageSize: 50, totalPages: 1 }, { status: 401 })
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
const container = await createRequestContainer()
|
|
100
|
+
const isSuperAdmin = await resolveIsSuperAdmin({ auth, container })
|
|
101
|
+
if (!isSuperAdmin) {
|
|
102
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
103
|
+
}
|
|
104
|
+
|
|
97
105
|
const url = new URL(req.url)
|
|
98
106
|
const parsed = listQuerySchema.safeParse({
|
|
99
107
|
id: url.searchParams.get('id') ?? undefined,
|
|
@@ -111,7 +119,6 @@ export async function GET(req: Request) {
|
|
|
111
119
|
)
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
const container = await createRequestContainer()
|
|
115
122
|
const em = container.resolve('em') as EntityManager
|
|
116
123
|
|
|
117
124
|
const { id, page, pageSize, search, sortField, sortDir, isActive } = parsed.data
|
|
@@ -256,6 +263,7 @@ const tenantGetDoc: OpenApiMethodDoc = {
|
|
|
256
263
|
errors: [
|
|
257
264
|
{ status: 400, description: 'Invalid query parameters', schema: directoryErrorSchema },
|
|
258
265
|
{ status: 401, description: 'Authentication required', schema: directoryErrorSchema },
|
|
266
|
+
{ status: 403, description: 'Requires super-admin', schema: directoryErrorSchema },
|
|
259
267
|
],
|
|
260
268
|
}
|
|
261
269
|
|
|
@@ -5,6 +5,7 @@ import { Tenant } from '@open-mercato/core/modules/directory/data/entities'
|
|
|
5
5
|
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
6
6
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
7
7
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
8
|
+
import { requireSuperAdmin } from '@open-mercato/core/modules/auth/lib/tenantAccess'
|
|
8
9
|
import { E } from '#generated/entities.ids.generated'
|
|
9
10
|
import {
|
|
10
11
|
parseWithCustomFields,
|
|
@@ -48,6 +49,7 @@ type SerializedTenant = ReturnType<typeof serializeTenant>
|
|
|
48
49
|
const createTenantCommand: CommandHandler<TenantPayload, Tenant> = {
|
|
49
50
|
id: 'directory.tenants.create',
|
|
50
51
|
async execute(rawInput, ctx) {
|
|
52
|
+
await requireSuperAdmin(ctx)
|
|
51
53
|
const { parsed, custom } = parseWithCustomFields(tenantCreateSchema, rawInput)
|
|
52
54
|
const de = (ctx.container.resolve('dataEngine') as DataEngine)
|
|
53
55
|
|
|
@@ -126,6 +128,7 @@ const createTenantCommand: CommandHandler<TenantPayload, Tenant> = {
|
|
|
126
128
|
const updateTenantCommand: CommandHandler<TenantPayload, Tenant> = {
|
|
127
129
|
id: 'directory.tenants.update',
|
|
128
130
|
async prepare(rawInput, ctx) {
|
|
131
|
+
await requireSuperAdmin(ctx)
|
|
129
132
|
const { parsed } = parseWithCustomFields(tenantUpdateSchema, rawInput)
|
|
130
133
|
const em = (ctx.container.resolve('em') as EntityManager)
|
|
131
134
|
const current = await em.findOne(Tenant, { id: parsed.id, deletedAt: null })
|
|
@@ -133,6 +136,7 @@ const updateTenantCommand: CommandHandler<TenantPayload, Tenant> = {
|
|
|
133
136
|
return { before: serializeTenant(current) }
|
|
134
137
|
},
|
|
135
138
|
async execute(rawInput, ctx) {
|
|
139
|
+
await requireSuperAdmin(ctx)
|
|
136
140
|
const { parsed, custom } = parseWithCustomFields(tenantUpdateSchema, rawInput)
|
|
137
141
|
const de = (ctx.container.resolve('dataEngine') as DataEngine)
|
|
138
142
|
const tenant = await de.updateOrmEntity({
|
|
@@ -191,12 +195,14 @@ const updateTenantCommand: CommandHandler<TenantPayload, Tenant> = {
|
|
|
191
195
|
const deleteTenantCommand: CommandHandler<{ body: any; query: Record<string, string> }, Tenant> = {
|
|
192
196
|
id: 'directory.tenants.delete',
|
|
193
197
|
async prepare(input, ctx) {
|
|
198
|
+
await requireSuperAdmin(ctx)
|
|
194
199
|
const id = requireId(input, 'Tenant id required')
|
|
195
200
|
const em = (ctx.container.resolve('em') as EntityManager)
|
|
196
201
|
const existing = await em.findOne(Tenant, { id, deletedAt: null })
|
|
197
202
|
return existing ? { before: serializeTenant(existing) } : {}
|
|
198
203
|
},
|
|
199
204
|
async execute(rawInput, ctx) {
|
|
205
|
+
await requireSuperAdmin(ctx)
|
|
200
206
|
const id = requireId(rawInput, 'Tenant id required')
|
|
201
207
|
const de = (ctx.container.resolve('dataEngine') as DataEngine)
|
|
202
208
|
const tenant = await de.deleteOrmEntity({
|
|
@@ -3,6 +3,10 @@ import { z } from 'zod'
|
|
|
3
3
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
6
|
+
import { beginEntitiesMutationGuard } from './definitions.mutation-guard'
|
|
7
|
+
import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
8
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
9
|
+
import type { ModuleConfigService } from '../../configs/lib/module-config-service'
|
|
6
10
|
|
|
7
11
|
// Tenant-scoped policy: when true, new custom entities are created with
|
|
8
12
|
// `access_restricted = true` unless the create request explicitly sets the flag.
|
|
@@ -14,24 +18,10 @@ export const metadata = {
|
|
|
14
18
|
PUT: { requireAuth: true, requireFeatures: ['entities.definitions.manage'] },
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
type ModuleConfigServiceLike = {
|
|
18
|
-
getValue: (
|
|
19
|
-
moduleId: string,
|
|
20
|
-
name: string,
|
|
21
|
-
options?: { defaultValue?: unknown; scope?: { tenantId?: string | null } },
|
|
22
|
-
) => Promise<unknown>
|
|
23
|
-
setValue: (
|
|
24
|
-
moduleId: string,
|
|
25
|
-
name: string,
|
|
26
|
-
value: unknown,
|
|
27
|
-
scope?: { tenantId?: string | null },
|
|
28
|
-
) => Promise<unknown>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
21
|
async function readPolicy(tenantId: string | null): Promise<boolean> {
|
|
32
22
|
try {
|
|
33
23
|
const { resolve } = await createRequestContainer()
|
|
34
|
-
const moduleConfigService = resolve('moduleConfigService') as
|
|
24
|
+
const moduleConfigService = resolve('moduleConfigService') as ModuleConfigService
|
|
35
25
|
const value = await moduleConfigService.getValue(CONFIG_MODULE, NEW_ENTITIES_RESTRICTED_KEY, {
|
|
36
26
|
defaultValue: false,
|
|
37
27
|
scope: { tenantId },
|
|
@@ -45,12 +35,20 @@ async function readPolicy(tenantId: string | null): Promise<boolean> {
|
|
|
45
35
|
export async function GET(req: Request) {
|
|
46
36
|
const auth = await getAuthFromRequest(req)
|
|
47
37
|
if (!auth || !auth.tenantId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
48
|
-
const
|
|
49
|
-
|
|
38
|
+
const { resolve } = await createRequestContainer()
|
|
39
|
+
const moduleConfigService = resolve('moduleConfigService') as ModuleConfigService
|
|
40
|
+
const record = await moduleConfigService.getRecord(CONFIG_MODULE, NEW_ENTITIES_RESTRICTED_KEY, {
|
|
41
|
+
tenantId: auth.tenantId ?? null,
|
|
42
|
+
})
|
|
43
|
+
return NextResponse.json({
|
|
44
|
+
newEntitiesRestrictedByDefault: record ? record.value === true : false,
|
|
45
|
+
updatedAt: record ? record.updatedAt : null,
|
|
46
|
+
})
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
const putBodySchema = z.object({
|
|
53
50
|
newEntitiesRestrictedByDefault: z.boolean(),
|
|
51
|
+
expectedUpdatedAt: z.string().optional(),
|
|
54
52
|
})
|
|
55
53
|
|
|
56
54
|
export async function PUT(req: Request) {
|
|
@@ -62,19 +60,64 @@ export async function PUT(req: Request) {
|
|
|
62
60
|
if (!parsed.success) {
|
|
63
61
|
return NextResponse.json({ error: 'Validation failed', details: parsed.error.flatten() }, { status: 400 })
|
|
64
62
|
}
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
63
|
+
const container = await createRequestContainer()
|
|
64
|
+
const { resolve } = container
|
|
65
|
+
const moduleConfigService = resolve('moduleConfigService') as ModuleConfigService
|
|
66
|
+
|
|
67
|
+
const currentRecord = await moduleConfigService.getRecord(
|
|
68
|
+
CONFIG_MODULE,
|
|
69
|
+
NEW_ENTITIES_RESTRICTED_KEY,
|
|
70
|
+
{ tenantId: auth.tenantId ?? null },
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const guard = await beginEntitiesMutationGuard({
|
|
74
|
+
container,
|
|
75
|
+
auth,
|
|
76
|
+
req,
|
|
77
|
+
resourceKind: 'entities.settings',
|
|
78
|
+
resourceId: NEW_ENTITIES_RESTRICTED_KEY,
|
|
79
|
+
operation: currentRecord ? 'update' : 'create',
|
|
80
|
+
mutationPayload: parsed.data as unknown as Record<string, unknown>,
|
|
81
|
+
})
|
|
82
|
+
if (guard.blockedResponse) return guard.blockedResponse
|
|
83
|
+
|
|
84
|
+
const ifMatchHeader = req.headers.get('If-Match') || req.headers.get('x-om-ext-optimistic-lock-expected-updated-at')
|
|
85
|
+
const expectedUpdatedAt = ifMatchHeader || parsed.data.expectedUpdatedAt
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await enforceCommandOptimisticLockWithGuards(container, {
|
|
89
|
+
resourceKind: 'entities.settings',
|
|
90
|
+
resourceId: NEW_ENTITIES_RESTRICTED_KEY,
|
|
91
|
+
current: currentRecord ? currentRecord.updatedAt : null,
|
|
92
|
+
expected: expectedUpdatedAt,
|
|
93
|
+
request: req,
|
|
94
|
+
})
|
|
95
|
+
} catch (lockError) {
|
|
96
|
+
if (isCrudHttpError(lockError)) {
|
|
97
|
+
return NextResponse.json(lockError.body, { status: lockError.status })
|
|
98
|
+
}
|
|
99
|
+
throw lockError
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const updatedRecord = await moduleConfigService.setValue(
|
|
68
103
|
CONFIG_MODULE,
|
|
69
104
|
NEW_ENTITIES_RESTRICTED_KEY,
|
|
70
105
|
parsed.data.newEntitiesRestrictedByDefault,
|
|
71
106
|
{ tenantId: auth.tenantId ?? null },
|
|
72
107
|
)
|
|
73
|
-
|
|
108
|
+
|
|
109
|
+
await guard.runAfterSuccess()
|
|
110
|
+
|
|
111
|
+
return NextResponse.json({
|
|
112
|
+
ok: true,
|
|
113
|
+
newEntitiesRestrictedByDefault: parsed.data.newEntitiesRestrictedByDefault,
|
|
114
|
+
updatedAt: updatedRecord ? updatedRecord.updatedAt : null,
|
|
115
|
+
})
|
|
74
116
|
}
|
|
75
117
|
|
|
76
118
|
const settingsResponseSchema = z.object({
|
|
77
119
|
newEntitiesRestrictedByDefault: z.boolean(),
|
|
120
|
+
updatedAt: z.string().nullable().optional(),
|
|
78
121
|
})
|
|
79
122
|
|
|
80
123
|
export const openApi: OpenApiRouteDoc = {
|
|
@@ -94,9 +137,10 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
94
137
|
description: 'Sets the tenant-scoped default-restricted policy for new custom entities.',
|
|
95
138
|
requestBody: { schema: putBodySchema },
|
|
96
139
|
responses: [
|
|
97
|
-
{ status: 200, description: 'Updated settings', schema: z.object({ ok: z.boolean(), newEntitiesRestrictedByDefault: z.boolean() }) },
|
|
140
|
+
{ status: 200, description: 'Updated settings', schema: z.object({ ok: z.boolean(), newEntitiesRestrictedByDefault: z.boolean(), updatedAt: z.string().nullable().optional() }) },
|
|
98
141
|
{ status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },
|
|
99
142
|
{ status: 401, description: 'Missing authentication', schema: z.object({ error: z.string() }) },
|
|
143
|
+
{ status: 409, description: 'Optimistic lock conflict', schema: z.object({ code: z.string(), error: z.string() }) },
|
|
100
144
|
],
|
|
101
145
|
},
|
|
102
146
|
},
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const settingsIcon = React.createElement(
|
|
4
|
+
'svg',
|
|
5
|
+
{
|
|
6
|
+
width: 16,
|
|
7
|
+
height: 16,
|
|
8
|
+
viewBox: '0 0 24 24',
|
|
9
|
+
fill: 'none',
|
|
10
|
+
stroke: 'currentColor',
|
|
11
|
+
strokeWidth: 2,
|
|
12
|
+
strokeLinecap: 'round',
|
|
13
|
+
strokeLinejoin: 'round',
|
|
14
|
+
},
|
|
15
|
+
React.createElement('circle', { cx: 12, cy: 12, r: 3 }),
|
|
16
|
+
React.createElement('path', {
|
|
17
|
+
d: 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'
|
|
18
|
+
})
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export const metadata = {
|
|
22
|
+
requireAuth: true,
|
|
23
|
+
requireFeatures: ['entities.definitions.manage'],
|
|
24
|
+
pageTitle: 'Custom Entities',
|
|
25
|
+
pageTitleKey: 'entities.settings.title',
|
|
26
|
+
pageGroup: 'Module Configs',
|
|
27
|
+
pageGroupKey: 'settings.sections.moduleConfigs',
|
|
28
|
+
pageOrder: 5,
|
|
29
|
+
icon: settingsIcon,
|
|
30
|
+
pageContext: 'settings' as const,
|
|
31
|
+
breadcrumb: [
|
|
32
|
+
{ label: 'Custom Entities', labelKey: 'entities.settings.title' },
|
|
33
|
+
],
|
|
34
|
+
} as const
|