@open-mercato/core 0.6.6-develop.6542.1.fee3e68aa6 → 0.6.6-develop.6551.1.1511bfa890

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 (162) 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 +7 -4
  9. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  10. package/dist/modules/attachments/components/AttachmentContentPreview.js +6 -35
  11. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  12. package/dist/modules/audit_logs/backend/audit-logs/page.js +14 -24
  13. package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
  14. package/dist/modules/auth/api/autologin.js +85 -0
  15. package/dist/modules/auth/api/autologin.js.map +7 -0
  16. package/dist/modules/auth/lib/autologin.js +19 -0
  17. package/dist/modules/auth/lib/autologin.js.map +7 -0
  18. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +1 -0
  19. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
  20. package/dist/modules/customer_accounts/api/admin/roles/[id].js +17 -3
  21. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  22. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js +6 -1
  23. package/dist/modules/customer_accounts/api/portal/users/[id]/roles.js.map +2 -2
  24. package/dist/modules/customer_accounts/api/portal/users-invite.js +6 -1
  25. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  26. package/dist/modules/customer_accounts/services/customerInvitationService.js +1 -0
  27. package/dist/modules/customer_accounts/services/customerInvitationService.js.map +2 -2
  28. package/dist/modules/customer_accounts/services/customerRbacService.js +6 -1
  29. package/dist/modules/customer_accounts/services/customerRbacService.js.map +2 -2
  30. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +2 -2
  31. package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
  32. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +27 -37
  33. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  34. package/dist/modules/customers/components/detail/DealDetailTabs.js +23 -35
  35. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  36. package/dist/modules/customers/components/detail/DetailTabsLayout.js +6 -21
  37. package/dist/modules/customers/components/detail/DetailTabsLayout.js.map +2 -2
  38. package/dist/modules/customers/components/detail/MobilePersonDetail.js +26 -32
  39. package/dist/modules/customers/components/detail/MobilePersonDetail.js.map +2 -2
  40. package/dist/modules/customers/components/detail/PersonDetailTabs.js +29 -39
  41. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  42. package/dist/modules/dictionaries/commands/entries.js +7 -14
  43. package/dist/modules/dictionaries/commands/entries.js.map +2 -2
  44. package/dist/modules/dictionaries/commands/entry-operations.js +7 -16
  45. package/dist/modules/dictionaries/commands/entry-operations.js.map +2 -2
  46. package/dist/modules/dictionaries/commands/factory.js +9 -3
  47. package/dist/modules/dictionaries/commands/factory.js.map +2 -2
  48. package/dist/modules/feature_toggles/api/check/boolean/route.js +0 -14
  49. package/dist/modules/feature_toggles/api/check/boolean/route.js.map +2 -2
  50. package/dist/modules/feature_toggles/defaults.json +8 -0
  51. package/dist/modules/integrations/backend/integrations/[id]/page.js +9 -80
  52. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  53. package/dist/modules/payment_gateways/data/entities.js +45 -0
  54. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  55. package/dist/modules/payment_gateways/lib/gateway-service.js +173 -38
  56. package/dist/modules/payment_gateways/lib/gateway-service.js.map +3 -3
  57. package/dist/modules/payment_gateways/lib/session-idempotency.js +104 -0
  58. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +7 -0
  59. package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js +11 -0
  60. package/dist/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.js.map +7 -0
  61. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js +16 -14
  62. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js.map +2 -2
  63. package/dist/modules/planner/components/AvailabilityRulesEditor.js +18 -30
  64. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  65. package/dist/modules/resources/backend/resources/resources/[id]/page.js +24 -24
  66. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  67. package/dist/modules/sales/backend/sales/channels/create/page.js +6 -0
  68. package/dist/modules/sales/backend/sales/channels/create/page.js.map +2 -2
  69. package/dist/modules/sales/backend/sales/channels/create/page.meta.js +2 -0
  70. package/dist/modules/sales/backend/sales/channels/create/page.meta.js.map +2 -2
  71. package/dist/modules/sales/backend/sales/channels/offers/page.js +6 -0
  72. package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
  73. package/dist/modules/sales/backend/sales/channels/offers/page.meta.js +2 -0
  74. package/dist/modules/sales/backend/sales/channels/offers/page.meta.js.map +2 -2
  75. package/dist/modules/sales/backend/sales/channels/page.js +6 -0
  76. package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
  77. package/dist/modules/sales/backend/sales/channels/page.meta.js +2 -0
  78. package/dist/modules/sales/backend/sales/channels/page.meta.js.map +2 -2
  79. package/dist/modules/sales/backend/sales/documents/[id]/page.js +6 -4
  80. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  81. package/dist/modules/sales/components/SalesChannelsDisabledNotice.js +15 -0
  82. package/dist/modules/sales/components/SalesChannelsDisabledNotice.js.map +7 -0
  83. package/dist/modules/sales/components/documents/SalesDocumentForm.js +7 -3
  84. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  85. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +10 -8
  86. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  87. package/dist/modules/sales/components/useSalesChannelsEnabled.js +44 -0
  88. package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +7 -0
  89. package/dist/modules/sales/lib/salesChannelsToggle.js +18 -0
  90. package/dist/modules/sales/lib/salesChannelsToggle.js.map +7 -0
  91. package/dist/modules/sales/lib/salesChannelsToggleId.js +5 -0
  92. package/dist/modules/sales/lib/salesChannelsToggleId.js.map +7 -0
  93. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +22 -25
  94. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  95. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -14
  96. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  97. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js +10 -29
  98. package/dist/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.js.map +2 -2
  99. package/dist/modules/sync_excel/lib/adapters/customers.js +19 -20
  100. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  101. package/dist/modules/sync_excel/lib/parser.js +185 -0
  102. package/dist/modules/sync_excel/lib/parser.js.map +2 -2
  103. package/dist/modules/sync_excel/lib/upload-storage.js +20 -1
  104. package/dist/modules/sync_excel/lib/upload-storage.js.map +2 -2
  105. package/generated/entities/gateway_session_initialization/index.ts +10 -0
  106. package/generated/entities.ids.generated.ts +1 -0
  107. package/generated/entity-fields-registry.ts +12 -0
  108. package/package.json +7 -7
  109. package/src/modules/api_keys/api/keys/route.ts +9 -4
  110. package/src/modules/attachments/components/AttachmentContentPreview.tsx +9 -44
  111. package/src/modules/audit_logs/backend/audit-logs/page.tsx +12 -22
  112. package/src/modules/auth/api/autologin.ts +100 -0
  113. package/src/modules/auth/lib/autologin.ts +36 -0
  114. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +1 -0
  115. package/src/modules/customer_accounts/api/admin/roles/[id].ts +17 -3
  116. package/src/modules/customer_accounts/api/portal/users/[id]/roles.ts +6 -1
  117. package/src/modules/customer_accounts/api/portal/users-invite.ts +6 -1
  118. package/src/modules/customer_accounts/services/customerInvitationService.ts +1 -0
  119. package/src/modules/customer_accounts/services/customerRbacService.ts +9 -2
  120. package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +6 -2
  121. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +27 -49
  122. package/src/modules/customers/components/detail/DealDetailTabs.tsx +24 -49
  123. package/src/modules/customers/components/detail/DetailTabsLayout.tsx +13 -24
  124. package/src/modules/customers/components/detail/MobilePersonDetail.tsx +25 -31
  125. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +29 -50
  126. package/src/modules/dictionaries/commands/entries.ts +7 -16
  127. package/src/modules/dictionaries/commands/entry-operations.ts +8 -18
  128. package/src/modules/dictionaries/commands/factory.ts +8 -2
  129. package/src/modules/feature_toggles/api/check/boolean/route.ts +0 -15
  130. package/src/modules/feature_toggles/defaults.json +8 -0
  131. package/src/modules/integrations/backend/integrations/[id]/page.tsx +16 -59
  132. package/src/modules/payment_gateways/data/entities.ts +39 -0
  133. package/src/modules/payment_gateways/lib/gateway-service.ts +211 -40
  134. package/src/modules/payment_gateways/lib/session-idempotency.ts +142 -0
  135. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +236 -0
  136. package/src/modules/payment_gateways/migrations/Migration20260709220938_payment_gateways.ts +10 -0
  137. package/src/modules/planner/backend/planner/availability-rulesets/[id]/page.tsx +14 -20
  138. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +16 -34
  139. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +27 -37
  140. package/src/modules/sales/backend/sales/channels/create/page.meta.ts +3 -0
  141. package/src/modules/sales/backend/sales/channels/create/page.tsx +7 -0
  142. package/src/modules/sales/backend/sales/channels/offers/page.meta.ts +3 -0
  143. package/src/modules/sales/backend/sales/channels/offers/page.tsx +7 -0
  144. package/src/modules/sales/backend/sales/channels/page.meta.ts +4 -0
  145. package/src/modules/sales/backend/sales/channels/page.tsx +7 -0
  146. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +7 -5
  147. package/src/modules/sales/components/SalesChannelsDisabledNotice.tsx +21 -0
  148. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +7 -3
  149. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +10 -8
  150. package/src/modules/sales/components/useSalesChannelsEnabled.ts +59 -0
  151. package/src/modules/sales/i18n/de.json +1 -0
  152. package/src/modules/sales/i18n/en.json +1 -0
  153. package/src/modules/sales/i18n/es.json +1 -0
  154. package/src/modules/sales/i18n/pl.json +1 -0
  155. package/src/modules/sales/lib/salesChannelsToggle.ts +26 -0
  156. package/src/modules/sales/lib/salesChannelsToggleId.ts +1 -0
  157. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +25 -34
  158. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +11 -17
  159. package/src/modules/staff/lib/timesheets-projects-ui/SavedViewTabs.tsx +11 -28
  160. package/src/modules/sync_excel/lib/adapters/customers.ts +20 -22
  161. package/src/modules/sync_excel/lib/parser.ts +245 -0
  162. package/src/modules/sync_excel/lib/upload-storage.ts +23 -1
