@controleonline/ui-common 1.2.79 → 1.2.81

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 (91) hide show
  1. package/AGENTS.md +8 -0
  2. package/package.json +9 -3
  3. package/src/api/fetch.js +7 -7
  4. package/src/react/components/AddImportModal.js +6 -2
  5. package/src/react/components/BackgroundRuntimeBridge.js +201 -215
  6. package/src/react/components/ConfirmModal.js +44 -9
  7. package/src/react/components/ConfirmModal.styles.js +55 -41
  8. package/src/react/components/CopyDeviceConfigModal.js +193 -0
  9. package/src/react/components/CopyDeviceConfigModal.styles.js +123 -0
  10. package/src/react/components/DefaultProvider.native.js +22 -0
  11. package/src/react/components/DefaultProvider.web.js +25 -1
  12. package/src/react/components/EntityLogBranch.js +482 -0
  13. package/src/react/components/EntityLogCards.js +369 -0
  14. package/src/react/components/EntityLogContent.js +113 -794
  15. package/src/react/components/LinkedOrderProductsTab.js +3 -3
  16. package/src/react/components/PrintService.js +130 -409
  17. package/src/react/components/RemoteCheckoutService.js +2 -2
  18. package/src/react/components/RuntimeBottomNavigationBar.js +141 -146
  19. package/src/react/components/RuntimeFooterMarqueeText.js +137 -0
  20. package/src/react/components/RuntimeInfoFooter.js +12 -15
  21. package/src/react/components/SystemErrorToast.js +49 -29
  22. package/src/react/components/SystemErrorToast.styles.js +43 -38
  23. package/src/react/components/UserAvatar.js +120 -28
  24. package/src/react/config/deviceConfigBootstrap.js +330 -304
  25. package/src/react/hooks/usePrintSpoolEffects.js +161 -0
  26. package/src/react/pages/DeviceDetailPage.js +6 -2882
  27. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +233 -0
  28. package/src/react/pages/Devices/detail/DeviceDetailHeader.js +138 -0
  29. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +167 -0
  30. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +216 -0
  31. package/src/react/pages/Devices/detail/DeviceDetailPaymentSection.js +40 -0
  32. package/src/react/pages/Devices/detail/DeviceDetailPdvConfigSection.js +390 -0
  33. package/src/react/pages/Devices/detail/DeviceDetailRenderers.js +135 -0
  34. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +493 -0
  35. package/src/react/pages/Devices/detail/OptionButtonChip.js +65 -0
  36. package/src/react/pages/Devices/detail/deviceDetailConstants.js +36 -0
  37. package/src/react/pages/Devices/detail/deviceDetailHelpers.js +70 -0
  38. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +483 -0
  39. package/src/react/pages/Devices/detail/useDeviceDetailCopyConfig.js +87 -0
  40. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +327 -0
  41. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +429 -0
  42. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +484 -0
  43. package/src/react/pages/Devices/detail/useDeviceDetailStateB.js +71 -0
  44. package/src/react/pages/Devices/deviceTypes/index.js +2 -0
  45. package/src/react/pages/Devices/deviceTypes/pdv.js +11 -0
  46. package/src/react/pages/Imports.js +3 -2
  47. package/src/react/pages/IntegrationConfigFields.js +126 -0
  48. package/src/react/pages/IntegrationConfigPage.js +122 -591
  49. package/src/react/pages/IntegrationConfigPage.utils.js +95 -0
  50. package/src/react/print/usePrintButtonController.js +9 -1
  51. package/src/react/services/Getnet/Checkout.js +81 -0
  52. package/src/react/services/Getnet/Getnet.js +77 -0
  53. package/src/react/services/paymentGatewayExecution.js +20 -0
  54. package/src/react/utils/bottomNavigationToolbar.js +161 -0
  55. package/src/react/utils/commercialDocumentOrders.js +18 -6
  56. package/src/react/utils/copyDeviceConfigs.js +198 -0
  57. package/src/react/utils/fileUrl.js +43 -0
  58. package/src/react/utils/importStatus.js +19 -10
  59. package/src/react/utils/logSettings.js +7 -7
  60. package/src/react/utils/maintenanceSettings.js +16 -0
  61. package/src/react/utils/passwordPolicy.js +73 -0
  62. package/src/react/utils/paymentDevices.js +6 -0
  63. package/src/react/utils/printManagedPrinter.js +45 -0
  64. package/src/react/utils/printRouting.js +63 -0
  65. package/src/react/utils/printSpoolUtils.js +119 -0
  66. package/src/react/utils/printerDevices.js +368 -368
  67. package/src/react/utils/runtimeFooter.js +6 -1
  68. package/src/react/utils/shopFranchises.js +186 -65
  69. package/src/react/utils/systemErrorMessage.js +71 -67
  70. package/src/store/device_config/customActions.js +119 -30
  71. package/src/tests/browser/manager/bottom-navigation-role-toolbar.spec.js +353 -0
  72. package/src/tests/browser/manager/device-detail-alias-save.spec.js +345 -0
  73. package/src/tests/browser/manager/device-detail-copy-config.spec.js +365 -0
  74. package/src/tests/browser/manager/runtime-footer-marquee.spec.js +262 -0
  75. package/src/tests/browser/manager/theme-confirm-toast.spec.js +236 -0
  76. package/src/tests/react/components/RuntimeFooterMarqueeText.test.js +171 -0
  77. package/src/tests/react/components/UserAvatar.test.js +6 -2
  78. package/src/tests/react/components/themeAwareModals.test.mjs +60 -0
  79. package/src/tests/react/config/deviceConfigBootstrap.test.js +104 -66
  80. package/src/tests/react/pages/deviceDetailDelete.test.js +78 -0
  81. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +48 -0
  82. package/src/tests/react/utils/bottomNavigationToolbar.test.js +222 -0
  83. package/src/tests/react/utils/copyDeviceConfigs.test.mjs +127 -0
  84. package/src/tests/react/utils/fileUrl.test.js +38 -0
  85. package/src/tests/react/utils/importStatus.test.js +27 -10
  86. package/src/tests/react/utils/passwordPolicy.test.js +27 -0
  87. package/src/tests/react/utils/printRouting.test.mjs +86 -0
  88. package/src/tests/react/utils/printSpoolUtils.test.mjs +52 -0
  89. package/src/tests/react/utils/runtimeFooter.test.js +10 -8
  90. package/src/tests/react/utils/shopFranchises.test.js +121 -115
  91. package/src/tests/react/utils/systemErrorMessage.test.js +32 -59
