@open-mercato/core 0.6.6 → 0.6.7-develop.6552.1.5208a19369

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 (268) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/gateway_session_initialization/index.js +23 -0
  3. package/dist/generated/entities/gateway_session_initialization/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +1 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +12 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/api_keys/api/keys/route.js +30 -13
  9. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  10. package/dist/modules/api_keys/cli.js +2 -1
  11. package/dist/modules/api_keys/cli.js.map +2 -2
  12. package/dist/modules/attachments/api/file/[id]/route.js +9 -7
  13. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  14. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +9 -7
  15. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  16. package/dist/modules/attachments/api/route.js +21 -17
  17. package/dist/modules/attachments/api/route.js.map +2 -2
  18. package/dist/modules/attachments/components/AttachmentContentPreview.js +6 -35
  19. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  20. package/dist/modules/attachments/lib/reconcileOrganization.js +110 -0
  21. package/dist/modules/attachments/lib/reconcileOrganization.js.map +7 -0
  22. package/dist/modules/attachments/lib/requestScope.js +10 -0
  23. package/dist/modules/attachments/lib/requestScope.js.map +7 -0
  24. package/dist/modules/audit_logs/backend/audit-logs/page.js +14 -24
  25. package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
  26. package/dist/modules/auth/api/autologin.js +85 -0
  27. package/dist/modules/auth/api/autologin.js.map +7 -0
  28. package/dist/modules/auth/api/users/route.js +6 -2
  29. package/dist/modules/auth/api/users/route.js.map +2 -2
  30. package/dist/modules/auth/cli.js +3 -2
  31. package/dist/modules/auth/cli.js.map +2 -2
  32. package/dist/modules/auth/lib/autologin.js +19 -0
  33. package/dist/modules/auth/lib/autologin.js.map +7 -0
  34. package/dist/modules/configs/lib/upgrade-actions.js +22 -0
  35. package/dist/modules/configs/lib/upgrade-actions.js.map +2 -2
  36. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +1 -0
  37. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
  38. package/dist/modules/customer_accounts/api/admin/roles/[id].js +17 -3
  39. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  40. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +6 -1
  41. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
  42. package/dist/modules/customer_accounts/api/portal/users-invite.js +6 -1
  43. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  44. package/dist/modules/customer_accounts/services/customerInvitationService.js +1 -0
  45. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  46. package/dist/modules/customer_accounts/services/customerRbacService.js +6 -1
  47. package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
  48. package/dist/modules/customer_accounts/widgets/injection-table.js +0 -2
  49. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  50. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -1
  51. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  52. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +2 -2
  53. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
  54. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +37 -43
  55. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  56. package/dist/modules/customers/components/detail/DealDetailTabs.js +23 -35
  57. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  58. package/dist/modules/customers/components/detail/DetailTabsLayout.js +6 -21
  59. package/dist/modules/customers/components/detail/DetailTabsLayout.js.map +2 -2
  60. package/dist/modules/customers/components/detail/MobilePersonDetail.js +26 -32
  61. package/dist/modules/customers/components/detail/MobilePersonDetail.js.map +2 -2
  62. package/dist/modules/customers/components/detail/PersonDetailTabs.js +29 -39
  63. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  64. package/dist/modules/customers/components/detail/useDealsAccess.js +16 -0
  65. package/dist/modules/customers/components/detail/useDealsAccess.js.map +7 -0
  66. package/dist/modules/dashboards/api/roles/widgets/route.js +22 -3
  67. package/dist/modules/dashboards/api/roles/widgets/route.js.map +2 -2
  68. package/dist/modules/dashboards/api/users/widgets/route.js +22 -3
  69. package/dist/modules/dashboards/api/users/widgets/route.js.map +2 -2
  70. package/dist/modules/dashboards/cli.js +4 -3
  71. package/dist/modules/dashboards/cli.js.map +2 -2
  72. package/dist/modules/dashboards/lib/widgetAssignmentScope.js +9 -1
  73. package/dist/modules/dashboards/lib/widgetAssignmentScope.js.map +2 -2
  74. package/dist/modules/dictionaries/commands/entries.js +7 -14
  75. package/dist/modules/dictionaries/commands/entries.js.map +2 -2
  76. package/dist/modules/dictionaries/commands/entry-operations.js +7 -16
  77. package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
  78. package/dist/modules/dictionaries/commands/factory.js +9 -3
  79. package/dist/modules/dictionaries/commands/factory.js.map +2 -2
  80. package/dist/modules/dictionaries/fields/dictionary.js +56 -41
  81. package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
  82. package/dist/modules/directory/utils/organizationScope.js +10 -3
  83. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  84. package/dist/modules/entities/api/records.js +5 -4
  85. package/dist/modules/entities/api/records.js.map +2 -2
  86. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js +1 -0
  87. package/dist/modules/entities/backend/entities/system/[entityId]/page.meta.js.map +2 -2
  88. package/dist/modules/entities/backend/entities/user/[entityId]/page.js +49 -49
  89. package/dist/modules/entities/backend/entities/user/[entityId]/page.js.map +2 -2
  90. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js +1 -0
  91. package/dist/modules/entities/backend/entities/user/[entityId]/page.meta.js.map +2 -2
  92. package/dist/modules/entities/cli.js +2 -1
  93. package/dist/modules/entities/cli.js.map +2 -2
  94. package/dist/modules/feature_toggles/api/check/boolean/route.js +0 -14
  95. package/dist/modules/feature_toggles/api/check/boolean/route.js.map +2 -2
  96. package/dist/modules/feature_toggles/defaults.json +8 -0
  97. package/dist/modules/integrations/backend/integrations/[id]/page.js +9 -80
  98. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  99. package/dist/modules/payment_gateways/data/entities.js +45 -0
  100. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  101. package/dist/modules/payment_gateways/lib/gateway-service.js +173 -38
  102. package/dist/modules/payment_gateways/lib/gateway-service.js.map +3 -3
  103. package/dist/modules/payment_gateways/lib/session-idempotency.js +104 -0
  104. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +7 -0
  105. package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js +11 -0
  106. package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js.map +7 -0
  107. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js +16 -14
  108. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js.map +2 -2
  109. package/dist/modules/planner/components/AvailabilityRulesEditor.js +18 -30
  110. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  111. package/dist/modules/query_index/api/status.js +2 -8
  112. package/dist/modules/query_index/api/status.js.map +2 -2
  113. package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -24
  114. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  115. package/dist/modules/sales/backend/sales/channels/create/page.js +6 -0
  116. package/dist/modules/sales/backend/sales/channels/create/page.js.map +2 -2
  117. package/dist/modules/sales/backend/sales/channels/create/page.meta.js +2 -0
  118. package/dist/modules/sales/backend/sales/channels/create/page.meta.js.map +2 -2
  119. package/dist/modules/sales/backend/sales/channels/offers/page.js +6 -0
  120. package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
  121. package/dist/modules/sales/backend/sales/channels/offers/page.meta.js +2 -0
  122. package/dist/modules/sales/backend/sales/channels/offers/page.meta.js.map +2 -2
  123. package/dist/modules/sales/backend/sales/channels/page.js +6 -0
  124. package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
  125. package/dist/modules/sales/backend/sales/channels/page.meta.js +2 -0
  126. package/dist/modules/sales/backend/sales/channels/page.meta.js.map +2 -2
  127. package/dist/modules/sales/backend/sales/documents/[id]/page.js +6 -4
  128. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  129. package/dist/modules/sales/commands/documents.js +44 -0
  130. package/dist/modules/sales/commands/documents.js.map +2 -2
  131. package/dist/modules/sales/commands/returns.js +2 -27
  132. package/dist/modules/sales/commands/returns.js.map +2 -2
  133. package/dist/modules/sales/components/SalesChannelsDisabledNotice.js +15 -0
  134. package/dist/modules/sales/components/SalesChannelsDisabledNotice.js.map +7 -0
  135. package/dist/modules/sales/components/documents/AddressesSection.js +6 -2
  136. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  137. package/dist/modules/sales/components/documents/ItemsSection.js +1 -0
  138. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  139. package/dist/modules/sales/components/documents/LineItemDialog.js +9 -0
  140. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  141. package/dist/modules/sales/components/documents/SalesDocumentForm.js +51 -3
  142. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  143. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +10 -8
  144. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  145. package/dist/modules/sales/components/documents/ShipmentDialog.js +59 -38
  146. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  147. package/dist/modules/sales/components/useSalesChannelsEnabled.js +44 -0
  148. package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +7 -0
  149. package/dist/modules/sales/lib/salesChannelsToggle.js +18 -0
  150. package/dist/modules/sales/lib/salesChannelsToggle.js.map +7 -0
  151. package/dist/modules/sales/lib/salesChannelsToggleId.js +5 -0
  152. package/dist/modules/sales/lib/salesChannelsToggleId.js.map +7 -0
  153. package/dist/modules/sales/lib/shipments/snapshots.js +27 -1
  154. package/dist/modules/sales/lib/shipments/snapshots.js.map +2 -2
  155. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +22 -25
  156. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  157. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -14
  158. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  159. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +10 -29
  160. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +2 -2
  161. package/dist/modules/sync_excel/lib/adapters/customers.js +19 -20
  162. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  163. package/dist/modules/sync_excel/lib/parser.js +185 -0
  164. package/dist/modules/sync_excel/lib/parser.js.map +2 -2
  165. package/dist/modules/sync_excel/lib/upload-storage.js +20 -1
  166. package/dist/modules/sync_excel/lib/upload-storage.js.map +2 -2
  167. package/generated/entities/gateway_session_initialization/index.ts +10 -0
  168. package/generated/entities.ids.generated.ts +1 -0
  169. package/generated/entity-fields-registry.ts +12 -0
  170. package/package.json +9 -8
  171. package/src/modules/api_keys/api/keys/route.ts +36 -13
  172. package/src/modules/api_keys/cli.ts +2 -3
  173. package/src/modules/attachments/api/file/[id]/route.ts +11 -7
  174. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +11 -7
  175. package/src/modules/attachments/api/route.ts +26 -21
  176. package/src/modules/attachments/components/AttachmentContentPreview.tsx +9 -44
  177. package/src/modules/attachments/lib/reconcileOrganization.ts +188 -0
  178. package/src/modules/attachments/lib/requestScope.ts +27 -0
  179. package/src/modules/audit_logs/backend/audit-logs/page.tsx +12 -22
  180. package/src/modules/auth/api/autologin.ts +100 -0
  181. package/src/modules/auth/api/users/route.ts +11 -2
  182. package/src/modules/auth/cli.ts +3 -2
  183. package/src/modules/auth/lib/autologin.ts +36 -0
  184. package/src/modules/configs/i18n/de.json +5 -1
  185. package/src/modules/configs/i18n/en.json +5 -1
  186. package/src/modules/configs/i18n/es.json +5 -1
  187. package/src/modules/configs/i18n/pl.json +5 -1
  188. package/src/modules/configs/lib/upgrade-actions.ts +24 -0
  189. package/src/modules/customer_accounts/AGENTS.md +8 -6
  190. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +1 -0
  191. package/src/modules/customer_accounts/api/admin/roles/[id].ts +17 -3
  192. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +6 -1
  193. package/src/modules/customer_accounts/api/portal/users-invite.ts +6 -1
  194. package/src/modules/customer_accounts/services/customerInvitationService.ts +1 -0
  195. package/src/modules/customer_accounts/services/customerRbacService.ts +9 -2
  196. package/src/modules/customer_accounts/widgets/injection-table.ts +0 -2
  197. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +11 -1
  198. package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +6 -2
  199. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +39 -55
  200. package/src/modules/customers/components/detail/DealDetailTabs.tsx +24 -49
  201. package/src/modules/customers/components/detail/DetailTabsLayout.tsx +13 -24
  202. package/src/modules/customers/components/detail/MobilePersonDetail.tsx +25 -31
  203. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +29 -50
  204. package/src/modules/customers/components/detail/useDealsAccess.ts +19 -0
  205. package/src/modules/dashboards/api/roles/widgets/route.ts +22 -3
  206. package/src/modules/dashboards/api/users/widgets/route.ts +22 -3
  207. package/src/modules/dashboards/cli.ts +4 -9
  208. package/src/modules/dashboards/lib/widgetAssignmentScope.ts +18 -0
  209. package/src/modules/dictionaries/commands/entries.ts +7 -16
  210. package/src/modules/dictionaries/commands/entry-operations.ts +8 -18
  211. package/src/modules/dictionaries/commands/factory.ts +8 -2
  212. package/src/modules/dictionaries/fields/dictionary.tsx +55 -32
  213. package/src/modules/dictionaries/i18n/de.json +2 -0
  214. package/src/modules/dictionaries/i18n/en.json +2 -0
  215. package/src/modules/dictionaries/i18n/es.json +2 -0
  216. package/src/modules/dictionaries/i18n/pl.json +2 -0
  217. package/src/modules/directory/utils/organizationScope.ts +36 -2
  218. package/src/modules/entities/api/records.ts +5 -7
  219. package/src/modules/entities/backend/entities/system/[entityId]/page.meta.ts +1 -0
  220. package/src/modules/entities/backend/entities/user/[entityId]/page.meta.ts +1 -0
  221. package/src/modules/entities/backend/entities/user/[entityId]/page.tsx +51 -49
  222. package/src/modules/entities/cli.ts +2 -1
  223. package/src/modules/entities/i18n/de.json +34 -0
  224. package/src/modules/entities/i18n/en.json +34 -0
  225. package/src/modules/entities/i18n/es.json +34 -0
  226. package/src/modules/entities/i18n/pl.json +34 -0
  227. package/src/modules/feature_toggles/api/check/boolean/route.ts +0 -15
  228. package/src/modules/feature_toggles/defaults.json +8 -0
  229. package/src/modules/integrations/backend/integrations/[id]/page.tsx +16 -59
  230. package/src/modules/payment_gateways/data/entities.ts +39 -0
  231. package/src/modules/payment_gateways/lib/gateway-service.ts +211 -40
  232. package/src/modules/payment_gateways/lib/session-idempotency.ts +142 -0
  233. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +236 -0
  234. package/src/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.ts +10 -0
  235. package/src/modules/planner/backend/planner/availability-rulesets/[id]/page.tsx +14 -20
  236. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +16 -34
  237. package/src/modules/query_index/api/status.ts +2 -8
  238. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +27 -37
  239. package/src/modules/sales/backend/sales/channels/create/page.meta.ts +3 -0
  240. package/src/modules/sales/backend/sales/channels/create/page.tsx +7 -0
  241. package/src/modules/sales/backend/sales/channels/offers/page.meta.ts +3 -0
  242. package/src/modules/sales/backend/sales/channels/offers/page.tsx +7 -0
  243. package/src/modules/sales/backend/sales/channels/page.meta.ts +4 -0
  244. package/src/modules/sales/backend/sales/channels/page.tsx +7 -0
  245. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +7 -5
  246. package/src/modules/sales/commands/documents.ts +65 -0
  247. package/src/modules/sales/commands/returns.ts +2 -32
  248. package/src/modules/sales/components/SalesChannelsDisabledNotice.tsx +21 -0
  249. package/src/modules/sales/components/documents/AddressesSection.tsx +6 -2
  250. package/src/modules/sales/components/documents/ItemsSection.tsx +5 -0
  251. package/src/modules/sales/components/documents/LineItemDialog.tsx +10 -0
  252. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +53 -3
  253. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +10 -8
  254. package/src/modules/sales/components/documents/ShipmentDialog.tsx +65 -40
  255. package/src/modules/sales/components/useSalesChannelsEnabled.ts +59 -0
  256. package/src/modules/sales/i18n/de.json +5 -0
  257. package/src/modules/sales/i18n/en.json +5 -0
  258. package/src/modules/sales/i18n/es.json +5 -0
  259. package/src/modules/sales/i18n/pl.json +5 -0
  260. package/src/modules/sales/lib/salesChannelsToggle.ts +26 -0
  261. package/src/modules/sales/lib/salesChannelsToggleId.ts +1 -0
  262. package/src/modules/sales/lib/shipments/snapshots.ts +30 -0
  263. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +25 -34
  264. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +11 -17
  265. package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +11 -28
  266. package/src/modules/sync_excel/lib/adapters/customers.ts +20 -22
  267. package/src/modules/sync_excel/lib/parser.ts +245 -0
  268. package/src/modules/sync_excel/lib/upload-storage.ts +23 -1
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import type { SectionAction } from '@open-mercato/ui/backend/detail'
5
5
  import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