@@ -9,13 +9,20 @@ import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customF
9
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
10
  import { useT } from '@open-mercato/shared/lib/i18n/context'
11
11
  import { useChannelFields, buildChannelPayload, type ChannelFormValues } from '@open-mercato/core/modules/sales/components/channels/channelFormFields'
12
+ import { useSalesChannelsEnabled } from '@open-mercato/core/modules/sales/components/useSalesChannelsEnabled'
13
+ import { SalesChannelsDisabledNotice } from '@open-mercato/core/modules/sales/components/SalesChannelsDisabledNotice'
12
14
  import { E } from '#generated/entities.ids.generated'
13
15
 
14
16
  export default function CreateChannelPage() {
15
17
  const t = useT()
16
18
  const { fields, groups } = useChannelFields()
19
+ const { enabled: channelsEnabled, isLoading: channelsEnabledLoading } = useSalesChannelsEnabled()
17
20
  const router = useRouter()
18
21
 
22
+ if (!channelsEnabled && !channelsEnabledLoading) {
23
+ return <SalesChannelsDisabledNotice />
24
+ }
25
+
19
26
  return (
20
27
  <Page>
21
28
  <PageBody>
@@ -1,4 +1,6 @@
1
1
  import React from 'react'
2
+ import type { RouteVisibilityContext } from '@open-mercato/shared/modules/registry'
3
+ import { isSalesChannelsEnabledForTenant } from '../../../../lib/salesChannelsToggle'
2
4
 
3
5
  const globeIcon = React.createElement(
4
6
  'svg',
@@ -12,6 +14,7 @@ const globeIcon = React.createElement(
12
14
  export const metadata = {
13
15
  requireAuth: true,
14
16
  requireFeatures: ['sales.channels.manage'],
17
+ visible: (ctx: RouteVisibilityContext) => isSalesChannelsEnabledForTenant(ctx.auth?.tenantId ?? null),
15
18
  pageTitle: 'Sales channel offers',
16
19
  pageTitleKey: 'sales.channels.offers.listTitle',
17
20
  pageGroup: 'Sales',
@@ -18,6 +18,8 @@ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuarde
18
18
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
19
19
  import { useT } from '@open-mercato/shared/lib/i18n/context'
20
20
  import { mapOfferRow, renderOfferPriceSummary, type OfferRow } from '@open-mercato/core/modules/sales/components/channels/offerTableUtils'
21
+ import { useSalesChannelsEnabled } from '@open-mercato/core/modules/sales/components/useSalesChannelsEnabled'
22
+ import { SalesChannelsDisabledNotice } from '@open-mercato/core/modules/sales/components/SalesChannelsDisabledNotice'
21
23
  import { createLogger } from '@open-mercato/shared/lib/logger'
22
24
 
23
25
  const logger = createLogger('sales')
@@ -34,6 +36,7 @@ const SAVE_CONTEXT_ID = 'sales-channel-offers-list'
34
36
 
35
37
  export default function SalesChannelOffersListPage() {
36
38
  const t = useT()
39
+ const { enabled: channelsEnabled, isLoading: channelsEnabledLoading } = useSalesChannelsEnabled()
37
40
  const router = useRouter()
38
41
  const { runMutation, retryLastMutation } = useGuardedMutation<{
39
42
  formId: string
@@ -332,6 +335,10 @@ export default function SalesChannelOffersListPage() {
332
335
  </div>
333
336
  )
334
337
 
338
+ if (!channelsEnabled && !channelsEnabledLoading) {
339
+ return <SalesChannelsDisabledNotice />
340
+ }
341
+
335
342
  return (
336
343
  <Page>
337
344
  <PageBody>
@@ -1,6 +1,10 @@
1
+ import type { RouteVisibilityContext } from '@open-mercato/shared/modules/registry'
2
+ import { isSalesChannelsEnabledForTenant } from '../../../lib/salesChannelsToggle'
3
+
1
4
  export const metadata = {
2
5
  requireAuth: true,
3
6
  requireFeatures: ['sales.channels.manage'],
7
+ visible: (ctx: RouteVisibilityContext) => isSalesChannelsEnabledForTenant(ctx.auth?.tenantId ?? null),
4
8
  pageTitle: 'Sales channels',
5
9
  pageTitleKey: 'sales.channels.nav.title',
6
10
  pageGroup: 'Sales',
@@ -16,6 +16,8 @@ import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-
16
16
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
17
17
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
18
18
  import { useT } from '@open-mercato/shared/lib/i18n/context'
19
+ import { useSalesChannelsEnabled } from '../../../components/useSalesChannelsEnabled'
20
+ import { SalesChannelsDisabledNotice } from '../../../components/SalesChannelsDisabledNotice'
19
21
  import { createLogger } from '@open-mercato/shared/lib/logger'
20
22
 
21
23
  const logger = createLogger('sales')
@@ -42,6 +44,7 @@ const SAVE_CONTEXT_ID = 'sales-channels-list'
42
44
 
43
45
  export default function SalesChannelsPage() {
44
46
  const t = useT()
47
+ const { enabled: channelsEnabled, isLoading: channelsEnabledLoading } = useSalesChannelsEnabled()
45
48
  const router = useRouter()
46
49
  const { runMutation, retryLastMutation } = useGuardedMutation<{
47
50
  formId: string
@@ -190,6 +193,10 @@ export default function SalesChannelsPage() {
190
193
  }
191
194
  }, [handleRefresh, mutationContext, runMutation, t])
192
195
 
196
+ if (!channelsEnabled && !channelsEnabledLoading) {
197
+ return <SalesChannelsDisabledNotice />
198
+ }
199
+
193
200
  return (
194
201
  <Page>
195
202
  <PageBody>
@@ -69,6 +69,7 @@ import { readMarkdownPreferenceCookie, writeMarkdownPreferenceCookie } from '@op
69
69
  import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
70
70
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
71
71
  import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
72
+ import { useSalesChannelsEnabled } from '@open-mercato/core/modules/sales/components/useSalesChannelsEnabled'
72
73
  import { createLogger } from '@open-mercato/shared/lib/logger'
73
74
 
74
75
  const logger = createLogger('sales')
@@ -1894,6 +1895,7 @@ export default function SalesDocumentDetailPage({
1894
1895
  includeAmountInMessageMetadata?: boolean
1895
1896
  }) {
1896
1897
  const t = useT()
1898
+ const { enabled: channelsEnabled } = useSalesChannelsEnabled()
1897
1899
  const router = useRouter()
1898
1900
  const searchParams = useSearchParams()
1899
1901
  const pathname = usePathname()
@@ -2578,11 +2580,11 @@ export default function SalesDocumentDetailPage({
2578
2580
  }, [record])
2579
2581
 
2580
2582
  React.useEffect(() => {
2581
- loadChannels().catch(() => {})
2583
+ if (channelsEnabled) loadChannels().catch(() => {})
2582
2584
  loadStatuses().catch(() => {})
2583
2585
  loadShippingMethods().catch(() => {})
2584
2586
  loadPaymentMethods().catch(() => {})
2585
- }, [loadChannels, loadPaymentMethods, loadShippingMethods, loadStatuses, scopeVersion])
2587
+ }, [channelsEnabled, loadChannels, loadPaymentMethods, loadShippingMethods, loadStatuses, scopeVersion])
2586
2588
 
2587
2589
  React.useEffect(() => {
2588
2590
  void refreshPaymentPresence()
@@ -3955,11 +3957,11 @@ export default function SalesDocumentDetailPage({
3955
3957
  emptyLabel: t('sales.documents.detail.empty', 'Not set'),
3956
3958
  type: 'email' as const,
3957
3959
  },
3958
- {
3959
- key: 'channel',
3960
+ ...(channelsEnabled ? [{
3961
+ key: 'channel' as const,
3960
3962
  title: t('sales.documents.detail.channel', 'Channel'),
3961
3963
  value: record?.channelId ?? null,
3962
- },
3964
+ }] : []),
3963
3965
  {
3964
3966
  key: 'status',
3965
3967
  title: t('sales.documents.detail.status', 'Status'),
@@ -0,0 +1,21 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+
7
+ export function SalesChannelsDisabledNotice() {
8
+ const t = useT()
9
+ return (
10
+ <Page>
11
+ <PageBody>
12
+ <div className="rounded-lg border bg-muted/50 p-6 text-sm text-muted-foreground">
13
+ {t(
14
+ 'sales.channels.disabledNotice',
15
+ 'Sales channels are disabled for this tenant. Enable the "Sales Channels" feature toggle to manage them.',
16
+ )}
17
+ </div>
18
+ </PageBody>
19
+ </Page>
20
+ )
21
+ }
@@ -57,6 +57,7 @@ import {
57
57
  type AddressValue,
58
58
  } from '@open-mercato/core/modules/customers/utils/addressFormat'
59
59
  import { AddressEditor, type AddressEditorDraft } from '@open-mercato/core/modules/customers/components/AddressEditor'
60
+ import { useSalesChannelsEnabled } from '../useSalesChannelsEnabled'
60
61
  import { createLogger } from '@open-mercato/shared/lib/logger'
61
62
 
62
63
  const logger = createLogger('sales')
@@ -804,6 +805,7 @@ function CustomerGroupComponent({ values, setValue, t, customers, setCustomers,
804
805
 
805
806
  export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind, inboxPreFill }: SalesDocumentFormProps) {
806
807
  const t = useT()
808
+ const { enabled: channelsEnabled } = useSalesChannelsEnabled()
807
809
  const [customers, setCustomers] = React.useState<CustomerOption[]>([])
808
810
  const [customerLoading, setCustomerLoading] = React.useState(false)
809
811
  const [channels, setChannels] = React.useState<ChannelOption[]>([])
@@ -1174,7 +1176,7 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1174
1176
  />
1175
1177
  ),
1176
1178
  },
1177
- {
1179
+ ...(channelsEnabled ? [{
1178
1180
  id: 'channelId',
1179
1181
  label: t('sales.documents.form.channel', 'Sales channel'),
1180
1182
  type: 'custom',
@@ -1196,7 +1198,7 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1196
1198
  selectedHintLabel={(id) => t('sales.documents.form.channel.selected', 'Selected channel: {{id}}', { id })}
1197
1199
  />
1198
1200
  ),
1199
- },
1201
+ } satisfies CrudField] : []),
1200
1202
  {
1201
1203
  id: 'shippingAddressSection',
1202
1204
  label: '',
@@ -1317,6 +1319,7 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1317
1319
  addressOptions,
1318
1320
  addressesError,
1319
1321
  addressesLoading,
1322
+ channelsEnabled,
1320
1323
  currencyLabels,
1321
1324
  fetchCurrencyOptions,
1322
1325
  loadAddresses,
@@ -1335,12 +1338,13 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1335
1338
  fields: [],
1336
1339
  component: (ctx) => <CustomerGroupComponent {...ctx} t={t} customers={customers} setCustomers={setCustomers} customerQuerySetter={customerQuerySetter} loadAddresses={loadAddresses} loadCustomers={loadCustomers} fetchCustomerEmail={fetchCustomerEmail} resetAddressFormState={resetAddressFormState} />,
1337
1340
  },
1338
- { id: 'channels-comments', title: '', column: 1, fields: ['channelId', 'comments'] },
1341
+ { id: 'channels-comments', title: '', column: 1, fields: channelsEnabled ? ['channelId', 'comments'] : ['comments'] },
1339
1342
  { id: 'currency', title: '', column: 2, fields: ['currencyCode'] },
1340
1343
  { id: 'shipping', title: '', column: 2, fields: ['shippingAddressSection'] },
1341
1344
  { id: 'billing', title: '', column: 2, fields: ['billingAddressSection'] },
1342
1345
  { id: 'custom', title: t('sales.documents.form.customFields', 'Custom fields'), column: 2, kind: 'customFields' },
1343
1346
  ], [
1347
+ channelsEnabled,
1344
1348
  customers,
1345
1349
  fetchCustomerEmail,
1346
1350
  loadAddresses,
@@ -24,6 +24,7 @@ import {
24
24
  normalizeDictionaryEntries,
25
25
  } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
26
26
  import { SALES_DOCUMENT_NUMBER_COLUMN_META } from './salesDocumentsColumns'
27
+ import { useSalesChannelsEnabled } from '../useSalesChannelsEnabled'
27
28
  import { createLogger } from '@open-mercato/shared/lib/logger'
28
29
 
29
30
  const logger = createLogger('sales')
@@ -146,6 +147,7 @@ function normalizeNumberInput(value: unknown): number | null {
146
147
 
147
148
  export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
148
149
  const t = useT()
150
+ const { enabled: channelsEnabled } = useSalesChannelsEnabled()
149
151
  const router = useRouter()
150
152
  const scopeVersion = useOrganizationScopeVersion()
151
153
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
@@ -293,20 +295,20 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
293
295
  )
294
296
 
295
297
  React.useEffect(() => {
296
- loadChannelOptions().catch(() => {})
298
+ if (channelsEnabled) loadChannelOptions().catch(() => {})
297
299
  loadTagOptions().catch(() => {})
298
300
  loadCustomerOptions().catch(() => {})
299
301
  loadStatusMap().catch(() => setStatusMap({}))
300
- }, [loadChannelOptions, loadCustomerOptions, loadStatusMap, loadTagOptions, scopeVersion])
302
+ }, [channelsEnabled, loadChannelOptions, loadCustomerOptions, loadStatusMap, loadTagOptions, scopeVersion])
301
303
 
302
304
  const filters = React.useMemo<FilterDef[]>(() => [
303
- {
305
+ ...(channelsEnabled ? [{
304
306
  id: 'channelId',
305
307
  label: t('sales.documents.list.filters.channel', 'Channel'),
306
308
  type: 'select',
307
309
  options: channelOptions,
308
310
  loadOptions: loadChannelOptions,
309
- },
311
+ } satisfies FilterDef] : []),
310
312
  {
311
313
  id: 'date',
312
314
  label: t('sales.documents.list.filters.date', 'Date'),
@@ -363,7 +365,7 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
363
365
  formatValue: (val: string) => tagOptions.find((o) => o.value === val)?.label ?? val,
364
366
  formatDescription: (val: string) => tagOptions.find((o) => o.value === val)?.description ?? null,
365
367
  },
366
- ], [channelOptions, loadChannelOptions, customerOptions, loadCustomerOptions, loadTagOptions, tagOptions, t])
368
+ ], [channelsEnabled, channelOptions, loadChannelOptions, customerOptions, loadCustomerOptions, loadTagOptions, tagOptions, t])
367
369
 
368
370
  const queryParams = React.useMemo(() => {
369
371
  const params = new URLSearchParams()
@@ -615,7 +617,7 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
615
617
  ),
616
618
  enableSorting: false,
617
619
  },
618
- {
620
+ ...(channelsEnabled ? [{
619
621
  accessorKey: 'channelId',
620
622
  header: t('sales.documents.list.table.channel', 'Channel'),
621
623
  cell: ({ row }) => {
@@ -627,7 +629,7 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
627
629
  )
628
630
  },
629
631
  enableSorting: false,
630
- },
632
+ } satisfies ColumnDef<SalesDocumentRow>] : []),
631
633
  {
632
634
  id: 'lineItemCount',
633
635
  accessorKey: 'lineItemCount',
@@ -661,7 +663,7 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
661
663
  ? <span className="text-xs text-muted-foreground">{new Date(row.original.date).toLocaleString()}</span>
662
664
  : <span className="text-xs text-muted-foreground">—</span>,
663
665
  },
664
- ], [channelOptions, kind, statusMap, t])
666
+ ], [channelsEnabled, channelOptions, kind, statusMap, t])
665
667
 
666
668
  const emptyLabel = kind === 'order'
667
669
  ? t('sales.documents.list.table.emptyOrders', 'No orders yet.')
@@ -0,0 +1,59 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
5
+ import { SALES_CHANNELS_TOGGLE_ID } from '../lib/salesChannelsToggleId'
6
+
7
+ export { SALES_CHANNELS_TOGGLE_ID }
8
+
9
+ const CACHE_TTL_MS = 60_000
10
+
11
+ type FeatureToggleCheckResponse = {
12
+ ok?: boolean
13
+ value?: unknown
14
+ }
15
+
16
+ // Fails open: channels stay visible unless the toggle explicitly resolves to
17
+ // false. A missing toggle definition (404) or an absent feature_toggles module
18
+ // must not hide sales channel UI.
19
+ async function fetchSalesChannelsEnabled(): Promise<boolean> {
20
+ try {
21
+ const call = await apiCall<FeatureToggleCheckResponse>(
22
+ `/api/feature_toggles/check/boolean?identifier=${SALES_CHANNELS_TOGGLE_ID}`,
23
+ )
24
+ if (!call.ok) return true
25
+ return !(call.result?.ok === true && call.result.value === false)
26
+ } catch {
27
+ return true
28
+ }
29
+ }
30
+
31
+ let cache: { value: Promise<boolean>; at: number } | null = null
32
+
33
+ function getSalesChannelsEnabled(): Promise<boolean> {
34
+ const now = Date.now()
35
+ if (!cache || now - cache.at > CACHE_TTL_MS) {
36
+ cache = { value: fetchSalesChannelsEnabled(), at: now }
37
+ }
38
+ return cache.value
39
+ }
40
+
41
+ export function useSalesChannelsEnabled(): { enabled: boolean; isLoading: boolean } {
42
+ const [resolved, setResolved] = React.useState<boolean | null>(null)
43
+
44
+ React.useEffect(() => {
45
+ let active = true
46
+ getSalesChannelsEnabled()
47
+ .then((value) => {
48
+ if (active) setResolved(value)
49
+ })
50
+ .catch(() => {
51
+ if (active) setResolved(true)
52
+ })
53
+ return () => {
54
+ active = false
55
+ }
56
+ }, [])
57
+
58
+ return { enabled: resolved !== false, isLoading: resolved === null }
59
+ }
@@ -60,6 +60,7 @@
60
60
  "sales.channels.actions.backToList": "Zurück zu Kanälen",
61
61
  "sales.channels.actions.create": "Kanal hinzufügen",
62
62
  "sales.channels.create": "Vertriebskanal erstellen",
63
+ "sales.channels.disabledNotice": "Vertriebskanäle sind für diesen Mandanten deaktiviert. Aktivieren Sie das Feature-Toggle \"Sales Channels\", um sie zu verwalten.",
63
64
  "sales.channels.form.address1": "Adresszeile 1",
64
65
  "sales.channels.form.address2": "Adresszeile 2",
65
66
  "sales.channels.form.city": "Stadt",
@@ -60,6 +60,7 @@
60
60
  "sales.channels.actions.backToList": "Back to channels",
61
61
  "sales.channels.actions.create": "Add channel",
62
62
  "sales.channels.create": "Create sales channel",
63
+ "sales.channels.disabledNotice": "Sales channels are disabled for this tenant. Enable the \"Sales Channels\" feature toggle to manage them.",
63
64
  "sales.channels.form.address1": "Address line 1",
64
65
  "sales.channels.form.address2": "Address line 2",
65
66
  "sales.channels.form.city": "City",
@@ -60,6 +60,7 @@
60
60
  "sales.channels.actions.backToList": "Volver a canales",
61
61
  "sales.channels.actions.create": "Añadir canal",
62
62
  "sales.channels.create": "Crear canal de ventas",
63
+ "sales.channels.disabledNotice": "Los canales de venta están desactivados para este inquilino. Active el conmutador de función \"Sales Channels\" para gestionarlos.",
63
64
  "sales.channels.form.address1": "Línea de dirección 1",
64
65
  "sales.channels.form.address2": "Línea de dirección 2",
65
66
  "sales.channels.form.city": "Ciudad",
@@ -60,6 +60,7 @@
60
60
  "sales.channels.actions.backToList": "Powrót do kanałów",
61
61
  "sales.channels.actions.create": "Dodaj kanał",
62
62
  "sales.channels.create": "Utwórz kanał sprzedaży",
63
+ "sales.channels.disabledNotice": "Kanały sprzedaży są wyłączone dla tego najemcy. Włącz przełącznik funkcji \"Sales Channels\", aby nimi zarządzać.",
63
64
  "sales.channels.form.address1": "Adres linia 1",
64
65
  "sales.channels.form.address2": "Adres linia 2",
65
66
  "sales.channels.form.city": "Miasto",
@@ -0,0 +1,26 @@
1
+ import { SALES_CHANNELS_TOGGLE_ID } from './salesChannelsToggleId'
2
+
3
+ export { SALES_CHANNELS_TOGGLE_ID }
4
+
5
+ type BoolConfigResult = { ok: boolean; value?: unknown }
6
+
7
+ type FeatureTogglesServiceLike = {
8
+ getBoolConfig: (identifier: string, tenantId: string) => Promise<BoolConfigResult>
9
+ }
10
+
11
+ // Server-side counterpart of useSalesChannelsEnabled with the same fail-open
12
+ // contract: channels stay enabled unless the toggle explicitly resolves to
13
+ // false. The DI container and feature_toggles service are optional peers, so
14
+ // they are loaded lazily and any failure keeps channels visible.
15
+ export async function isSalesChannelsEnabledForTenant(tenantId: string | null | undefined): Promise<boolean> {
16
+ if (!tenantId) return true
17
+ try {
18
+ const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
19
+ const container = await createRequestContainer()
20
+ const service = container.resolve('featureTogglesService') as FeatureTogglesServiceLike
21
+ const result = await service.getBoolConfig(SALES_CHANNELS_TOGGLE_ID, tenantId)
22
+ return !(result.ok === true && result.value === false)
23
+ } catch {
24
+ return true
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ export const SALES_CHANNELS_TOGGLE_ID = 'sales_channels_enabled'
@@ -5,6 +5,7 @@ import Link from 'next/link'
5
5
  import { usePathname, useRouter, useSearchParams } from 'next/navigation'
6
6
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
8
+ import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
8
9
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
9
10
  import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
10
11
  import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
@@ -432,29 +433,22 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
432
433
  </div>
433
434
  </div>
434
435
 
435
- <div className="border-b">
436
- <nav
437
- className="flex flex-wrap items-center gap-5 text-sm"
436
+ <Tabs
437
+ value={activePanel}
438
+ onValueChange={(value) => setActivePanel(value as 'details' | 'availability' | 'jobHistory')}
439
+ variant="underline"
440
+ >
441
+ <TabsList
442
+ className="w-full flex-wrap"
438
443
  aria-label={t('staff.teamMembers.detail.tabs.label', 'Team member sections')}
439
444
  >
440
445
  {panelTabs.map((tab) => (
441
- <button
442
- key={tab.id}
443
- type="button"
444
- role="tab"
445
- aria-selected={activePanel === tab.id}
446
- onClick={() => setActivePanel(tab.id)}
447
- className={`relative -mb-px border-b-2 px-0 py-2 text-sm font-medium transition-colors ${
448
- activePanel === tab.id
449
- ? 'border-accent-indigo text-foreground'
450
- : 'border-transparent text-muted-foreground hover:text-foreground'
451
- }`}
452
- >
446
+ <TabsTrigger key={tab.id} value={tab.id}>
453
447
  {tab.label}
454
- </button>
448
+ </TabsTrigger>
455
449
  ))}
456
- </nav>
457
- </div>
450
+ </TabsList>
451
+ </Tabs>
458
452
 
459
453
  {activePanel === 'details' ? (
460
454
  <>
@@ -500,22 +494,19 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
500
494
 
501
495
  <div className="rounded-lg border bg-card p-4">
502
496
  <div className="flex flex-wrap items-center justify-between gap-3">
503
- <div className="flex gap-2">
504
- {tabs.map((tab) => (
505
- <button
506
- key={tab.id}
507
- type="button"
508
- onClick={() => setActiveTab(tab.id)}
509
- className={`relative -mb-px border-b-2 px-0 py-1 text-sm font-medium transition-colors ${
510
- activeTab === tab.id
511
- ? 'border-accent-indigo text-foreground'
512
- : 'border-transparent text-muted-foreground hover:text-foreground'
513
- }`}
514
- >
515
- {tab.label}
516
- </button>
517
- ))}
518
- </div>
497
+ <Tabs
498
+ value={activeTab}
499
+ onValueChange={(value) => setActiveTab(value as 'notes' | 'activities' | 'addresses')}
500
+ variant="underline"
501
+ >
502
+ <TabsList className="h-auto flex-wrap border-b-0">
503
+ {tabs.map((tab) => (
504
+ <TabsTrigger key={tab.id} value={tab.id}>
505
+ {tab.label}
506
+ </TabsTrigger>
507
+ ))}
508
+ </TabsList>
509
+ </Tabs>
519
510
  {sectionAction ? (
520
511
  <Button
521
512
  type="button"
@@ -12,6 +12,7 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
12
12
  import { DataTable, withDataTableNamespaces } from '@open-mercato/ui/backend/DataTable'
13
13
  import { RowActions } from '@open-mercato/ui/backend/RowActions'
14
14
  import { Button } from '@open-mercato/ui/primitives/button'
15
+ import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
15
16
  import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
16
17
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
17
18
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -362,29 +363,22 @@ export default function StaffTeamEditPage({ params }: { params?: { id?: string }
362
363
  <Page>
363
364
  <PageBody>
364
365
  <div className="space-y-6">
365
- <div className="border-b">
366
- <nav className="flex flex-wrap items-center gap-5 text-sm" aria-label={memberLabels.tabs.label}>
366
+ <Tabs
367
+ value={activeTab}
368
+ onValueChange={(value) => setActiveTab(value as 'details' | 'members')}
369
+ variant="underline"
370
+ >
371
+ <TabsList className="w-full flex-wrap" aria-label={memberLabels.tabs.label}>
367
372
  {[
368
373
  { id: 'details', label: memberLabels.tabs.details },
369
374
  { id: 'members', label: memberLabels.tabs.members },
370
375
  ].map((tab) => (
371
- <button
372
- key={tab.id}
373
- type="button"
374
- role="tab"
375
- aria-selected={activeTab === tab.id}
376
- onClick={() => setActiveTab(tab.id as 'details' | 'members')}
377
- className={`relative -mb-px border-b-2 px-0 py-2 text-sm font-medium transition-colors ${
378
- activeTab === tab.id
379
- ? 'border-accent-indigo text-foreground'
380
- : 'border-transparent text-muted-foreground hover:text-foreground'
381
- }`}
382
- >
376
+ <TabsTrigger key={tab.id} value={tab.id}>
383
377
  {tab.label}
384
- </button>
378
+ </TabsTrigger>
385
379
  ))}
386
- </nav>
387
- </div>
380
+ </TabsList>
381
+ </Tabs>
388
382
 
389
383
  {activeTab === 'details' ? (
390
384
  <TeamForm
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { Button } from '@open-mercato/ui/primitives/button'
4
+ import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
5
5
 
6
6
  export type SavedViewTab = {
7
7
  id: string
@@ -19,35 +19,18 @@ export type SavedViewTabsProps = {
19
19
 
20
20
  export function SavedViewTabs({ tabs, activeId, onSelect, className, ariaLabel }: SavedViewTabsProps) {
21
21
  return (
22
- <div
23
- role="tablist"
24
- aria-label={ariaLabel ?? 'Saved views'}
25
- className={`flex items-center gap-1 border-b border-border ${className ?? ''}`}
26
- >
27
- {tabs.map((tab) => {
28
- const isActive = tab.id === activeId
29
- return (
30
- <Button
22
+ <Tabs value={activeId} onValueChange={onSelect} variant="underline" className={className}>
23
+ <TabsList className="w-full flex-wrap" aria-label={ariaLabel ?? 'Saved views'}>
24
+ {tabs.map((tab) => (
25
+ <TabsTrigger
31
26
  key={tab.id}
32
- type="button"
33
- variant="ghost"
34
- size="sm"
35
- role="tab"
36
- aria-selected={isActive}
37
- className={`h-auto rounded-none px-3 py-2 text-sm hover:bg-transparent ${
38
- isActive
39
- ? 'border-b-2 border-foreground font-medium text-foreground'
40
- : 'border-b-2 border-transparent text-muted-foreground hover:text-foreground'
41
- }`}
42
- onClick={() => onSelect(tab.id)}
27
+ value={tab.id}
28
+ count={typeof tab.count === 'number' ? tab.count : undefined}
43
29
  >
44
30
  {tab.label}
45
- {typeof tab.count === 'number' ? (
46
- <span className="ml-1.5 text-xs text-muted-foreground/70 tabular-nums">{tab.count}</span>
47
- ) : null}
48
- </Button>
49
- )
50
- })}
51
- </div>
31
+ </TabsTrigger>
32
+ ))}
33
+ </TabsList>
34
+ </Tabs>
52
35
  )
53
36
  }