@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
@@ -0,0 +1,36 @@
1
+ export type AutoLoginCredentials = {
2
+ email: string
3
+ password: string
4
+ tenantId: string | null
5
+ }
6
+
7
+ function readEnvValue(env: Record<string, string | undefined>, key: string): string {
8
+ const value = env[key]
9
+ return typeof value === 'string' ? value.trim() : ''
10
+ }
11
+
12
+ /**
13
+ * Reads the optional demo-autologin credentials from env. When both
14
+ * `OM_AUTOLOGIN_EMAIL` and `OM_AUTOLOGIN_PASSWORD` are set, the homepage hands
15
+ * unauthenticated visitors to the autologin route which signs them in and drops
16
+ * them into the app. Unset (the default) → `null` → normal login flow.
17
+ *
18
+ * `OM_AUTOLOGIN_TENANT` is optional and only needed when the same email exists
19
+ * across multiple tenants. Pure: pass the env bag so it stays testable.
20
+ */
21
+ export function resolveAutoLoginCredentials(
22
+ env: Record<string, string | undefined> = process.env,
23
+ ): AutoLoginCredentials | null {
24
+ const email = readEnvValue(env, 'OM_AUTOLOGIN_EMAIL')
25
+ const password = env.OM_AUTOLOGIN_PASSWORD ?? ''
26
+ if (!email || !password) return null
27
+ const tenantId = readEnvValue(env, 'OM_AUTOLOGIN_TENANT')
28
+ return { email, password, tenantId: tenantId || null }
29
+ }
30
+
31
+ /** True when demo autologin credentials are configured via env. */
32
+ export function isAutoLoginEnabled(
33
+ env: Record<string, string | undefined> = process.env,
34
+ ): boolean {
35
+ return resolveAutoLoginCredentials(env) !== null
36
+ }
@@ -189,5 +189,9 @@
189
189
  "configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Warmup-Drosselfenster",
190
190
  "configs.systemStatus.variables.scheduleAutoReindex.description": "Automatisch Query-Index-Neuindizierungsaufträge bei Lücken planen.",
191
191
  "configs.systemStatus.variables.scheduleAutoReindex.label": "Automatische Neuindizierung",
192
- "configs.systemStatus.viewDocs": "Dokumentation anzeigen"
192
+ "configs.systemStatus.viewDocs": "Dokumentation anzeigen",
193
+ "configs.upgrades.attachmentsOrgReconcile.cta": "Anhang-Organisationen abgleichen",
194
+ "configs.upgrades.attachmentsOrgReconcile.loading": "Anhang-Organisationen werden abgeglichen…",
195
+ "configs.upgrades.attachmentsOrgReconcile.message": "Version {{version}} behebt Anhänge, die unter der falschen Organisation gespeichert wurden. Führen Sie den Abgleich aus, um vorhandene Anhänge in die Organisation des zugehörigen Datensatzes zu verschieben.",
196
+ "configs.upgrades.attachmentsOrgReconcile.success": "Anhang-Organisationen abgeglichen."
193
197
  }
@@ -189,5 +189,9 @@
189
189
  "configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Warmup throttle window",
190
190
  "configs.systemStatus.variables.scheduleAutoReindex.description": "Automatically schedule query index reindex jobs when gaps appear.",
191
191
  "configs.systemStatus.variables.scheduleAutoReindex.label": "Auto reindex",
192
- "configs.systemStatus.viewDocs": "View documentation"
192
+ "configs.systemStatus.viewDocs": "View documentation",
193
+ "configs.upgrades.attachmentsOrgReconcile.cta": "Reconcile attachment organizations",
194
+ "configs.upgrades.attachmentsOrgReconcile.loading": "Reconciling attachment organizations…",
195
+ "configs.upgrades.attachmentsOrgReconcile.message": "Version {{version}} fixes attachments that were saved under the wrong organization. Run the reconciliation to move existing attachments to the organization of the record they belong to.",
196
+ "configs.upgrades.attachmentsOrgReconcile.success": "Attachment organizations reconciled."
193
197
  }
