@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.
Files changed (228) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_deal_person_link/index.js +2 -0
  3. package/dist/generated/entities/customer_deal_person_link/index.js.map +2 -2
  4. package/dist/generated/entities/customer_user_invitation/index.js +2 -0
  5. package/dist/generated/entities/customer_user_invitation/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/auth/lib/backendChrome.js +10 -9
  9. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  10. package/dist/modules/auth/lib/tenantAccess.js +7 -0
  11. package/dist/modules/auth/lib/tenantAccess.js.map +2 -2
  12. package/dist/modules/business_rules/api/openmercato-call-options/route.js +85 -0
  13. package/dist/modules/business_rules/api/openmercato-call-options/route.js.map +7 -0
  14. package/dist/modules/business_rules/api/rules/route.js +71 -4
  15. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  16. package/dist/modules/business_rules/components/ActionBuilder.js +47 -4
  17. package/dist/modules/business_rules/components/ActionBuilder.js.map +3 -3
  18. package/dist/modules/business_rules/components/ActionRow.js +106 -16
  19. package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
  20. package/dist/modules/business_rules/components/utils/actionValidation.js +6 -0
  21. package/dist/modules/business_rules/components/utils/actionValidation.js.map +2 -2
  22. package/dist/modules/business_rules/lib/action-executor.js +148 -0
  23. package/dist/modules/business_rules/lib/action-executor.js.map +2 -2
  24. package/dist/modules/business_rules/lib/openmercato-call-options-types.js +5 -0
  25. package/dist/modules/business_rules/lib/openmercato-call-options-types.js.map +7 -0
  26. package/dist/modules/business_rules/lib/openmercato-call-options.js +231 -0
  27. package/dist/modules/business_rules/lib/openmercato-call-options.js.map +7 -0
  28. package/dist/modules/business_rules/lib/rule-engine.js +5 -1
  29. package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
  30. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js +11 -1
  31. package/dist/modules/communication_channels/api/post/webhook/[provider]/route.js.map +2 -2
  32. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js +11 -6
  33. package/dist/modules/communication_channels/api/post/webhooks/gmail/route.js.map +2 -2
  34. package/dist/modules/configs/lib/upgrade-actions.js +12 -0
  35. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/admin/users/[id].js +10 -1
  37. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/users-invite.js +61 -4
  39. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
  40. package/dist/modules/customer_accounts/api/portal/users-invite.js +20 -2
  41. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  42. package/dist/modules/customer_accounts/data/entities.js +3 -0
  43. package/dist/modules/customer_accounts/data/entities.js.map +2 -2
  44. package/dist/modules/customer_accounts/data/validators.js +1 -0
  45. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  46. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js +26 -0
  47. package/dist/modules/customer_accounts/emails/CustomerInvitationEmail.js.map +7 -0
  48. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js +29 -3
  49. package/dist/modules/customer_accounts/lib/customerEntityOwnership.js.map +2 -2
  50. package/dist/modules/customer_accounts/lib/customerUrl.js +19 -10
  51. package/dist/modules/customer_accounts/lib/customerUrl.js.map +2 -2
  52. package/dist/modules/customer_accounts/lib/invitationEmail.js +35 -0
  53. package/dist/modules/customer_accounts/lib/invitationEmail.js.map +7 -0
  54. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js +13 -0
  55. package/dist/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.js.map +7 -0
  56. package/dist/modules/customer_accounts/services/customerInvitationService.js +32 -2
  57. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  58. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js +47 -7
  59. package/dist/modules/customer_accounts/subscribers/autoLinkCrm.js.map +2 -2
  60. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +2 -2
  61. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  62. package/dist/modules/customer_accounts/widgets/injection-table.js +1 -1
  63. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  64. package/dist/modules/customers/api/deals/[id]/people/route.js +4 -2
  65. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  66. package/dist/modules/customers/api/deals/[id]/route.js +7 -3
  67. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  68. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +11 -1
  69. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  70. package/dist/modules/customers/backend/customers/deals/[id]/page.js +14 -8
  71. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  72. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +21 -6
  73. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  74. package/dist/modules/customers/commands/deals.js +48 -10
  75. package/dist/modules/customers/commands/deals.js.map +2 -2
  76. package/dist/modules/customers/commands/people.js +2 -0
  77. package/dist/modules/customers/commands/people.js.map +2 -2
  78. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +26 -6
  79. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  80. package/dist/modules/customers/components/detail/DealDetailTabs.js +30 -8
  81. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  82. package/dist/modules/customers/components/detail/PersonDetailTabs.js +30 -8
  83. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  84. package/dist/modules/customers/data/entities.js +8 -0
  85. package/dist/modules/customers/data/entities.js.map +2 -2
  86. package/dist/modules/customers/data/validators.js +2 -1
  87. package/dist/modules/customers/data/validators.js.map +2 -2
  88. package/dist/modules/customers/migrations/Migration20260723210000_customers.js +15 -0
  89. package/dist/modules/customers/migrations/Migration20260723210000_customers.js.map +7 -0
  90. package/dist/modules/directory/api/tenants/route.js +8 -2
  91. package/dist/modules/directory/api/tenants/route.js.map +2 -2
  92. package/dist/modules/directory/commands/tenants.js +6 -0
  93. package/dist/modules/directory/commands/tenants.js.map +2 -2
  94. package/dist/modules/entities/api/entity-settings.js +59 -9
  95. package/dist/modules/entities/api/entity-settings.js.map +2 -2
  96. package/dist/modules/entities/backend/config/settings/page.js +10 -0
  97. package/dist/modules/entities/backend/config/settings/page.js.map +7 -0
  98. package/dist/modules/entities/backend/config/settings/page.meta.js +36 -0
  99. package/dist/modules/entities/backend/config/settings/page.meta.js.map +7 -0
  100. package/dist/modules/entities/backend/entities/user/create/page.js +16 -7
  101. package/dist/modules/entities/backend/entities/user/create/page.js.map +2 -2
  102. package/dist/modules/entities/components/EntitySettingsManager.js +136 -0
  103. package/dist/modules/entities/components/EntitySettingsManager.js.map +7 -0
  104. package/dist/modules/inbox_ops/api/webhook/inbound.js +14 -10
  105. package/dist/modules/inbox_ops/api/webhook/inbound.js.map +2 -2
  106. package/dist/modules/payment_gateways/api/sessions/route.js +1 -3
  107. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  108. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +11 -1
  109. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  110. package/dist/modules/payment_gateways/data/entities.js +4 -0
  111. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  112. package/dist/modules/payment_gateways/lib/gateway-service.js +15 -6
  113. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  114. package/dist/modules/payment_gateways/lib/session-idempotency.js +33 -0
  115. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  116. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js +18 -0
  117. package/dist/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.js.map +7 -0
  118. package/dist/modules/payment_gateways/setup.js +47 -0
  119. package/dist/modules/payment_gateways/setup.js.map +2 -2
  120. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +50 -0
  121. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +7 -0
  122. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +134 -0
  123. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +7 -0
  124. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js +11 -0
  125. package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.js.map +7 -0
  126. package/dist/modules/portal/lib/navigation.js +7 -0
  127. package/dist/modules/portal/lib/navigation.js.map +7 -0
  128. package/dist/modules/query_index/lib/engine.js +43 -0
  129. package/dist/modules/query_index/lib/engine.js.map +2 -2
  130. package/dist/modules/query_index/lib/search-tokens.js +15 -5
  131. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  132. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +11 -1
  133. package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +2 -2
  134. package/generated/entities/customer_deal_person_link/index.ts +1 -0
  135. package/generated/entities/customer_user_invitation/index.ts +1 -0
  136. package/generated/entity-fields-registry.ts +2 -0
  137. package/package.json +7 -7
  138. package/src/modules/auth/lib/backendChrome.tsx +15 -9
  139. package/src/modules/auth/lib/tenantAccess.ts +8 -0
  140. package/src/modules/business_rules/api/openmercato-call-options/route.ts +94 -0
  141. package/src/modules/business_rules/api/rules/route.ts +92 -4
  142. package/src/modules/business_rules/components/ActionBuilder.tsx +54 -5
  143. package/src/modules/business_rules/components/ActionRow.tsx +156 -18
  144. package/src/modules/business_rules/components/utils/actionValidation.ts +6 -0
  145. package/src/modules/business_rules/i18n/de.json +12 -0
  146. package/src/modules/business_rules/i18n/en.json +12 -0
  147. package/src/modules/business_rules/i18n/es.json +12 -0
  148. package/src/modules/business_rules/i18n/pl.json +12 -0
  149. package/src/modules/business_rules/lib/action-executor.ts +198 -0
  150. package/src/modules/business_rules/lib/openmercato-call-options-types.ts +31 -0
  151. package/src/modules/business_rules/lib/openmercato-call-options.ts +297 -0
  152. package/src/modules/business_rules/lib/rule-engine.ts +4 -0
  153. package/src/modules/communication_channels/api/post/webhook/[provider]/route.ts +11 -1
  154. package/src/modules/communication_channels/api/post/webhooks/gmail/route.ts +17 -11
  155. package/src/modules/configs/lib/upgrade-actions.ts +14 -0
  156. package/src/modules/customer_accounts/api/admin/users/[id].ts +16 -1
  157. package/src/modules/customer_accounts/api/admin/users-invite.ts +78 -3
  158. package/src/modules/customer_accounts/api/portal/users-invite.ts +22 -1
  159. package/src/modules/customer_accounts/data/entities.ts +3 -0
  160. package/src/modules/customer_accounts/data/validators.ts +1 -0
  161. package/src/modules/customer_accounts/emails/CustomerInvitationEmail.tsx +45 -0
  162. package/src/modules/customer_accounts/i18n/de.json +6 -0
  163. package/src/modules/customer_accounts/i18n/en.json +6 -0
  164. package/src/modules/customer_accounts/i18n/es.json +6 -0
  165. package/src/modules/customer_accounts/i18n/pl.json +6 -0
  166. package/src/modules/customer_accounts/lib/customerEntityOwnership.ts +68 -2
  167. package/src/modules/customer_accounts/lib/customerUrl.ts +27 -11
  168. package/src/modules/customer_accounts/lib/invitationEmail.ts +42 -0
  169. package/src/modules/customer_accounts/migrations/.snapshot-open-mercato.json +16 -0
  170. package/src/modules/customer_accounts/migrations/Migration20260723120000_customer_accounts.ts +13 -0
  171. package/src/modules/customer_accounts/services/customerInvitationService.ts +56 -3
  172. package/src/modules/customer_accounts/subscribers/autoLinkCrm.ts +75 -8
  173. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -2
  174. package/src/modules/customer_accounts/widgets/injection-table.ts +4 -1
  175. package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
  176. package/src/modules/customers/api/deals/[id]/route.ts +10 -2
  177. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +12 -1
  178. package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +1 -0
  179. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -9
  180. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +24 -6
  181. package/src/modules/customers/commands/deals.ts +50 -9
  182. package/src/modules/customers/commands/people.ts +3 -0
  183. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +30 -7
  184. package/src/modules/customers/components/detail/DealDetailTabs.tsx +34 -9
  185. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +34 -9
  186. package/src/modules/customers/data/entities.ts +9 -1
  187. package/src/modules/customers/data/validators.ts +1 -0
  188. package/src/modules/customers/i18n/de.json +1 -0
  189. package/src/modules/customers/i18n/en.json +1 -0
  190. package/src/modules/customers/i18n/es.json +1 -0
  191. package/src/modules/customers/i18n/pl.json +1 -0
  192. package/src/modules/customers/migrations/.snapshot-open-mercato.json +27 -1
  193. package/src/modules/customers/migrations/Migration20260723210000_customers.ts +15 -0
  194. package/src/modules/directory/api/tenants/route.ts +9 -1
  195. package/src/modules/directory/commands/tenants.ts +6 -0
  196. package/src/modules/entities/api/entity-settings.ts +66 -22
  197. package/src/modules/entities/backend/config/settings/page.meta.ts +34 -0
  198. package/src/modules/entities/backend/config/settings/page.tsx +12 -0
  199. package/src/modules/entities/backend/entities/user/create/page.tsx +16 -7
  200. package/src/modules/entities/components/EntitySettingsManager.tsx +160 -0
  201. package/src/modules/entities/i18n/de.json +9 -0
  202. package/src/modules/entities/i18n/en.json +9 -0
  203. package/src/modules/entities/i18n/es.json +9 -0
  204. package/src/modules/entities/i18n/pl.json +9 -0
  205. package/src/modules/inbox_ops/api/webhook/inbound.ts +14 -12
  206. package/src/modules/payment_gateways/api/sessions/route.ts +1 -3
  207. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +13 -1
  208. package/src/modules/payment_gateways/data/entities.ts +5 -0
  209. package/src/modules/payment_gateways/i18n/de.json +5 -1
  210. package/src/modules/payment_gateways/i18n/en.json +5 -1
  211. package/src/modules/payment_gateways/i18n/es.json +5 -1
  212. package/src/modules/payment_gateways/i18n/pl.json +5 -1
  213. package/src/modules/payment_gateways/lib/gateway-service.ts +17 -8
  214. package/src/modules/payment_gateways/lib/session-idempotency.ts +36 -0
  215. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +10 -1
  216. package/src/modules/payment_gateways/migrations/Migration20260803103035_payment_gateways.ts +17 -0
  217. package/src/modules/payment_gateways/setup.ts +62 -0
  218. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +63 -0
  219. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.meta.ts +9 -0
  220. package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +164 -0
  221. package/src/modules/portal/i18n/de.json +18 -0
  222. package/src/modules/portal/i18n/en.json +18 -0
  223. package/src/modules/portal/i18n/es.json +18 -0
  224. package/src/modules/portal/i18n/pl.json +18 -0
  225. package/src/modules/portal/lib/navigation.ts +3 -0
  226. package/src/modules/query_index/lib/engine.ts +52 -0
  227. package/src/modules/query_index/lib/search-tokens.ts +16 -4
  228. 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
- type CompanyDetailTabsProps = {
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
- export function CompanyDetailTabs({
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
- [builtInTabs, injectedTabs],
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
- type DealDetailTabsProps = {
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
- return SUPPORTED_TAB_IDS.has(tab as DealTabId) ? (tab as DealTabId) : 'activities'
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
- export function DealDetailTabs({
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
- [builtInTabs, injectedTabs],
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
- type PersonDetailTabsProps = {
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
- return SUPPORTED_TAB_IDS.has(tab as PersonTabId) ? (tab as PersonTabId) : 'activities'
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
- export function PersonDetailTabs({
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
- [builtInTabs, injectedTabs],
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 ModuleConfigServiceLike
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 newEntitiesRestrictedByDefault = await readPolicy(auth.tenantId ?? null)
49
- return NextResponse.json({ newEntitiesRestrictedByDefault })
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 { resolve } = await createRequestContainer()
66
- const moduleConfigService = resolve('moduleConfigService') as ModuleConfigServiceLike
67
- await moduleConfigService.setValue(
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
- return NextResponse.json({ ok: true, newEntitiesRestrictedByDefault: parsed.data.newEntitiesRestrictedByDefault })
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