@@ -0,0 +1,95 @@
1
+ import { Linking, Platform } from 'react-native';
2
+
3
+ export const ROUTE_PROVIDER_MAP = {
4
+ UberIntegrationPage: 'uber',
5
+ AsaasIntegrationPage: 'asaas',
6
+ ClickSignIntegrationPage: 'clicksign',
7
+ ReceitaFederalIntegrationPage: 'receita-federal',
8
+ };
9
+
10
+ export const routeNameToPath = routeName =>
11
+ String(routeName || '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
12
+
13
+ export const normalizeTextValue = value => {
14
+ let text = String(value ?? '').trim();
15
+ if (
16
+ (text.startsWith('"') && text.endsWith('"') && text.length >= 2) ||
17
+ (text.startsWith("'") && text.endsWith("'") && text.length >= 2)
18
+ ) {
19
+ text = text.slice(1, -1).trim();
20
+ }
21
+ return text;
22
+ };
23
+
24
+ export const formatApiError = error => {
25
+ if (!error) return 'Nao foi possivel carregar a configuracao da integracao.';
26
+ if (typeof error === 'string') return error;
27
+ return error?.message || error?.description || error?.errmsg || 'Nao foi possivel carregar a configuracao da integracao.';
28
+ };
29
+
30
+ export const getConfigFields = providerConfig => {
31
+ if (!providerConfig) return [];
32
+ if (Array.isArray(providerConfig.tabs) && providerConfig.tabs.length > 0) {
33
+ return providerConfig.tabs.flatMap(tab => tab.fields || []);
34
+ }
35
+ return providerConfig.fields || [];
36
+ };
37
+
38
+ export const buildFieldValues = (providerConfig, source) => {
39
+ const sourceMap = Array.isArray(source)
40
+ ? source.reduce((acc, item) => {
41
+ const key = String(item?.configKey || '').trim();
42
+ if (key) acc[key] = item?.configValue;
43
+ return acc;
44
+ }, {})
45
+ : source && typeof source === 'object' ? source : {};
46
+
47
+ return getConfigFields(providerConfig).reduce((acc, field) => {
48
+ acc[field.key] = normalizeTextValue(sourceMap[field.key]);
49
+ return acc;
50
+ }, {});
51
+ };
52
+
53
+ export const resolveProviderId = ({ route, currentCompany }) => {
54
+ const routeCompanyId = String(route?.params?.companyId || '').replace(/\D/g, '');
55
+ if (routeCompanyId) return Number(routeCompanyId);
56
+
57
+ const currentCompanyId = String(currentCompany?.id || '').replace(/\D/g, '');
58
+ return currentCompanyId ? Number(currentCompanyId) : null;
59
+ };
60
+
61
+ export const resolveFallbackConfigs = ({ providerId, currentCompany }) =>
62
+ Number(providerId) === Number(currentCompany?.id) ? currentCompany?.configs || {} : {};
63
+
64
+ export const isConnectedValue = value =>
65
+ value === true || value === 1 || value === '1' || String(value).trim().toLowerCase() === 'true';
66
+
67
+ export const toConfigRequestValue = value => {
68
+ if (value === undefined || value === null) return '';
69
+ if (typeof value === 'string') return normalizeTextValue(value);
70
+ if (typeof value === 'object') return JSON.stringify(value);
71
+ return String(value);
72
+ };
73
+
74
+ export const extractAuthorizationUrl = response => normalizeTextValue(
75
+ response?.member?.[0]?.authorization_url || response?.member?.[0]?.auth_url ||
76
+ response?.member?.[0]?.url || response?.authorization_url || response?.auth_url ||
77
+ response?.url || response?.data?.authorization_url || response?.data?.auth_url || response?.data?.url,
78
+ );
79
+
80
+ export const formatUberOAuthError = error => {
81
+ const normalized = normalizeTextValue(error).toLowerCase();
82
+ if (normalized === 'invalid_scope') {
83
+ return 'O Uber nao liberou o scope pos_provisioning para este app. Esse app precisa estar aprovado/whitelisted no dashboard do Uber.';
84
+ }
85
+ if (normalized === 'access_denied') return 'O login do Uber foi cancelado.';
86
+ return normalizeTextValue(error) || 'Nao foi possivel concluir a conexao com o Uber.';
87
+ };
88
+
89
+ export const openAuthorizationUrl = async authUrl => {
90
+ if (Platform.OS === 'web' && typeof window !== 'undefined' && typeof window.location?.assign === 'function') {
91
+ window.location.assign(authUrl);
92
+ return;
93
+ }
94
+ await Linking.openURL(authUrl);
95
+ };
@@ -28,6 +28,7 @@ import {
28
28
  resolvePrinterSelectionValue,
29
29
  resolveSelectedPrinter,
30
30
  } from '@controleonline/ui-common/src/react/print/selection';
31
+ import {resolvePrintRoutingError} from '@controleonline/ui-common/src/react/utils/printRouting';
31
32
 
32
33
  const resolveCurrentItemId = storeGetters =>
33
34
  storeGetters?.item?.['@id']
@@ -396,7 +397,14 @@ export const usePrintButtonController = ({
396
397
  return;
397
398
  }
398
399
 
399
- setError(global.t?.t('orders', 'title', 'selectPrinter') || 'Selecione a impressora.');
400
+ setError(
401
+ resolvePrintRoutingError({
402
+ targetDeviceId: selectedPrinter?.device,
403
+ selectedPrinter,
404
+ }) ||
405
+ global.t?.t('orders', 'title', 'selectPrinter') ||
406
+ 'Selecione a impressora.',
407
+ );
400
408
  return;
401
409
  }
402
410
 
@@ -0,0 +1,81 @@
1
+ import GetnetService from './Getnet';
2
+
3
+ /**
4
+ * Map wallet paymentCode / paymentType label to Getnet deeplink paymentType.
5
+ */
6
+ export const resolveGetnetPaymentType = payment => {
7
+ const raw = String(
8
+ payment?.paymentCode ||
9
+ payment?.paymentType?.paymentType ||
10
+ payment?.paymentType?.name ||
11
+ payment?.type ||
12
+ '',
13
+ )
14
+ .trim()
15
+ .toLowerCase();
16
+
17
+ if (!raw) return 'credit';
18
+
19
+ if (raw.includes('pix')) return 'pix';
20
+ if (raw.includes('debit') || raw.includes('debito') || raw.includes('débito')) {
21
+ return 'debit';
22
+ }
23
+ if (raw.includes('voucher') || raw.includes('vr') || raw.includes('va')) {
24
+ return 'voucher';
25
+ }
26
+ if (raw.includes('credit') || raw.includes('credito') || raw.includes('crédito')) {
27
+ return 'credit';
28
+ }
29
+
30
+ return 'credit';
31
+ };
32
+
33
+ export const runGetnetCheckoutPayment = async ({
34
+ installments = null,
35
+ order = null,
36
+ payment = null,
37
+ total = 0,
38
+ }) => {
39
+ const resolvedTotal = Number(total || 0);
40
+
41
+ if (resolvedTotal <= 0) {
42
+ throw new Error('Informe um valor de pagamento valido.');
43
+ }
44
+
45
+ const orderId =
46
+ order?.id ||
47
+ String(order?.['@id'] || '')
48
+ .split('/')
49
+ .pop() ||
50
+ '';
51
+
52
+ const callerId = `getnet-${orderId || 'order'}-${Date.now()}`;
53
+ const paymentType = resolveGetnetPaymentType(payment);
54
+ const resolvedInstallments =
55
+ Number(installments || payment?.installments || 1) || 1;
56
+
57
+ const response = await new GetnetService().payment({
58
+ amount: resolvedTotal,
59
+ paymentType,
60
+ callerId,
61
+ installments: resolvedInstallments,
62
+ orderId: orderId ? String(orderId) : undefined,
63
+ allowPrintCurrentTransaction: true,
64
+ });
65
+
66
+ if (!response?.success) {
67
+ const detail =
68
+ response?.result?.resultDetails ||
69
+ response?.result?.result ||
70
+ response?.code ||
71
+ 'Pagamento Getnet nao aprovado.';
72
+ throw new Error(
73
+ typeof detail === 'string' ? detail : JSON.stringify(detail),
74
+ );
75
+ }
76
+
77
+ return {
78
+ paidAmount: resolvedTotal,
79
+ response,
80
+ };
81
+ };
@@ -0,0 +1,77 @@
1
+ import Getnet from '@controleonline/react-native-getnet-payment';
2
+
3
+ /**
4
+ * Thin adapter over the native Getnet POS deeplink module.
5
+ * paymentType: credit | debit | voucher | pix
6
+ */
7
+ class GetnetService {
8
+ async payment({
9
+ amount,
10
+ paymentType = 'credit',
11
+ callerId,
12
+ installments = 1,
13
+ creditType,
14
+ allowPrintCurrentTransaction = true,
15
+ orderId,
16
+ }) {
17
+ if (!callerId) {
18
+ throw new Error('callerId is required for Getnet payment.');
19
+ }
20
+
21
+ const resolvedAmount = Number(amount || 0);
22
+ if (resolvedAmount <= 0) {
23
+ throw new Error('Informe um valor de pagamento valido.');
24
+ }
25
+
26
+ const params = {
27
+ amount: resolvedAmount,
28
+ paymentType: String(paymentType || 'credit').toLowerCase(),
29
+ callerId: String(callerId),
30
+ installments: Number(installments) > 0 ? Number(installments) : 1,
31
+ allowPrintCurrentTransaction: Boolean(allowPrintCurrentTransaction),
32
+ };
33
+
34
+ if (params.installments > 1 && creditType) {
35
+ params.creditType = creditType;
36
+ }
37
+
38
+ if (orderId) {
39
+ params.orderId = String(orderId);
40
+ }
41
+
42
+ const response = await Getnet.payment(params);
43
+
44
+ return {
45
+ success: Boolean(response?.success),
46
+ code: response?.result ?? response?.code ?? '',
47
+ result: response,
48
+ authorizationCode: response?.authorizationCode,
49
+ nsu: response?.nsu,
50
+ cvNumber: response?.cvNumber,
51
+ };
52
+ }
53
+
54
+ async refund({amount, cvNumber, transactionDate, originTerminal}) {
55
+ const response = await Getnet.refund({
56
+ amount: Number(amount || 0),
57
+ cvNumber,
58
+ transactionDate,
59
+ originTerminal,
60
+ });
61
+
62
+ return {
63
+ success: Boolean(response?.success),
64
+ result: response,
65
+ };
66
+ }
67
+
68
+ async reprint() {
69
+ return Getnet.reprint();
70
+ }
71
+
72
+ async checkStatus(callerId) {
73
+ return Getnet.checkStatus(callerId);
74
+ }
75
+ }
76
+
77
+ export default GetnetService;
@@ -1,9 +1,11 @@
1
1
  import {runCieloCheckoutPayment} from './Cielo/Checkout';
2
2
  import {runInfinitePayCheckoutPayment} from './InfinitePay/Checkout';
3
+ import {runGetnetCheckoutPayment} from './Getnet/Checkout';
3
4
 
4
5
  import {
5
6
  PAYMENT_GATEWAY_CIELO,
6
7
  PAYMENT_GATEWAY_INFINITE_PAY,
8
+ PAYMENT_GATEWAY_GETNET,
7
9
  } from '../utils/paymentDevices';
8
10
 
9
11
  export const normalizeGatewayPaymentError = (
@@ -87,5 +89,23 @@ export const runConfiguredGatewayPayment = async ({
87
89
  };
88
90
  }
89
91
 
92
+ if (gateway === PAYMENT_GATEWAY_GETNET) {
93
+ const {response, paidAmount} = await runGetnetCheckoutPayment({
94
+ installments,
95
+ order,
96
+ payment,
97
+ total: resolvedTotal,
98
+ });
99
+
100
+ if (!response?.success) {
101
+ throw new Error(normalizeGatewayPaymentError(response?.result));
102
+ }
103
+
104
+ return {
105
+ paidAmount,
106
+ response,
107
+ };
108
+ }
109
+
90
110
  throw new Error('Gateway de pagamento indisponível neste equipamento.');
91
111
  };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Toolbar bottom-nav helpers: role-aware menus + safe fallback.
3
+ * Preset is never a permission bypass — only Home/Profile on empty/failed API.
4
+ * CommonJS so unit tests can run with node --test like runtimeLanguage.
5
+ */
6
+
7
+ /** Routes allowed in the controlled fallback (never business routes). */
8
+ const SAFE_TOOLBAR_FALLBACK_ROUTES = Object.freeze([
9
+ 'HomePage',
10
+ 'ProfilePage',
11
+ ]);
12
+
13
+ const SAFE_ROUTE_SET = new Set(SAFE_TOOLBAR_FALLBACK_ROUTES);
14
+
15
+ const normalizeMenuType = (value, fallback = 'home') => {
16
+ const normalized = String(value == null ? '' : value)
17
+ .trim()
18
+ .toLowerCase();
19
+ return normalized || fallback;
20
+ };
21
+
22
+ const filterMenusByType = (menus, menuType) => {
23
+ const normalizedMenuType =
24
+ menuType === null || menuType === undefined || String(menuType).trim() === ''
25
+ ? null
26
+ : normalizeMenuType(menuType, '');
27
+
28
+ const modules = Array.isArray(menus)
29
+ ? menus.map(module => ({
30
+ ...module,
31
+ menus: Array.isArray(module?.menus)
32
+ ? module.menus.map(menu => ({...menu}))
33
+ : [],
34
+ }))
35
+ : [];
36
+
37
+ if (!normalizedMenuType) {
38
+ return modules;
39
+ }
40
+
41
+ return modules
42
+ .map(module => ({
43
+ ...module,
44
+ menus: module.menus.filter(
45
+ menu => normalizeMenuType(menu?.menuType, '') === normalizedMenuType,
46
+ ),
47
+ }))
48
+ .filter(module => module.menus.length > 0);
49
+ };
50
+
51
+ const resolvePresetLabel = (translate, descriptor) => {
52
+ if (!descriptor || typeof descriptor === 'string') {
53
+ return descriptor || '';
54
+ }
55
+
56
+ const translated = translate?.(
57
+ descriptor.store,
58
+ descriptor.type,
59
+ descriptor.key,
60
+ );
61
+
62
+ return translated || descriptor.fallback || descriptor.key || '';
63
+ };
64
+
65
+ /**
66
+ * Build a minimal safe toolbar module list from a navigation preset.
67
+ * Only HomePage + ProfilePage are kept so fallback cannot expose unauthorized
68
+ * business routes (Clientes, Oportunidades, etc.).
69
+ */
70
+ const buildSafeToolbarFallbackMenus = (
71
+ preset,
72
+ menuType = 'toolbar',
73
+ translate,
74
+ presetKey = 'safe-toolbar-fallback',
75
+ ) => {
76
+ const items = Array.isArray(preset?.items) ? preset.items : [];
77
+ const safeItems = items.filter(item => SAFE_ROUTE_SET.has(item?.route));
78
+
79
+ if (safeItems.length === 0) {
80
+ return [];
81
+ }
82
+
83
+ const menus = safeItems.map((item, index) => ({
84
+ ...item,
85
+ menuType,
86
+ sortOrder: Number(item?.sortOrder || (index + 1) * 10),
87
+ label: resolvePresetLabel(translate, item?.label),
88
+ }));
89
+
90
+ return [
91
+ {
92
+ id: presetKey || 'safe-toolbar-fallback',
93
+ label: preset?.label || '',
94
+ icon: preset?.icon || '',
95
+ menus,
96
+ },
97
+ ];
98
+ };
99
+
100
+ /**
101
+ * Prefer API toolbar menus when present; otherwise safe preset fallback.
102
+ * Does not use the full business preset as a permission bypass.
103
+ */
104
+ const resolveToolbarRuntimeMenus = ({
105
+ apiMenus,
106
+ preset,
107
+ presetKey,
108
+ menuType = 'toolbar',
109
+ translate,
110
+ } = {}) => {
111
+ const filtered = filterMenusByType(apiMenus || [], menuType);
112
+ const hasAuthorizedItems = filtered.some(
113
+ module => Array.isArray(module?.menus) && module.menus.length > 0,
114
+ );
115
+
116
+ if (hasAuthorizedItems) {
117
+ return filtered;
118
+ }
119
+
120
+ if (preset) {
121
+ return buildSafeToolbarFallbackMenus(
122
+ preset,
123
+ menuType,
124
+ translate,
125
+ presetKey || 'safe-toolbar-fallback',
126
+ );
127
+ }
128
+
129
+ return [];
130
+ };
131
+
132
+ /**
133
+ * Flatten toolbar menus into bottom-nav item shape (route/icon/label/...).
134
+ */
135
+ const mapRuntimeMenusToNavItems = (runtimeMenus, menuType) =>
136
+ filterMenusByType(runtimeMenus || [], menuType)
137
+ .flatMap(module => (Array.isArray(module?.menus) ? module.menus : []))
138
+ .map(item => ({
139
+ route: item.route,
140
+ icon: item.icon || 'circle',
141
+ label: item.label,
142
+ menuKey: item.menuKey,
143
+ routeParams: item.routeParams,
144
+ menuType: item.menuType || menuType,
145
+ sortOrder: Number(item?.sortOrder || 0),
146
+ }))
147
+ .filter(item => Boolean(item.route))
148
+ .sort((left, right) => {
149
+ const orderDiff = left.sortOrder - right.sortOrder;
150
+ if (orderDiff !== 0) return orderDiff;
151
+ return String(left.label || left.menuKey || '').localeCompare(
152
+ String(right.label || right.menuKey || ''),
153
+ );
154
+ });
155
+
156
+ module.exports = {
157
+ SAFE_TOOLBAR_FALLBACK_ROUTES,
158
+ buildSafeToolbarFallbackMenus,
159
+ mapRuntimeMenusToNavItems,
160
+ resolveToolbarRuntimeMenus,
161
+ };
@@ -121,21 +121,33 @@ export const fetchOrderProducts = async orderRef => {
121
121
  return extractCollectionItems(response);
122
122
  };
123
123
 
124
- export const searchCompanyProducts = async ({ companyId, query = '' }) => {
124
+ export const searchCompanyProducts = async ({
125
+ companyId,
126
+ query = '',
127
+ categoryId = null,
128
+ }) => {
125
129
  if (!companyId) {
126
130
  return [];
127
131
  }
128
132
 
129
- const params = {
130
- company: toEntityIri(companyId, 'people'),
131
- active: 1,
132
- 'order[product]': 'ASC',
133
- };
133
+ const params = {
134
+ company: toEntityIri(companyId, 'people'),
135
+ active: 1,
136
+ 'order[product]': 'ASC',
137
+ };
134
138
 
135
139
  if (String(query || '').trim()) {
136
140
  params.product = String(query).trim();
137
141
  }
138
142
 
143
+ // ProductCategoryTreeFilter — includes the category and its descendants.
144
+ // Prefer server-side filter: product payloads do not always embed productCategory
145
+ // under the product:read serialization group.
146
+ const normalizedCategoryId = normalizeEntityId(categoryId);
147
+ if (normalizedCategoryId) {
148
+ params['productCategory.category'] = normalizedCategoryId;
149
+ }
150
+
139
151
  const response = await api.fetch('products', { params });
140
152
  return extractCollectionItems(response);
141
153
  };