@@ -189,5 +189,9 @@
189
189
  "configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Ventana de limitación del calentamiento",
190
190
  "configs.systemStatus.variables.scheduleAutoReindex.description": "Programar automáticamente trabajos de reindexación cuando se detecten brechas.",
191
191
  "configs.systemStatus.variables.scheduleAutoReindex.label": "Reindexación automática",
192
- "configs.systemStatus.viewDocs": "Ver documentación"
192
+ "configs.systemStatus.viewDocs": "Ver documentación",
193
+ "configs.upgrades.attachmentsOrgReconcile.cta": "Reconciliar organizaciones de adjuntos",
194
+ "configs.upgrades.attachmentsOrgReconcile.loading": "Reconciliando organizaciones de adjuntos…",
195
+ "configs.upgrades.attachmentsOrgReconcile.message": "La versión {{version}} corrige los adjuntos guardados en la organización incorrecta. Ejecuta la reconciliación para mover los adjuntos existentes a la organización del registro al que pertenecen.",
196
+ "configs.upgrades.attachmentsOrgReconcile.success": "Organizaciones de adjuntos reconciliadas."
193
197
  }
@@ -189,5 +189,9 @@
189
189
  "configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Okno throttlingu rozgrzewki",
190
190
  "configs.systemStatus.variables.scheduleAutoReindex.description": "Automatycznie uruchamia przebudowę indeksu zapytań, gdy wykryte zostaną luki.",
191
191
  "configs.systemStatus.variables.scheduleAutoReindex.label": "Automatyczna przebudowa indeksu",
192
- "configs.systemStatus.viewDocs": "Zobacz dokumentację"
192
+ "configs.systemStatus.viewDocs": "Zobacz dokumentację",
193
+ "configs.upgrades.attachmentsOrgReconcile.cta": "Uzgodnij organizacje załączników",
194
+ "configs.upgrades.attachmentsOrgReconcile.loading": "Uzgadnianie organizacji załączników…",
195
+ "configs.upgrades.attachmentsOrgReconcile.message": "Wersja {{version}} naprawia załączniki zapisane pod niewłaściwą organizacją. Uruchom uzgadnianie, aby przenieść istniejące załączniki do organizacji rekordu, do którego należą.",
196
+ "configs.upgrades.attachmentsOrgReconcile.success": "Organizacje załączników zostały uzgodnione."
193
197
  }