6
7
  import { cn } from '@open-mercato/shared/lib/utils'
7
8
  import { Plus } from 'lucide-react'
8
9
 
@@ -46,31 +47,19 @@ export function DetailTabsLayout<TId extends string = string>({
46
47
  return (
47
48
  <div className={cn('space-y-4', className)}>
48
49
  <div className={cn('flex flex-wrap items-center justify-between gap-3', headerClassName)}>
49
- <nav
50
- className={cn('flex flex-wrap items-center gap-3 text-sm', navClassName)}
51
- role="tablist"
52
- aria-label={navAriaLabel}
50
+ <Tabs
51
+ value={activeTab}
52
+ onValueChange={(value) => handleTabChange(value as TId)}
53
+ variant="underline"
53
54
  >
54
- {tabs.map((tab) => (
55
- <Button
56
- key={tab.id}
57
- type="button"
58
- variant="ghost"
59
- size="sm"
60
- role="tab"
61
- aria-selected={activeTab === tab.id}
62
- onClick={() => handleTabChange(tab.id)}
63
- className={cn(
64
- 'h-auto rounded-none border-b-2 px-0 py-1',
65
- activeTab === tab.id
66
- ? 'border-accent-indigo text-foreground hover:bg-transparent'
67
- : 'border-transparent text-muted-foreground hover:text-foreground hover:bg-transparent'
68
- )}
69
- >
70
- {tab.label}
71
- </Button>
72
- ))}
73
- </nav>
55
+ <TabsList className={cn('h-auto flex-wrap border-b-0', navClassName)} aria-label={navAriaLabel}>
56
+ {tabs.map((tab) => (
57
+ <TabsTrigger key={tab.id} value={tab.id}>
58
+ {tab.label}
59
+ </TabsTrigger>
60
+ ))}
61
+ </TabsList>
62
+ </Tabs>
74
63
  {sectionAction ? (
75
64
  <Button
76
65
  type="button"
@@ -3,8 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { useRouter, useSearchParams } from 'next/navigation'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
- import { cn } from '@open-mercato/shared/lib/utils'
7
- import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
8
7
 
9
8
  export type MobilePersonZone = 'details' | 'activity'
10
9
 
@@ -80,35 +79,30 @@ export function MobilePersonDetail({
80
79
 
81
80
  return (
82
81
  <div className="space-y-4">
83
- <div
84
- role="tablist"
85
- aria-label={t(
86
- 'customers.people.mobile.zoneSwitcher.ariaLabel',
87
- 'Zone selector',
88
- )}
89
- className="grid grid-cols-2 gap-1 rounded-lg border border-border/70 bg-muted/20 p-1"
90
- onKeyDown={handleKeyDown}
91
- >
92
- {ZONES.map((entry) => {
93
- const isActive = zone === entry.id
94
- return (
95
- <Button
96
- key={entry.id}
97
- type="button"
98
- role="tab"
99
- aria-selected={isActive}
100
- aria-controls={`mobile-person-zone-${entry.id}`}
101
- variant={isActive ? 'default' : 'ghost'}
102
- className={cn(
103
- 'h-11 rounded-lg text-sm font-semibold',
104
- isActive ? '' : 'text-muted-foreground',
105
- )}
106
- onClick={() => handleZoneChange(entry.id)}
107
- >
108
- {t(entry.labelKey, entry.fallback)}
109
- </Button>
110
- )
111
- })}
82
+ <div onKeyDown={handleKeyDown}>
83
+ <Tabs
84
+ value={zone}
85
+ onValueChange={(value) => handleZoneChange(value as MobilePersonZone)}
86
+ variant="underline"
87
+ >
88
+ <TabsList
89
+ aria-label={t(
90
+ 'customers.people.mobile.zoneSwitcher.ariaLabel',
91
+ 'Zone selector',
92
+ )}
93
+ className="w-full"
94
+ >
95
+ {ZONES.map((entry) => (
96
+ <TabsTrigger
97
+ key={entry.id}
98
+ value={entry.id}
99
+ className="flex-1 justify-center"
100
+ >
101
+ {t(entry.labelKey, entry.fallback)}
102
+ </TabsTrigger>
103
+ ))}
104
+ </TabsList>
105
+ </Tabs>
112
106
  </div>
113
107
  <div
114
108
  id={`mobile-person-zone-${zone}`}
@@ -1,9 +1,9 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { cn } from '@open-mercato/shared/lib/utils'
5
4
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
5
  import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
7
7
  import {
8
8
  SquareCheckBig,
9
9
  Mail,
@@ -32,7 +32,7 @@ type TabDef = {
32
32
  id: PersonTabId
33
33
  label: string
34
34
  icon?: React.ReactNode
35
- badge?: React.ReactNode
35
+ count?: React.ReactNode
36
36
  }
37
37
 
38
38
  type PersonDetailTabsProps = {
@@ -56,21 +56,9 @@ export function resolveLegacyTab(tab: string | null | undefined): PersonTabId {
56
56
  return SUPPORTED_TAB_IDS.has(tab as PersonTabId) ? (tab as PersonTabId) : 'activities'
57
57
  }
58
58
 
59
- function CountBadge({ count }: { count: number }) {
60
- if (count <= 0) return null
61
- return (
62
- <span className="ml-1 rounded-full bg-muted px-1.5 py-0.5 text-xs font-medium leading-none text-muted-foreground">
63
- {count > 999 ? '999+' : count}
64
- </span>
65
- )
66
- }
67
-
68
- function NewBadge() {
69
- return (
70
- <span className="ml-1.5 rounded bg-foreground px-1.5 py-0.5 text-overline font-semibold leading-none text-background">
71
- NEW
72
- </span>
73
- )
59
+ function formatTabCount(count: number): string | number | undefined {
60
+ if (count <= 0) return undefined
61
+ return count > 999 ? '999+' : count
74
62
  }
75
63
 
76
64
  export function PersonDetailTabs({
@@ -94,7 +82,7 @@ export function PersonDetailTabs({
94
82
  id: 'activities',
95
83
  label: t('customers.people.detail.tabs.activities', 'Activities'),
96
84
  icon: <SquareCheckBig className="size-4" />,
97
- badge: <CountBadge count={activitiesCount} />,
85
+ count: formatTabCount(activitiesCount),
98
86
  },
99
87
  {
100
88
  id: 'emails',
@@ -105,37 +93,37 @@ export function PersonDetailTabs({
105
93
  id: 'deals',
106
94
  label: t('customers.people.detail.tabs.deals', 'Deals'),
107
95
  icon: <Briefcase className="size-4" />,
108
- badge: <CountBadge count={dealsCount} />,
96
+ count: formatTabCount(dealsCount),
109
97
  },
110
98
  {
111
99
  id: 'companies',
112
100
  label: t('customers.people.detail.tabs.companies', 'Companies'),
113
101
  icon: <Building2 className="size-4" />,
114
- badge: <CountBadge count={companiesCount} />,
102
+ count: formatTabCount(companiesCount),
115
103
  },
116
104
  {
117
105
  id: 'addresses',
118
106
  label: t('customers.people.detail.tabs.addresses', 'Addresses'),
119
107
  icon: <MapPin className="size-4" />,
120
- badge: <CountBadge count={addressesCount} />,
108
+ count: formatTabCount(addressesCount),
121
109
  },
122
110
  {
123
111
  id: 'tasks',
124
112
  label: t('customers.people.detail.tabs.tasks', 'Tasks'),
125
113
  icon: <Check className="size-4" />,
126
- badge: <CountBadge count={tasksCount} />,
114
+ count: formatTabCount(tasksCount),
127
115
  },
128
116
  {
129
117
  id: 'changelog',
130
118
  label: t('customers.people.detail.tabs.changelog', 'Change log'),
131
119
  icon: <History className="size-4" />,
132
- badge: <NewBadge />,
120
+ count: 'NEW',
133
121
  },
134
122
  {
135
123
  id: 'files',
136
124
  label: t('customers.people.detail.tabs.files', 'Files'),
137
125
  icon: <Paperclip className="size-4" />,
138
- badge: <CountBadge count={filesCount} />,
126
+ count: formatTabCount(filesCount),
139
127
  },
140
128
  ],
141
129
  [t, activitiesCount, dealsCount, companiesCount, addressesCount, tasksCount, filesCount],
@@ -155,33 +143,24 @@ export function PersonDetailTabs({
155
143
  return (
156
144
  <div>
157
145
  {/* Tab navigation — full width above both zones */}
158
- <div className="flex items-end justify-between gap-2 border-b" role="tablist" aria-label={t('customers.people.detail.tabs.label', 'Person detail sections')}>
159
- <nav className="-mb-px flex flex-1 gap-1 overflow-x-auto px-1">
160
- {allTabs.map((tab) => {
161
- const isActive = activeTab === tab.id
162
- return (
163
- <Button
164
- key={tab.id}
165
- type="button"
166
- variant="ghost"
167
- size="sm"
168
- role="tab"
169
- aria-selected={isActive}
170
- onClick={() => onTabChange(tab.id)}
171
- className={cn(
172
- 'h-auto shrink-0 rounded-none border-b-2 px-3 py-2.5 hover:bg-transparent',
173
- isActive
174
- ? 'border-foreground text-foreground font-semibold'
175
- : 'border-transparent text-muted-foreground hover:text-foreground',
176
- )}
177
- >
178
- {tab.icon && <span className="mr-1.5">{tab.icon}</span>}
146
+ <div className="flex items-end justify-between gap-2 border-b">
147
+ <Tabs
148
+ value={activeTab}
149
+ onValueChange={(value) => onTabChange(value as PersonTabId)}
150
+ variant="underline"
151
+ className="min-w-0 flex-1"
152
+ >
153
+ <TabsList
154
+ aria-label={t('customers.people.detail.tabs.label', 'Person detail sections')}
155
+ className="-mb-px w-full overflow-x-auto border-b-0 px-1"
156
+ >
157
+ {allTabs.map((tab) => (
158
+ <TabsTrigger key={tab.id} value={tab.id} leading={tab.icon} count={tab.count}>
179
159
  {tab.label}
180
- {tab.badge}
181
- </Button>
182
- )
183
- })}
184
- </nav>
160
+ </TabsTrigger>
161
+ ))}
162
+ </TabsList>
163
+ </Tabs>
185
164
  {sectionAction ? (
186
165
  <Button
187
166
  type="button"
@@ -0,0 +1,19 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { useBackendChrome } from '@open-mercato/ui/backend/BackendChromeProvider'
5
+ import { hasFeature } from '@open-mercato/shared/security/features'
6
+
7
+ export type DealsAccess = {
8
+ canViewDeals: boolean
9
+ isReady: boolean
10
+ }
11
+
12
+ export function useDealsAccess(): DealsAccess {
13
+ const { payload, isReady } = useBackendChrome()
14
+ const canViewDeals = React.useMemo(
15
+ () => hasFeature(payload?.grantedFeatures ?? [], 'customers.deals.view'),
16
+ [payload?.grantedFeatures],
17
+ )
18
+ return { canViewDeals, isReady }
19
+ }
@@ -6,7 +6,10 @@ import { DashboardRoleWidgets } from '@open-mercato/core/modules/dashboards/data
6
6
  import { Role } from '@open-mercato/core/modules/auth/data/entities'
7
7
  import { roleWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'
8
8
  import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
9
- import { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
9
+ import {
10
+ resolveWidgetAssignmentReadScope,
11
+ resolveWidgetAssignmentTargetAccess,
12
+ } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
10
13
  import { hasFeature } from '@open-mercato/shared/security/features'
11
14
  import {
12
15
  runCrudMutationGuardAfterSuccess,
@@ -70,7 +73,15 @@ export async function GET(req: Request) {
70
73
  })
71
74
 
72
75
  const role = await em.findOne(Role, { id: roleId, deletedAt: null })
73
- if (!role || (tenantId && role.tenantId !== tenantId)) {
76
+ const access = resolveWidgetAssignmentTargetAccess({
77
+ isSuperAdmin: !!acl.isSuperAdmin,
78
+ scopeTenantId: tenantId,
79
+ target: role,
80
+ })
81
+ if (access === 'forbidden') {
82
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
83
+ }
84
+ if (access === 'not-found') {
74
85
  return NextResponse.json({ error: 'Role not found' }, { status: 404 })
75
86
  }
76
87
 
@@ -121,7 +132,15 @@ export async function PUT(req: Request) {
121
132
  const organizationId = auth.orgId ?? null
122
133
 
123
134
  const role = await em.findOne(Role, { id: parsed.data.roleId, deletedAt: null })
124
- if (!role || (tenantId && role.tenantId !== tenantId)) {
135
+ const access = resolveWidgetAssignmentTargetAccess({
136
+ isSuperAdmin: !!acl.isSuperAdmin,
137
+ scopeTenantId: tenantId,
138
+ target: role,
139
+ })
140
+ if (access === 'forbidden') {
141
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
142
+ }
143
+ if (access === 'not-found') {
125
144
  return NextResponse.json({ error: 'Role not found' }, { status: 404 })
126
145
  }
127
146
 
@@ -7,7 +7,10 @@ import { User } from '@open-mercato/core/modules/auth/data/entities'
7
7
  import { userWidgetSettingsSchema } from '@open-mercato/core/modules/dashboards/data/validators'
8
8
  import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
9
9
  import { resolveAllowedWidgetIds } from '@open-mercato/core/modules/dashboards/lib/access'
10
- import { resolveWidgetAssignmentReadScope } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
10
+ import {
11
+ resolveWidgetAssignmentReadScope,
12
+ resolveWidgetAssignmentTargetAccess,
13
+ } from '@open-mercato/core/modules/dashboards/lib/widgetAssignmentScope'
11
14
  import { hasFeature } from '@open-mercato/shared/security/features'
12
15
  import {
13
16
  runCrudMutationGuardAfterSuccess,
@@ -53,7 +56,15 @@ export async function GET(req: Request) {
53
56
  })
54
57
 
55
58
  const targetUserForRead = await em.findOne(User, { id: userId, deletedAt: null })
56
- if (!targetUserForRead || (tenantId && (targetUserForRead.tenantId ?? null) !== tenantId)) {
59
+ const readAccess = resolveWidgetAssignmentTargetAccess({
60
+ isSuperAdmin: !!acl.isSuperAdmin,
61
+ scopeTenantId: tenantId,
62
+ target: targetUserForRead,
63
+ })
64
+ if (readAccess === 'forbidden') {
65
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
66
+ }
67
+ if (readAccess === 'not-found') {
57
68
  return NextResponse.json({ error: 'User not found' }, { status: 404 })
58
69
  }
59
70
 
@@ -121,7 +132,15 @@ export async function PUT(req: Request) {
121
132
  const organizationId = auth.orgId ?? null
122
133
 
123
134
  const targetUser = await em.findOne(User, { id: parsed.data.userId, deletedAt: null })
124
- if (!targetUser || (tenantId && (targetUser.tenantId ?? null) !== tenantId)) {
135
+ const writeAccess = resolveWidgetAssignmentTargetAccess({
136
+ isSuperAdmin: !!acl.isSuperAdmin,
137
+ scopeTenantId: tenantId,
138
+ target: targetUser,
139
+ })
140
+ if (writeAccess === 'forbidden') {
141
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
142
+ }
143
+ if (writeAccess === 'not-found') {
125
144
  return NextResponse.json({ error: 'User not found' }, { status: 404 })
126
145
  }
127
146
 
@@ -6,6 +6,7 @@ import { Role } from '@open-mercato/core/modules/auth/data/entities'
6
6
  import { loadAllWidgets } from '@open-mercato/core/modules/dashboards/lib/widgets'
7
7
  import { appendWidgetsToRoles, resolveAnalyticsWidgetIds } from '@open-mercato/core/modules/dashboards/lib/role-widgets'
8
8
  import { seedAnalyticsData } from './seed/analytics'
9
+ import { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'
9
10
 
10
11
  type Args = Record<string, string>
11
12
 
@@ -108,10 +109,7 @@ const seedDefaults: ModuleCli = {
108
109
  return
109
110
  }
110
111
 
111
- const roleNames = roleCsv
112
- .split(',')
113
- .map((name) => name.trim())
114
- .filter(Boolean)
112
+ const roleNames = parseCommaSeparatedList(roleCsv)
115
113
 
116
114
  if (!roleNames.length) {
117
115
  console.log('No roles provided, nothing to seed.')
@@ -125,7 +123,7 @@ const seedDefaults: ModuleCli = {
125
123
  tenantId,
126
124
  organizationId,
127
125
  roleNames,
128
- widgetIds: widgetCsv ? widgetCsv.split(',').map((id) => id.trim()).filter(Boolean) : undefined,
126
+ widgetIds: widgetCsv ? parseCommaSeparatedList(widgetCsv) : undefined,
129
127
  logger: (message) => console.log(message),
130
128
  })
131
129
  },
@@ -143,10 +141,7 @@ const enableAnalyticsWidgets: ModuleCli = {
143
141
  return
144
142
  }
145
143
 
146
- const roleNames = roleCsv
147
- .split(',')
148
- .map((name) => name.trim())
149
- .filter(Boolean)
144
+ const roleNames = parseCommaSeparatedList(roleCsv)
150
145
 
151
146
  if (!roleNames.length) {
152
147
  console.log('No roles provided, nothing to update.')
@@ -8,6 +8,24 @@ export type ResolvedAssignmentScope = {
8
8
  organizationId: string | null
9
9
  }
10
10
 
11
+ export type WidgetAssignmentTarget = {
12
+ tenantId?: string | null
13
+ }
14
+
15
+ export type WidgetAssignmentTargetAccess = 'allowed' | 'forbidden' | 'not-found'
16
+
17
+ export function resolveWidgetAssignmentTargetAccess(params: {
18
+ isSuperAdmin: boolean
19
+ scopeTenantId: string | null
20
+ target: WidgetAssignmentTarget | null | undefined
21
+ }): WidgetAssignmentTargetAccess {
22
+ const { isSuperAdmin, scopeTenantId, target } = params
23
+ if (!isSuperAdmin && !scopeTenantId) return 'forbidden'
24
+ if (!target) return 'not-found'
25
+ if (scopeTenantId && (target.tenantId ?? null) !== scopeTenantId) return 'not-found'
26
+ return 'allowed'
27
+ }
28
+
11
29
  export function resolveWidgetAssignmentReadScope(params: {
12
30
  auth: AuthScopeInput
13
31
  isSuperAdmin: boolean
@@ -4,22 +4,13 @@ import {
4
4
  dictionaryEntryCommandCreateSchema,
5
5
  dictionaryEntryCommandUpdateSchema,
6
6
  } from '@open-mercato/core/modules/dictionaries/data/validators'
7
- import { registerDictionaryEntryCommands } from '@open-mercato/core/modules/dictionaries/commands/factory'
8
- import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
7
+ import {
8
+ ensureDictionaryEntryScope,
9
+ registerDictionaryEntryCommands,
10
+ } from '@open-mercato/core/modules/dictionaries/commands/factory'
9
11
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
10
12
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
11
13
 
12
- function ensureScope(ctx: CommandRuntimeContext, scope: { tenantId: string; organizationId: string }): void {
13
- const tenantId = ctx.auth?.tenantId ?? null
14
- if (tenantId && tenantId !== scope.tenantId) {
15
- throw new CrudHttpError(403, { error: 'Forbidden' })
16
- }
17
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
18
- if (organizationId && organizationId !== scope.organizationId) {
19
- throw new CrudHttpError(403, { error: 'Forbidden' })
20
- }
21
- }
22
-
23
14
  async function ensureDictionary(em: EntityManager, id: string): Promise<Dictionary> {
24
15
  const dictionary = await em.findOne(Dictionary, {
25
16
  id,
@@ -37,12 +28,12 @@ registerDictionaryEntryCommands({
37
28
  translationKeyPrefix: 'dictionaries.entries',
38
29
  createSchema: dictionaryEntryCommandCreateSchema,
39
30
  updateSchema: dictionaryEntryCommandUpdateSchema,
40
- ensureScope,
31
+ ensureScope: ensureDictionaryEntryScope,
41
32
  duplicateError: 'An entry with this value already exists.',
42
33
  resolveDictionaryForCreate: async ({ em, ctx, parsed }) => {
43
34
  const dictionary = await ensureDictionary(em, parsed.dictionaryId)
44
35
  const scope = { tenantId: dictionary.tenantId, organizationId: dictionary.organizationId }
45
- ensureScope(ctx, scope)
36
+ ensureDictionaryEntryScope(ctx, scope)
46
37
  return { dictionary, scope }
47
38
  },
48
39
  resolveEntry: async ({ em, ctx, id }) => {
@@ -54,7 +45,7 @@ registerDictionaryEntryCommands({
54
45
  throw new CrudHttpError(404, { error: 'Dictionary not found' })
55
46
  }
56
47
  const scope = { tenantId: entry.tenantId, organizationId: entry.organizationId }
57
- ensureScope(ctx, scope)
48
+ ensureDictionaryEntryScope(ctx, scope)
58
49
  return { entry, dictionary: entry.dictionary, scope }
59
50
  },
60
51
  ensureDictionaryForUndo: async ({ em, snapshot }) => {
@@ -6,7 +6,8 @@ import {
6
6
  type ReorderDictionaryEntriesCommandInput,
7
7
  type SetDefaultDictionaryEntryCommandInput,
8
8
  } from '@open-mercato/core/modules/dictionaries/data/validators'
9
- import { registerCommand, type CommandHandler, type CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
9
+ import { ensureDictionaryEntryScope } from '@open-mercato/core/modules/dictionaries/commands/factory'
10
+ import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
10
11
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
11
12
  import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
12
13
  import { emitCrudSideEffects, emitCrudUndoSideEffects } from '@open-mercato/shared/lib/commands/helpers'
@@ -36,17 +37,6 @@ const dictionaryCrudEvents: CrudEventsConfig = {
36
37
  }),
37
38
  }
38
39
 
39
- function ensureScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
40
- const tenantId = ctx.auth?.tenantId ?? null
41
- if (tenantId && tenantId !== scope.tenantId) {
42
- throw new CrudHttpError(403, { error: 'Forbidden' })
43
- }
44
- const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
45
- if (organizationId && organizationId !== scope.organizationId) {
46
- throw new CrudHttpError(403, { error: 'Forbidden' })
47
- }
48
- }
49
-
50
40
  async function requireDictionary(
51
41
  em: EntityManager,
52
42
  id: string,
@@ -84,7 +74,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
84
74
  async prepare(rawInput, ctx) {
85
75
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
86
76
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
87
- ensureScope(ctx, scope)
77
+ ensureDictionaryEntryScope(ctx, scope)
88
78
  const em = (ctx.container.resolve('em') as EntityManager).fork()
89
79
  const ids = parsed.entries.map((e) => e.id)
90
80
  const existing = await findWithDecryption(
@@ -108,7 +98,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
108
98
  async execute(rawInput, ctx) {
109
99
  const parsed = reorderDictionaryEntriesCommandSchema.parse(rawInput)
110
100
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
111
- ensureScope(ctx, scope)
101
+ ensureDictionaryEntryScope(ctx, scope)
112
102
 
113
103
  const em = (ctx.container.resolve('em') as EntityManager).fork()
114
104
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
@@ -201,7 +191,7 @@ const reorderDictionaryEntriesCommand: CommandHandler<
201
191
  const undo = extractUndoPayload<ReorderUndoPayload>(logEntry)
202
192
  const before = undo?.before ?? (logEntry?.snapshotBefore as ReorderSnapshot | null | undefined) ?? null
203
193
  if (!before) return
204
- ensureScope(ctx, before.scope)
194
+ ensureDictionaryEntryScope(ctx, before.scope)
205
195
 
206
196
  const em = (ctx.container.resolve('em') as EntityManager).fork()
207
197
  const ids = before.positions.map((p) => p.id)
@@ -265,7 +255,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
265
255
  async prepare(rawInput, ctx) {
266
256
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
267
257
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
268
- ensureScope(ctx, scope)
258
+ ensureDictionaryEntryScope(ctx, scope)
269
259
  const em = (ctx.container.resolve('em') as EntityManager).fork()
270
260
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
271
261
  const previousDefaults = await findWithDecryption(
@@ -291,7 +281,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
291
281
  async execute(rawInput, ctx) {
292
282
  const parsed = setDefaultDictionaryEntryCommandSchema.parse(rawInput)
293
283
  const scope: DictionaryScope = { tenantId: parsed.tenantId, organizationId: parsed.organizationId }
294
- ensureScope(ctx, scope)
284
+ ensureDictionaryEntryScope(ctx, scope)
295
285
 
296
286
  const em = (ctx.container.resolve('em') as EntityManager).fork()
297
287
  const dictionary = await requireDictionary(em, parsed.dictionaryId, scope)
@@ -399,7 +389,7 @@ const setDefaultDictionaryEntryCommand: CommandHandler<
399
389
  const undo = extractUndoPayload<DefaultUndoPayload>(logEntry)
400
390
  const before = undo?.before ?? (logEntry?.snapshotBefore as DefaultSnapshot | null | undefined) ?? null
401
391
  if (!before) return
402
- ensureScope(ctx, before.scope)
392
+ ensureDictionaryEntryScope(ctx, before.scope)
403
393
 
404
394
  const em = (ctx.container.resolve('em') as EntityManager).fork()
405
395
  const dictionary = await requireDictionary(em, before.dictionaryId, before.scope)
@@ -12,6 +12,7 @@ import {
12
12
  } from '@open-mercato/shared/lib/commands'
13
13
  import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
14
14
  import { buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'
15
+ import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
15
16
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
16
17
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
17
18
  import type { z } from 'zod'
@@ -22,6 +23,11 @@ type DictionaryScope = {
22
23
  organizationId: string
23
24
  }
24
25
 
26
+ export function ensureDictionaryEntryScope(ctx: CommandRuntimeContext, scope: DictionaryScope): void {
27
+ ensureTenantScope(ctx, scope.tenantId)
28
+ ensureOrganizationScope(ctx, scope.organizationId)
29
+ }
30
+
25
31
  export type DictionaryEntrySnapshot = {
26
32
  id: string
27
33
  dictionaryId: string
@@ -87,8 +93,8 @@ export type DictionaryEntryCommandConfig<TCreate, TUpdate> = {
87
93
  labels?: DictionaryEntryCommandLabels
88
94
  }
89
95
 
90
- function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
91
- return typeof fn === 'function' ? fn : () => {}
96
+ export function toScopeEnsurer(fn: EnsureScopeFn | undefined): EnsureScopeFn {
97
+ return typeof fn === 'function' ? fn : ensureDictionaryEntryScope
92
98
  }
93
99
 
94
100
  async function loadSnapshot(em: EntityManager, id: string): Promise<DictionaryEntrySnapshot | null> {