@@ -1,6 +1,11 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import * as semver from 'semver'
3
3
  import type { AppContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { createLogger } from '@open-mercato/shared/lib/logger'
5
+ import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
6
+ import { reconcileAttachmentOrganizations } from '@open-mercato/core/modules/attachments/lib/reconcileOrganization'
7
+
8
+ const logger = createLogger('configs').child({ component: 'upgrade-actions' })
4
9
 
5
10
  export type UpgradeActionContext = {
6
11
  tenantId: string
@@ -38,6 +43,25 @@ export function compareVersions(a: string, b: string): number {
38
43
  }
39
44
 
40
45
  export const upgradeActions: UpgradeActionDefinition[] = [
46
+ {
47
+ id: 'attachments.reconcile-organization',
48
+ version: '0.6.6',
49
+ messageKey: 'configs.upgrades.attachmentsOrgReconcile.message',
50
+ ctaKey: 'configs.upgrades.attachmentsOrgReconcile.cta',
51
+ successKey: 'configs.upgrades.attachmentsOrgReconcile.success',
52
+ loadingKey: 'configs.upgrades.attachmentsOrgReconcile.loading',
53
+ async run({ container, em, tenantId }) {
54
+ const queryEngine = container.resolve('queryEngine') as QueryEngine
55
+ const report = await reconcileAttachmentOrganizations({ em, queryEngine, tenantId })
56
+ logger.info('attachments organization reconcile completed', {
57
+ tenantId,
58
+ scanned: report.scanned,
59
+ updated: report.updated,
60
+ unresolved: report.unresolved,
61
+ skippedVirtual: report.skippedVirtual,
62
+ })
63
+ },
64
+ },
41
65
  {
42
66
  id: 'customers.seed-interaction-statuses',
43
67
  version: '0.6.5',
@@ -306,15 +306,17 @@ Both link to `/backend/customer_accounts/{sourceEntityId}` for staff review.
306
306
 
307
307
  | Spot ID | Widget | Purpose |
308
308
  |---------|--------|---------|
309
- | `customers.person` | `account-status` | Shows portal account status on Customers v2 person detail forms |
310
- | `crud-form:customers.person` | `account-status` | Alias for CrudForm hosts deriving the spot from `customers.person` |
311
- | `crud-form:customers:customer_person_profile:fields` | `account-status` | Shows portal account status on CRM person detail page |
312
- | `customers.company` | `company-users` | Shows portal users on Customers v2 company detail forms |
313
- | `crud-form:customers.company` | `company-users` | Alias for CrudForm hosts deriving the spot from `customers.company` |
314
- | `crud-form:customers:customer_company_profile:fields` | `company-users` | Shows portal users linked to a CRM company |
309
+ | `customers.person` | `account-status` | Alias for hosts requesting the bare `customers.person` entity spot |
310
+ | `crud-form:customers.person` | `account-status` | Shows portal account status on the person v2 detail page (`people-v2/[id]`) |
311
+ | `customers.company` | `company-users` | Alias for hosts requesting the bare `customers.company` entity spot |
312
+ | `crud-form:customers.company` | `company-users` | Shows portal users on the company v2 detail page (`companies-v2/[id]`) |
315
313
 
316
314
  Both inject as column 2 groups with priority 200, gated by `customer_accounts.view` feature.
317
315
 
316
+ CrudForm derives its spot id as `crud-form:${entityId}` with every `:` normalized to `.`, so a
317
+ registered `crud-form:` key whose entity segment still contains `:` can never be requested by any
318
+ host. Register against the normalized (dot) form only — `injection-table.test.ts` enforces this.
319
+
318
320
  ## Backend Pages
319
321
 
320
322
  | Path | Purpose |
@@ -49,6 +49,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
49
49
  const role = await em.findOne(CustomerRole, {
50
50
  id: params.id,
51
51
  tenantId,
52
+ organizationId: auth.orgId,
52
53
  deletedAt: null,
53
54
  })
54
55
  if (!role) {
@@ -36,6 +36,7 @@ export async function GET(req: Request, { params }: { params: { id: string } })
36
36
  const role = await em.findOne(CustomerRole, {
37
37
  id: params.id,
38
38
  tenantId: auth.tenantId,
39
+ organizationId: auth.orgId,
39
40
  deletedAt: null,
40
41
  })
41
42
  if (!role) {
@@ -94,6 +95,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
94
95
  const role = await em.findOne(CustomerRole, {
95
96
  id: params.id,
96
97
  tenantId: auth.tenantId,
98
+ organizationId: auth.orgId,
97
99
  deletedAt: null,
98
100
  })
99
101
  if (!role) {
@@ -127,7 +129,11 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
127
129
  if (parsed.data.isDefault !== undefined) updates.isDefault = parsed.data.isDefault
128
130
  if (parsed.data.customerAssignable !== undefined) updates.customerAssignable = parsed.data.customerAssignable
129
131
 
130
- await em.nativeUpdate(CustomerRole, { id: role.id }, updates)
132
+ await em.nativeUpdate(CustomerRole, {
133
+ id: role.id,
134
+ tenantId: auth.tenantId,
135
+ organizationId: auth.orgId,
136
+ }, updates)
131
137
 
132
138
  void emitCustomerAccountsEvent('customer_accounts.role.updated', {
133
139
  id: role.id,
@@ -157,6 +163,7 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
157
163
  const role = await em.findOne(CustomerRole, {
158
164
  id: params.id,
159
165
  tenantId: auth.tenantId,
166
+ organizationId: auth.orgId,
160
167
  deletedAt: null,
161
168
  })
162
169
  if (!role) {
@@ -191,8 +198,15 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
191
198
  return NextResponse.json({ ok: false, error: `Cannot delete role with ${assignedUsersCount} assigned user(s). Reassign users first.` }, { status: 400 })
192
199
  }
193
200
 
194
- await em.nativeUpdate(CustomerRole, { id: role.id }, { deletedAt: new Date() })
195
- await em.nativeUpdate(CustomerRoleAcl, { role: role.id as any }, { deletedAt: new Date() })
201
+ await em.nativeUpdate(CustomerRole, {
202
+ id: role.id,
203
+ tenantId: auth.tenantId,
204
+ organizationId: auth.orgId,
205
+ }, { deletedAt: new Date() })
206
+ await em.nativeUpdate(CustomerRoleAcl, {
207
+ role: role.id as any,
208
+ tenantId: auth.tenantId,
209
+ }, { deletedAt: new Date() })
196
210
 
197
211
  void emitCustomerAccountsEvent('customer_accounts.role.deleted', {
198
212
  id: role.id,
@@ -60,7 +60,12 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
60
60
  ? await findWithDecryption(
61
61
  em,
62
62
  CustomerRole,
63
- { id: { $in: requestedRoleIds }, tenantId: auth.tenantId, deletedAt: null } as any,
63
+ {
64
+ id: { $in: requestedRoleIds },
65
+ tenantId: auth.tenantId,
66
+ organizationId: auth.orgId,
67
+ deletedAt: null,
68
+ } as any,
64
69
  undefined,
65
70
  { tenantId: auth.tenantId, organizationId: auth.orgId },
66
71
  )
@@ -67,7 +67,12 @@ export async function POST(req: Request) {
67
67
  ? await findWithDecryption(
68
68
  em,
69
69
  CustomerRole,
70
- { id: { $in: requestedRoleIds }, tenantId: auth.tenantId, deletedAt: null } as any,
70
+ {
71
+ id: { $in: requestedRoleIds },
72
+ tenantId: auth.tenantId,
73
+ organizationId: auth.orgId,
74
+ deletedAt: null,
75
+ } as any,
71
76
  undefined,
72
77
  { tenantId: auth.tenantId, organizationId: auth.orgId },
73
78
  )
@@ -133,6 +133,7 @@ export class CustomerInvitationService {
133
133
  {
134
134
  id: { $in: roleIds } as any,
135
135
  tenantId: invitation.tenantId,
136
+ organizationId: invitation.organizationId,
136
137
  deletedAt: null,
137
138
  } as any,
138
139
  undefined,
@@ -29,7 +29,9 @@ export class CustomerRbacService {
29
29
  }
30
30
 
31
31
  private getCacheKey(userId: string, scope: { tenantId: string; organizationId: string }): string {
32
- return `customer_rbac:${userId}:${scope.tenantId}:${scope.organizationId}`
32
+ // Keep the authorization query versioned in the key so cache entries created
33
+ // before scope hardening cannot preserve grants that are no longer valid.
34
+ return `customer_rbac:v2:${userId}:${scope.tenantId}:${scope.organizationId}`
33
35
  }
34
36
 
35
37
  private getUserTag(userId: string): string {
@@ -89,8 +91,13 @@ export class CustomerRbacService {
89
91
  // Aggregate role ACLs
90
92
  const links = await em.find(CustomerUserRole, {
91
93
  user: userId as any,
94
+ role: {
95
+ tenantId: scope.tenantId,
96
+ organizationId: scope.organizationId,
97
+ deletedAt: null,
98
+ },
92
99
  deletedAt: null,
93
- }, { populate: ['role'] })
100
+ } as any, { populate: ['role'] })
94
101
  const roleIds = links.map((l) => (l.role as any)?.id).filter(Boolean)
95
102
 
96
103
  let isPortalAdmin = false
@@ -19,10 +19,8 @@ const companyUsersWidget = {
19
19
  export const injectionTable: ModuleInjectionTable = {
20
20
  'customers.person': [accountStatusWidget],
21
21
  'crud-form:customers.person': [accountStatusWidget],
22
- 'crud-form:customers:customer_person_profile:fields': [accountStatusWidget],
23
22
  'customers.company': [companyUsersWidget],
24
23
  'crud-form:customers.company': [companyUsersWidget],
25
- 'crud-form:customers:customer_company_profile:fields': [companyUsersWidget],
26
24
  // Step 4.10 — Portal AiChat injection example.
27
25
  // Mapped to the portal profile page's `pageAfter('profile')` spot;
28
26
  // third-party modules targeting other portal pages can copy this entry.
@@ -31,6 +31,7 @@ import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
31
31
  import { coerceDisplayName } from '../../../../lib/displayName'
32
32
  import { CompanyDetailHeader } from '../../../../components/detail/CompanyDetailHeader'
33
33
  import { CompanyDetailTabs, resolveLegacyTab, type CompanyTabId } from '../../../../components/detail/CompanyDetailTabs'
34
+ import { useDealsAccess } from '../../../../components/detail/useDealsAccess'
34
35
  import { CompanyKpiBar } from '../../../../components/detail/CompanyKpiBar'
35
36
  import { ScheduleActivityDialog, type ScheduleActivityEditData } from '../../../../components/detail/ScheduleActivityDialog'
36
37
  import { ChangelogTab } from '../../../../components/detail/ChangelogTab'
@@ -69,6 +70,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
69
70
  }, [searchParams])
70
71
  const [activeTab, setActiveTab] = React.useState<CompanyTabId>(initialTab)
71
72
  const [sectionAction, setSectionAction] = React.useState<SectionAction | null>(null)
73
+ const { canViewDeals, isReady: isDealsAccessReady } = useDealsAccess()
72
74
 
73
75
  // Form state
74
76
  const [isDirty, setIsDirty] = React.useState(false)
@@ -328,6 +330,14 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
328
330
  setSectionAction(null)
329
331
  }, [activeTab])
330
332
 
333
+ // A `?tab=deals` deep link must not strand users without `customers.deals.view`
334
+ // on a tab that no longer exists for them. Wait for the granted features to load
335
+ // so a permitted user is never bounced off the tab mid-fetch.
336
+ React.useEffect(() => {
337
+ if (!isDealsAccessReady || canViewDeals) return
338
+ setActiveTab((current) => (current === 'deals' ? 'people' : current))
339
+ }, [isDealsAccessReady, canViewDeals])
340
+
331
341
  // Deals scope
332
342
  const dealsScope = React.useMemo(
333
343
  () => (currentCompanyId ? ({ kind: 'company', entityId: currentCompanyId } as const) : null),
@@ -544,7 +554,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
544
554
  />
545
555
  )}
546
556
 
547
- {activeTab === 'deals' && (
557
+ {activeTab === 'deals' && canViewDeals && (
548
558
  <DealsSection
549
559
  scope={dealsScope}
550
560
  emptyLabel={t('customers.companies.detail.empty.deals', 'No deals linked to this company.')}
@@ -24,9 +24,13 @@ export function ViewTabsRow({ active, className }: ViewTabsRowProps): React.Reac
24
24
  map: translateWithFallback(t, 'customers.deals.kanban.view.map', 'Map'),
25
25
  }
26
26
 
27
+ // Link-based tab row (two routes), so the Tabs primitive (state-driven,
28
+ // onValueChange) does not fit — real <Link> semantics must stay. Classes
29
+ // mirror the Tabs underline variant: accent-indigo active border,
30
+ // shadow-focus halo.
27
31
  const baseTab =
28
- 'inline-flex items-center px-3.5 py-2.5 text-sm leading-normal transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
29
- const activeTab = 'border-b-2 border-foreground font-semibold text-foreground'
32
+ 'inline-flex items-center px-3.5 py-2.5 text-sm leading-normal transition-colors focus-visible:outline-none focus-visible:shadow-focus'
33
+ const activeTab = 'border-b-2 border-accent-indigo font-semibold text-foreground'
30
34
  const inactiveTab = 'border-b-2 border-transparent font-normal text-muted-foreground hover:text-foreground'
31
35
 
32
36
  // Renders the active tab as a non-navigating `<span>` and the inactive tab as a `<Link>`,
@@ -1,10 +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'
7
- import { Badge } from '@open-mercato/ui/primitives/badge'
6
+ import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
8
7
  import {
9
8
  Users,
10
9
  Handshake,
@@ -14,6 +13,7 @@ import {
14
13
  Plus,
15
14
  } from 'lucide-react'
16
15
  import type { SectionAction } from '@open-mercato/ui/backend/detail'
16
+ import { useDealsAccess } from './useDealsAccess'
17
17
 
18
18
  export type CompanyTabId =
19
19
  | 'people'
@@ -27,7 +27,7 @@ type TabDef = {
27
27
  id: CompanyTabId
28
28
  label: string
29
29
  icon?: React.ReactNode
30
- badge?: React.ReactNode
30
+ count?: React.ReactNode
31
31
  }
32
32
 
33
33
  type CompanyDetailTabsProps = {
@@ -58,21 +58,9 @@ export function resolveLegacyTab(tab: string | null | undefined): CompanyTabId {
58
58
  return tab as CompanyTabId
59
59
  }
60
60
 
61
- function NewBadge() {
62
- return (
63
- <span className="ml-1.5 rounded bg-foreground px-1.5 py-0.5 text-overline font-semibold leading-none text-background">
64
- NEW
65
- </span>
66
- )
67
- }
68
-
69
- function CountBadge({ count }: { count: number }) {
70
- if (count <= 0) return null
71
- return (
72
- <span className="ml-1.5 rounded-full bg-muted px-1.5 py-0.5 text-xs font-medium leading-none text-muted-foreground">
73
- {count > 999 ? '999+' : count}
74
- </span>
75
- )
61
+ function formatTabCount(count: number): string | number | undefined {
62
+ if (count <= 0) return undefined
63
+ return count > 999 ? '999+' : count
76
64
  }
77
65
 
78
66
  export function CompanyDetailTabs({
@@ -87,6 +75,7 @@ export function CompanyDetailTabs({
87
75
  children,
88
76
  }: CompanyDetailTabsProps) {
89
77
  const t = useT()
78
+ const { canViewDeals } = useDealsAccess()
90
79
 
91
80
  const builtInTabs: TabDef[] = React.useMemo(
92
81
  () => [
@@ -94,34 +83,38 @@ export function CompanyDetailTabs({
94
83
  id: 'people',
95
84
  label: t('customers.companies.detail.tabs.people', 'People'),
96
85
  icon: <Users className="size-4" />,
97
- badge: <CountBadge count={peopleCount} />,
98
- },
99
- {
100
- id: 'deals',
101
- label: t('customers.companies.detail.tabs.deals', 'Deals'),
102
- icon: <Handshake className="size-4" />,
103
- badge: <CountBadge count={dealsCount} />,
86
+ count: formatTabCount(peopleCount),
104
87
  },
88
+ ...(canViewDeals
89
+ ? [
90
+ {
91
+ id: 'deals' as CompanyTabId,
92
+ label: t('customers.companies.detail.tabs.deals', 'Deals'),
93
+ icon: <Handshake className="size-4" />,
94
+ count: formatTabCount(dealsCount),
95
+ },
96
+ ]
97
+ : []),
105
98
  {
106
99
  id: 'activity-log',
107
100
  label: t('customers.companies.detail.tabs.activityLog', 'Activity log'),
108
101
  icon: <Clock className="size-4" />,
109
- badge: <CountBadge count={activitiesCount} />,
102
+ count: formatTabCount(activitiesCount),
110
103
  },
111
104
  {
112
105
  id: 'changelog',
113
106
  label: t('customers.companies.detail.tabs.changelog', 'Changelog'),
114
107
  icon: <History className="size-4" />,
115
- badge: <NewBadge />,
108
+ count: 'NEW',
116
109
  },
117
110
  {
118
111
  id: 'files',
119
112
  label: t('customers.companies.detail.tabs.files', 'Files'),
120
113
  icon: <Paperclip className="size-4" />,
121
- badge: <CountBadge count={filesCount} />,
114
+ count: formatTabCount(filesCount),
122
115
  },
123
116
  ],
124
- [t, peopleCount, dealsCount, activitiesCount, filesCount],
117
+ [t, canViewDeals, peopleCount, dealsCount, activitiesCount, filesCount],
125
118
  )
126
119
 
127
120
  const allTabs: TabDef[] = React.useMemo(
@@ -138,33 +131,24 @@ export function CompanyDetailTabs({
138
131
  return (
139
132
  <div>
140
133
  {/* Tab navigation */}
141
- <div className="flex items-end justify-between gap-2 border-b" role="tablist" aria-label={t('customers.companies.detail.tabs.label', 'Company detail sections')}>
142
- <nav className="-mb-px flex flex-1 gap-1 overflow-x-auto px-1">
143
- {allTabs.map((tab) => {
144
- const isActive = activeTab === tab.id
145
- return (
146
- <Button
147
- key={tab.id}
148
- type="button"
149
- variant="ghost"
150
- size="sm"
151
- role="tab"
152
- aria-selected={isActive}
153
- onClick={() => onTabChange(tab.id)}
154
- className={cn(
155
- 'h-auto shrink-0 rounded-none border-b-2 px-3 py-2.5 hover:bg-transparent',
156
- isActive
157
- ? 'border-foreground text-foreground font-semibold'
158
- : 'border-transparent text-muted-foreground hover:text-foreground',
159
- )}
160
- >
161
- {tab.icon && <span className="mr-1.5">{tab.icon}</span>}
134
+ <div className="flex items-end justify-between gap-2 border-b">
135
+ <Tabs
136
+ value={activeTab}
137
+ onValueChange={(value) => onTabChange(value as CompanyTabId)}
138
+ variant="underline"
139
+ className="min-w-0 flex-1"
140
+ >
141
+ <TabsList
142
+ aria-label={t('customers.companies.detail.tabs.label', 'Company detail sections')}
143
+ className="-mb-px w-full overflow-x-auto border-b-0 px-1"
144
+ >
145
+ {allTabs.map((tab) => (
146
+ <TabsTrigger key={tab.id} value={tab.id} leading={tab.icon} count={tab.count}>
162
147
  {tab.label}
163
- {tab.badge}
164
- </Button>
165
- )
166
- })}
167
- </nav>
148
+ </TabsTrigger>
149
+ ))}
150
+ </TabsList>
151
+ </Tabs>
168
152
  {sectionAction ? (
169
153
  <Button
170
154
  type="button"
@@ -2,9 +2,8 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { Activity, Building2, History, NotebookPen, Paperclip, Users } from 'lucide-react'
5
- import { cn } from '@open-mercato/shared/lib/utils'
6
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
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 DealTabId =
10
9
  | 'activities'
@@ -19,7 +18,7 @@ type TabDef = {
19
18
  id: DealTabId
20
19
  label: string
21
20
  icon?: React.ReactNode
22
- badge?: React.ReactNode
21
+ count?: React.ReactNode
23
22
  }
24
23
 
25
24
  type DealDetailTabsProps = {
@@ -38,21 +37,9 @@ export function resolveLegacyTab(tab: string | null | undefined): DealTabId {
38
37
  return SUPPORTED_TAB_IDS.has(tab as DealTabId) ? (tab as DealTabId) : 'activities'
39
38
  }
40
39
 
41
- function CountBadge({ count }: { count: number }) {
42
- if (count <= 0) return null
43
- return (
44
- <span className="ml-1.5 rounded-sm bg-muted px-1.5 py-px text-xs font-semibold leading-none text-muted-foreground">
45
- {count > 999 ? '999+' : count}
46
- </span>
47
- )
48
- }
49
-
50
- function NewBadge() {
51
- return (
52
- <span className="ml-1.5 rounded-sm bg-muted px-1.5 py-px text-xs font-semibold leading-none text-muted-foreground">
53
- NEW
54
- </span>
55
- )
40
+ function formatTabCount(count: number): string | number | undefined {
41
+ if (count <= 0) return undefined
42
+ return count > 999 ? '999+' : count
56
43
  }
57
44
 
58
45
  export function DealDetailTabs({
@@ -76,13 +63,13 @@ export function DealDetailTabs({
76
63
  id: 'people',
77
64
  label: t('customers.deals.detail.tabs.people', 'People'),
78
65
  icon: <Users className="size-4" />,
79
- badge: <CountBadge count={peopleCount} />,
66
+ count: formatTabCount(peopleCount),
80
67
  },
81
68
  {
82
69
  id: 'companies',
83
70
  label: t('customers.deals.detail.tabs.companies', 'Companies'),
84
71
  icon: <Building2 className="size-4" />,
85
- badge: <CountBadge count={companiesCount} />,
72
+ count: formatTabCount(companiesCount),
86
73
  },
87
74
  {
88
75
  id: 'notes',
@@ -98,7 +85,7 @@ export function DealDetailTabs({
98
85
  id: 'changelog',
99
86
  label: t('customers.deals.detail.tabs.changelog', 'Changelog'),
100
87
  icon: <History className="size-4" />,
101
- badge: <NewBadge />,
88
+ count: 'NEW',
102
89
  },
103
90
  ],
104
91
  [companiesCount, peopleCount, t],
@@ -117,34 +104,22 @@ export function DealDetailTabs({
117
104
 
118
105
  return (
119
106
  <div>
120
- <div className="border-b border-border" role="tablist" aria-label={t('customers.deals.detail.tabs.label', 'Deal detail sections')}>
121
- <nav className="-mb-px flex gap-7 overflow-x-auto" role="presentation">
122
- {allTabs.map((tab) => {
123
- const isActive = activeTab === tab.id
124
- return (
125
- <Button
126
- key={tab.id}
127
- type="button"
128
- variant="ghost"
129
- size="sm"
130
- role="tab"
131
- aria-selected={isActive}
132
- onClick={() => onTabChange(tab.id)}
133
- className={cn(
134
- 'h-auto shrink-0 rounded-none border-b-2 px-3 py-2.5 hover:bg-transparent',
135
- isActive
136
- ? 'border-foreground text-foreground font-semibold'
137
- : 'border-transparent text-muted-foreground hover:text-foreground',
138
- )}
139
- >
140
- {tab.icon ? <span className="mr-1.5">{tab.icon}</span> : null}
141
- {tab.label}
142
- {tab.badge}
143
- </Button>
144
- )
145
- })}
146
- </nav>
147
- </div>
107
+ <Tabs
108
+ value={activeTab}
109
+ onValueChange={(value) => onTabChange(value as DealTabId)}
110
+ variant="underline"
111
+ >
112
+ <TabsList
113
+ aria-label={t('customers.deals.detail.tabs.label', 'Deal detail sections')}
114
+ className="w-full overflow-x-auto"
115
+ >
116
+ {allTabs.map((tab) => (
117
+ <TabsTrigger key={tab.id} value={tab.id} leading={tab.icon} count={tab.count}>
118
+ {tab.label}
119
+ </TabsTrigger>
120
+ ))}
121
+ </TabsList>
122
+ </Tabs>
148
123
 
149
124
  <div className="pt-5" role="tabpanel">
150
125
  {children}