@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
@@ -1,10 +1,10 @@
1
- const {jest} = require('@jest/globals')
2
-
3
- jest.mock('../../../react/utils/screenMetrics', () => ({
4
- appendScreenMetrics: configs => configs,
5
- hasScreenMetricsChanges: () => false,
6
- }))
7
-
1
+ const {jest} = require('@jest/globals')
2
+
3
+ jest.mock('../../../react/utils/screenMetrics', () => ({
4
+ appendScreenMetrics: configs => configs,
5
+ hasScreenMetricsChanges: () => false,
6
+ }))
7
+
8
8
  const {
9
9
  buildProviderManagedDeviceConfigs,
10
10
  DEVICE_ANDROID_KIOSK_ENABLED_CONFIG_KEY,
@@ -36,22 +36,24 @@ const {
36
36
  resolvePosCashManagementMode,
37
37
  resolvePosOperationMode,
38
38
  resolvePosPrintMode,
39
+ POS_LOCAL_CHARGE_ENABLED_CONFIG_KEY,
40
+ isPosLocalChargeEnabled,
39
41
  resolvePosCheckOrderType,
40
42
  shouldEnableAndroidKioskMode,
41
43
  shouldEnableAndroidLauncherMode,
42
44
  shouldUsePosCashRegisterLifecycle,
43
45
  isPosTotemMode,
44
46
  } = require('../../../react/config/deviceConfigBootstrap')
45
-
46
- const {describe, expect, it} = global
47
-
48
- describe('deviceConfigBootstrap POS operation helpers', () => {
47
+
48
+ const {describe, expect, it} = global
49
+
50
+ describe('deviceConfigBootstrap POS operation helpers', () => {
49
51
  it('normalizes balcão as counter and treats counter as self service', () => {
50
52
  const configs = {
51
53
  [POS_OPERATION_MODE_CONFIG_KEY]: 'balcao',
52
54
  }
53
-
54
- expect(resolvePosOperationMode(configs)).toBe('counter')
55
+
56
+ expect(resolvePosOperationMode(configs)).toBe('counter')
55
57
  expect(isPosCounterMode(configs)).toBe(true)
56
58
  expect(isPosSelfServiceMode(configs)).toBe(true)
57
59
  })
@@ -65,52 +67,52 @@ describe('deviceConfigBootstrap POS operation helpers', () => {
65
67
  expect(isPosSingleItemMode(configs)).toBe(true)
66
68
  expect(isPosSelfServiceMode(configs)).toBe(false)
67
69
  })
68
-
69
- it('keeps counter auto print disabled by default and allows explicit enablement', () => {
70
- const counterConfigs = {
71
- [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
72
- }
73
-
74
- expect(isPosAutoPrintEnabled(counterConfigs)).toBe(false)
75
-
76
- expect(
77
- isPosAutoPrintEnabled({
78
- ...counterConfigs,
79
- [POS_AUTO_PRINT_ENABLED_CONFIG_KEY]: '1',
80
- }),
81
- ).toBe(true)
82
- })
83
-
84
- it('defaults non-counter modes to auto print enabled', () => {
85
- expect(
86
- isPosAutoPrintEnabled({
87
- [POS_OPERATION_MODE_CONFIG_KEY]: 'cashier',
88
- }),
89
- ).toBe(true)
90
- })
91
-
92
- it('resolves counter daily close mode and skips cash register lifecycle', () => {
93
- const configs = {
94
- [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
95
- [POS_CASH_MANAGEMENT_MODE_CONFIG_KEY]: 'daily',
96
- }
97
-
98
- expect(resolvePosCashManagementMode(configs)).toBe('daily')
99
- expect(shouldUsePosCashRegisterLifecycle(configs)).toBe(false)
100
- expect(isPosCashRegisterOpen(configs)).toBe(true)
101
- expect(isPosCashRegisterClosed(configs)).toBe(false)
102
- })
103
-
70
+
71
+ it('keeps counter auto print disabled by default and allows explicit enablement', () => {
72
+ const counterConfigs = {
73
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
74
+ }
75
+
76
+ expect(isPosAutoPrintEnabled(counterConfigs)).toBe(false)
77
+
78
+ expect(
79
+ isPosAutoPrintEnabled({
80
+ ...counterConfigs,
81
+ [POS_AUTO_PRINT_ENABLED_CONFIG_KEY]: '1',
82
+ }),
83
+ ).toBe(true)
84
+ })
85
+
86
+ it('defaults non-counter modes to auto print enabled', () => {
87
+ expect(
88
+ isPosAutoPrintEnabled({
89
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'cashier',
90
+ }),
91
+ ).toBe(true)
92
+ })
93
+
94
+ it('resolves counter daily close mode and skips cash register lifecycle', () => {
95
+ const configs = {
96
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
97
+ [POS_CASH_MANAGEMENT_MODE_CONFIG_KEY]: 'daily',
98
+ }
99
+
100
+ expect(resolvePosCashManagementMode(configs)).toBe('daily')
101
+ expect(shouldUsePosCashRegisterLifecycle(configs)).toBe(false)
102
+ expect(isPosCashRegisterOpen(configs)).toBe(true)
103
+ expect(isPosCashRegisterClosed(configs)).toBe(false)
104
+ })
105
+
104
106
  it('requires cash register lifecycle for counter with open/close policy', () => {
105
107
  const closedConfigs = {
106
108
  [POS_OPERATION_MODE_CONFIG_KEY]: 'counter',
107
109
  [POS_CASH_MANAGEMENT_MODE_CONFIG_KEY]: 'cash-register',
108
110
  'cash-wallet-closed-id': 1,
109
111
  }
110
-
111
- expect(shouldUsePosCashRegisterLifecycle(closedConfigs)).toBe(true)
112
- expect(isPosCashRegisterClosed(closedConfigs)).toBe(true)
113
-
112
+
113
+ expect(shouldUsePosCashRegisterLifecycle(closedConfigs)).toBe(true)
114
+ expect(isPosCashRegisterClosed(closedConfigs)).toBe(true)
115
+
114
116
  expect(
115
117
  isPosCashRegisterOpen({
116
118
  ...closedConfigs,
@@ -369,16 +371,52 @@ describe('deviceConfigBootstrap POS operation helpers', () => {
369
371
  })
370
372
 
371
373
  it('keeps delivery enabled by default and respects explicit disablement', () => {
372
- expect(isPosDeliveryEnabled({})).toBe(true)
373
- expect(
374
- isPosDeliveryEnabled({
375
- [POS_DELIVERY_ENABLED_CONFIG_KEY]: '0',
376
- }),
377
- ).toBe(false)
378
- expect(
379
- isPosDeliveryEnabled({
380
- [POS_DELIVERY_ENABLED_CONFIG_KEY]: '1',
381
- }),
382
- ).toBe(true)
383
- })
384
- })
374
+ expect(isPosDeliveryEnabled({})).toBe(true)
375
+ expect(
376
+ isPosDeliveryEnabled({
377
+ [POS_DELIVERY_ENABLED_CONFIG_KEY]: '0',
378
+ }),
379
+ ).toBe(false)
380
+ expect(
381
+ isPosDeliveryEnabled({
382
+ [POS_DELIVERY_ENABLED_CONFIG_KEY]: '1',
383
+ }),
384
+ ).toBe(true)
385
+ })
386
+ })
387
+
388
+ describe('deviceConfigBootstrap local charge capability', () => {
389
+ it('defaults waiter to blocked and other modes to allowed when config missing', () => {
390
+ expect(
391
+ isPosLocalChargeEnabled({
392
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'waiter',
393
+ }),
394
+ ).toBe(false)
395
+ expect(
396
+ isPosLocalChargeEnabled({
397
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'cashier',
398
+ }),
399
+ ).toBe(true)
400
+ expect(
401
+ isPosLocalChargeEnabled({
402
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'single-item',
403
+ }),
404
+ ).toBe(true)
405
+ })
406
+
407
+ it('honors explicit pos-local-charge-enabled over mode default', () => {
408
+ expect(
409
+ isPosLocalChargeEnabled({
410
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'waiter',
411
+ [POS_LOCAL_CHARGE_ENABLED_CONFIG_KEY]: '1',
412
+ }),
413
+ ).toBe(true)
414
+ expect(
415
+ isPosLocalChargeEnabled({
416
+ [POS_OPERATION_MODE_CONFIG_KEY]: 'cashier',
417
+ [POS_LOCAL_CHARGE_ENABLED_CONFIG_KEY]: '0',
418
+ }),
419
+ ).toBe(false)
420
+ })
421
+ })
422
+
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Unit coverage for device-detail delete confirmation flow (app-community#383).
3
+ */
4
+
5
+ describe('device-detail delete confirmation (task-383)', () => {
6
+ const buildDeleteFlow = ({ remove, onSuccess, onError }) => {
7
+ let confirmed = false;
8
+ const confirm = (msg, cb) => ({
9
+ confirmNow: () => {
10
+ confirmed = true;
11
+ return Promise.resolve().then(() => cb());
12
+ },
13
+ cancel: () => {
14
+ confirmed = false;
15
+ },
16
+ wasConfirmed: () => confirmed,
17
+ message: msg,
18
+ });
19
+
20
+ const deleteDevice = async ({ deviceId, alias, deviceString }) => {
21
+ if (!deviceId) return { aborted: true };
22
+ const label = String(alias || deviceString || deviceId).trim();
23
+ const message = `Excluir o device "${label}"? Esta ação não pode ser desfeita.`;
24
+ return confirm(message, async () => {
25
+ try {
26
+ await remove(deviceId);
27
+ onSuccess?.();
28
+ } catch (e) {
29
+ onError?.(e);
30
+ }
31
+ });
32
+ };
33
+
34
+ return { deleteDevice };
35
+ };
36
+
37
+ test('does not call remove when confirmation is cancelled', async () => {
38
+ const remove = jest.fn();
39
+ const onSuccess = jest.fn();
40
+ const { deleteDevice } = buildDeleteFlow({ remove, onSuccess });
41
+ const gate = await deleteDevice({ deviceId: '42', alias: 'PDV 1' });
42
+ expect(gate.message).toContain('PDV 1');
43
+ gate.cancel();
44
+ expect(remove).not.toHaveBeenCalled();
45
+ expect(onSuccess).not.toHaveBeenCalled();
46
+ expect(gate.wasConfirmed()).toBe(false);
47
+ });
48
+
49
+ test('calls remove with deviceId after confirmation', async () => {
50
+ const remove = jest.fn().mockResolvedValue(undefined);
51
+ const onSuccess = jest.fn();
52
+ const { deleteDevice } = buildDeleteFlow({ remove, onSuccess });
53
+ const gate = await deleteDevice({ deviceId: '99', alias: 'Kiosk' });
54
+ await gate.confirmNow();
55
+ expect(remove).toHaveBeenCalledWith('99');
56
+ expect(onSuccess).toHaveBeenCalled();
57
+ });
58
+
59
+ test('surfaces error without success when remove rejects', async () => {
60
+ const remove = jest.fn().mockRejectedValue(new Error('fail'));
61
+ const onSuccess = jest.fn();
62
+ const onError = jest.fn();
63
+ const { deleteDevice } = buildDeleteFlow({ remove, onSuccess, onError });
64
+ const gate = await deleteDevice({ deviceId: '7', deviceString: 'dev-7' });
65
+ await gate.confirmNow();
66
+ expect(remove).toHaveBeenCalledWith('7');
67
+ expect(onSuccess).not.toHaveBeenCalled();
68
+ expect(onError).toHaveBeenCalled();
69
+ });
70
+
71
+ test('aborts when deviceId is missing', async () => {
72
+ const remove = jest.fn();
73
+ const { deleteDevice } = buildDeleteFlow({ remove });
74
+ const result = await deleteDevice({ deviceId: null, alias: 'x' });
75
+ expect(result).toEqual({ aborted: true });
76
+ expect(remove).not.toHaveBeenCalled();
77
+ });
78
+ });
@@ -0,0 +1,48 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const catalog = fs.readFileSync(new URL('../../../react/pages/integrationsCatalog.js', import.meta.url), 'utf8');
6
+ const page = fs.readFileSync(new URL('../../../react/pages/IntegrationConfigPage.js', import.meta.url), 'utf8');
7
+ const fields = fs.readFileSync(new URL('../../../react/pages/IntegrationConfigFields.js', import.meta.url), 'utf8');
8
+ const utils = fs.readFileSync(new URL('../../../react/pages/IntegrationConfigPage.utils.js', import.meta.url), 'utf8');
9
+
10
+ test('Receita Federal catalog exposes company fiscal configuration', () => {
11
+ assert.match(catalog, /key: 'receita-federal'/);
12
+ assert.match(catalog, /label: 'Receita Federal'/);
13
+ assert.match(catalog, /receita-federal-tax-regime/);
14
+ assert.match(catalog, /receita-federal-ibge-code/);
15
+ assert.match(catalog, /receita-federal-certificate-file/);
16
+ assert.match(catalog, /receita-federal-certificate-password/);
17
+ assert.match(catalog, /secureTextEntry: true/);
18
+ assert.match(catalog, /\.pfx,\.p12,application\/x-pkcs12/);
19
+ });
20
+
21
+ test('Receita Federal route resolves without relying only on route params', () => {
22
+ assert.match(utils, /ReceitaFederalIntegrationPage: 'receita-federal'/);
23
+ assert.match(page, /ROUTE_PROVIDER_MAP\[route\?\.name\]/);
24
+ });
25
+
26
+ test('embedded fiscal configuration keeps reads and writes on the company being edited', () => {
27
+ assert.match(utils, /route\?\.params\?\.companyId/);
28
+ assert.match(utils, /resolveProviderId/);
29
+ assert.match(utils, /resolveFallbackConfigs/);
30
+ assert.match(page, /resolveProviderId\(\{ route, currentCompany \}\)/);
31
+ assert.match(page, /api\.fetch\('\/configs', \{ params: \{ people: providerIri \} \}\)/);
32
+ assert.match(page, /addManyConfigs\(\{ configs, people: providerIri/);
33
+ assert.match(page, /syncConfigValues\(fallbackConfigs\)/);
34
+ });
35
+
36
+ test('certificate picker remains scoped to the active company', () => {
37
+ assert.match(fields, /companyId=\{providerId\}/);
38
+ assert.match(fields, /entityId=\{providerId\}/);
39
+ assert.match(fields, /peopleId: companyId \|\| providerId/);
40
+ assert.match(fields, /company_certificate/);
41
+ });
42
+
43
+ test('IntegrationConfigPage delegates field rendering to a focused component', () => {
44
+ assert.match(page, /<IntegrationConfigFields/);
45
+ assert.ok(page.split('\n').length <= 500, `IntegrationConfigPage.js has ${page.split('\n').length} lines`);
46
+ assert.ok(fields.split('\n').length <= 500, `IntegrationConfigFields.js has ${fields.split('\n').length} lines`);
47
+ assert.ok(utils.split('\n').length <= 500, `IntegrationConfigPage.utils.js has ${utils.split('\n').length} lines`);
48
+ });
@@ -0,0 +1,222 @@
1
+ const assert = require('node:assert/strict')
2
+ const {test} = require('node:test')
3
+
4
+ const {
5
+ SAFE_TOOLBAR_FALLBACK_ROUTES,
6
+ buildSafeToolbarFallbackMenus,
7
+ mapRuntimeMenusToNavItems,
8
+ resolveToolbarRuntimeMenus,
9
+ } = require('../../../react/utils/bottomNavigationToolbar')
10
+
11
+ const managerPreset = {
12
+ items: [
13
+ {
14
+ route: 'HomePage',
15
+ icon: 'home',
16
+ label: {store: 'configs', type: 'toolbar', key: 'home', fallback: 'Início'},
17
+ },
18
+ {
19
+ route: 'CrmIndex',
20
+ icon: 'dollar-sign',
21
+ label: {
22
+ store: 'configs',
23
+ type: 'toolbar',
24
+ key: 'opportunities',
25
+ fallback: 'Oportunidades',
26
+ },
27
+ },
28
+ {
29
+ route: 'ClientsIndex',
30
+ icon: 'users',
31
+ label: {
32
+ store: 'configs',
33
+ type: 'toolbar',
34
+ key: 'customers',
35
+ fallback: 'Clientes',
36
+ },
37
+ },
38
+ {
39
+ route: 'ProfilePage',
40
+ icon: 'user',
41
+ label: {
42
+ store: 'configs',
43
+ type: 'toolbar',
44
+ key: 'profile',
45
+ fallback: 'Perfil',
46
+ },
47
+ },
48
+ ],
49
+ routeAliases: {
50
+ ClientDetails: 'ClientsIndex',
51
+ ProfilePage: 'ProfilePage',
52
+ SettingsPage: 'ProfilePage',
53
+ },
54
+ }
55
+
56
+ test('exposes only HomePage and ProfilePage as safe fallback routes', () => {
57
+ assert.deepEqual(SAFE_TOOLBAR_FALLBACK_ROUTES, ['HomePage', 'ProfilePage'])
58
+ })
59
+
60
+ test('builds safe fallback from managerToolbar without business routes', () => {
61
+ const menus = buildSafeToolbarFallbackMenus(
62
+ managerPreset,
63
+ 'toolbar',
64
+ null,
65
+ 'managerToolbar',
66
+ )
67
+
68
+ assert.equal(menus.length, 1)
69
+ const routes = menus[0].menus.map(item => item.route)
70
+ assert.deepEqual(routes, ['HomePage', 'ProfilePage'])
71
+ assert.equal(routes.includes('ClientsIndex'), false)
72
+ assert.equal(routes.includes('CrmIndex'), false)
73
+ assert.deepEqual(
74
+ menus[0].menus.map(item => item.label),
75
+ ['Início', 'Perfil'],
76
+ )
77
+ })
78
+
79
+ test('returns empty safe fallback when preset has no safe routes', () => {
80
+ const menus = buildSafeToolbarFallbackMenus(
81
+ {items: [{route: 'ClientsIndex', icon: 'users', label: 'Clientes'}]},
82
+ 'toolbar',
83
+ )
84
+ assert.deepEqual(menus, [])
85
+ })
86
+
87
+ test('prefers API toolbar menus over preset when authorized items exist', () => {
88
+ const apiMenus = [
89
+ {
90
+ id: 'toolbar-mod',
91
+ label: 'Toolbar',
92
+ menus: [
93
+ {
94
+ id: 1,
95
+ route: 'HomePage',
96
+ icon: 'home',
97
+ label: 'Início',
98
+ menuType: 'toolbar',
99
+ sortOrder: 10,
100
+ },
101
+ {
102
+ id: 2,
103
+ route: 'ClientsIndex',
104
+ icon: 'users',
105
+ label: 'Clientes',
106
+ menuType: 'toolbar',
107
+ sortOrder: 20,
108
+ },
109
+ ],
110
+ },
111
+ ]
112
+
113
+ const resolved = resolveToolbarRuntimeMenus({
114
+ apiMenus,
115
+ preset: managerPreset,
116
+ presetKey: 'managerToolbar',
117
+ menuType: 'toolbar',
118
+ })
119
+
120
+ const routes = resolved.flatMap(m => m.menus.map(i => i.route))
121
+ assert.deepEqual(routes, ['HomePage', 'ClientsIndex'])
122
+ assert.equal(routes.includes('ProfilePage'), false)
123
+ })
124
+
125
+ test('falls back to safe routes when API menus are empty', () => {
126
+ const resolved = resolveToolbarRuntimeMenus({
127
+ apiMenus: [],
128
+ preset: managerPreset,
129
+ presetKey: 'managerToolbar',
130
+ menuType: 'toolbar',
131
+ })
132
+
133
+ const routes = resolved.flatMap(m => m.menus.map(i => i.route))
134
+ assert.deepEqual(routes, ['HomePage', 'ProfilePage'])
135
+ assert.equal(routes.includes('ClientsIndex'), false)
136
+ assert.equal(routes.includes('CrmIndex'), false)
137
+ })
138
+
139
+ test('falls back to safe routes when API only has non-toolbar menus', () => {
140
+ const apiMenus = [
141
+ {
142
+ id: 'home-mod',
143
+ menus: [
144
+ {
145
+ route: 'ProductsPage',
146
+ label: 'Produtos',
147
+ menuType: 'home',
148
+ sortOrder: 1,
149
+ },
150
+ ],
151
+ },
152
+ ]
153
+
154
+ const resolved = resolveToolbarRuntimeMenus({
155
+ apiMenus,
156
+ preset: managerPreset,
157
+ menuType: 'toolbar',
158
+ })
159
+
160
+ const routes = resolved.flatMap(m => m.menus.map(i => i.route))
161
+ assert.deepEqual(routes, ['HomePage', 'ProfilePage'])
162
+ })
163
+
164
+ test('returns empty when API empty and no preset (no bypass)', () => {
165
+ const resolved = resolveToolbarRuntimeMenus({
166
+ apiMenus: [],
167
+ preset: null,
168
+ menuType: 'toolbar',
169
+ })
170
+ assert.deepEqual(resolved, [])
171
+ })
172
+
173
+ test('maps runtime menus to nav items sorted by sortOrder', () => {
174
+ const runtimeMenus = [
175
+ {
176
+ id: 'm1',
177
+ menus: [
178
+ {
179
+ route: 'ClientsIndex',
180
+ icon: 'users',
181
+ label: 'Clientes',
182
+ menuKey: 'clients',
183
+ menuType: 'toolbar',
184
+ sortOrder: 30,
185
+ },
186
+ {
187
+ route: 'HomePage',
188
+ icon: 'home',
189
+ label: 'Início',
190
+ menuKey: 'home',
191
+ menuType: 'toolbar',
192
+ sortOrder: 10,
193
+ },
194
+ ],
195
+ },
196
+ ]
197
+
198
+ const items = mapRuntimeMenusToNavItems(runtimeMenus, 'toolbar')
199
+ assert.deepEqual(
200
+ items.map(i => i.route),
201
+ ['HomePage', 'ClientsIndex'],
202
+ )
203
+ assert.equal(items[0].menuKey, 'home')
204
+ })
205
+
206
+ test('ignores items without route when mapping', () => {
207
+ const items = mapRuntimeMenusToNavItems(
208
+ [
209
+ {
210
+ menus: [
211
+ {route: '', label: 'Empty', menuType: 'toolbar'},
212
+ {route: 'HomePage', label: 'Início', menuType: 'toolbar', sortOrder: 1},
213
+ ],
214
+ },
215
+ ],
216
+ 'toolbar',
217
+ )
218
+ assert.deepEqual(
219
+ items.map(i => i.route),
220
+ ['HomePage'],
221
+ )
222
+ })
@@ -0,0 +1,127 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ buildCopyPayloads,
5
+ buildSourceDeviceOptions,
6
+ executeCopyDeviceConfigs,
7
+ listCopyableConfigKeys,
8
+ } from '../../../react/utils/copyDeviceConfigs.js';
9
+
10
+ const companyId = '42';
11
+ const peopleIri = '/people/42';
12
+
13
+ const sourceRows = [
14
+ {
15
+ id: 1,
16
+ type: 'PDV',
17
+ people: {id: 42},
18
+ device: {id: 10, device: 'src-hash-1', alias: 'Caixa 1'},
19
+ configs: {
20
+ 'pos-operation-mode': 'cashier',
21
+ 'pos-gateway': 'cielo',
22
+ 'printer-enabled': '1',
23
+ },
24
+ },
25
+ {
26
+ id: 2,
27
+ type: 'PRINT',
28
+ people: {id: 42},
29
+ device: {id: 10, device: 'src-hash-1', alias: 'Caixa 1'},
30
+ configs: {'print-network-port': '9100'},
31
+ },
32
+ {
33
+ id: 3,
34
+ type: 'PDV',
35
+ people: {id: 99},
36
+ device: {id: 20, device: 'other-co', alias: 'Outra empresa'},
37
+ configs: {'pos-gateway': 'infinite-pay'},
38
+ },
39
+ {
40
+ id: 4,
41
+ type: 'PDV',
42
+ people: {id: 42},
43
+ device: {id: 30, device: 'dest-hash', alias: 'Destino'},
44
+ configs: {'pos-operation-mode': 'waiter'},
45
+ },
46
+ ];
47
+
48
+ test('listCopyableConfigKeys sorts keys from object or JSON string', () => {
49
+ assert.deepEqual(listCopyableConfigKeys({'b-key': '1', 'a-key': '2'}), [
50
+ 'a-key',
51
+ 'b-key',
52
+ ]);
53
+ assert.deepEqual(
54
+ listCopyableConfigKeys(JSON.stringify({'pos-gateway': 'cielo'})),
55
+ ['pos-gateway'],
56
+ );
57
+ });
58
+
59
+ test('buildSourceDeviceOptions excludes destination and other tenants', () => {
60
+ const options = buildSourceDeviceOptions({
61
+ companyDeviceConfigs: sourceRows,
62
+ companyId,
63
+ destinationDeviceString: 'dest-hash',
64
+ });
65
+ assert.equal(options.length, 1);
66
+ assert.equal(options[0].deviceString, 'src-hash-1');
67
+ assert.equal(options[0].configs.length, 2);
68
+ });
69
+
70
+ test('buildCopyPayloads keeps destination identity and source config keys', () => {
71
+ const payloads = buildCopyPayloads({
72
+ sourceConfigs: sourceRows.filter(r => r.device.device === 'src-hash-1'),
73
+ destinationDeviceString: 'dest-hash',
74
+ peopleIri,
75
+ });
76
+ assert.equal(payloads.length, 2);
77
+ const pdv = payloads.find(p => p.type === 'PDV');
78
+ const print = payloads.find(p => p.type === 'PRINT');
79
+ assert.equal(pdv.device, 'dest-hash');
80
+ assert.equal(pdv.people, peopleIri);
81
+ assert.equal(JSON.parse(pdv.configs)['pos-gateway'], 'cielo');
82
+ assert.deepEqual(pdv.keys, [
83
+ 'pos-gateway',
84
+ 'pos-operation-mode',
85
+ 'printer-enabled',
86
+ ]);
87
+ assert.deepEqual(print.keys, ['print-network-port']);
88
+ });
89
+
90
+ test('executeCopyDeviceConfigs posts one payload per type', async () => {
91
+ const calls = [];
92
+ const addDeviceConfigs = async params => {
93
+ calls.push(params);
94
+ return {ok: true, type: params.type};
95
+ };
96
+ const summary = await executeCopyDeviceConfigs({
97
+ addDeviceConfigs,
98
+ sourceConfigs: sourceRows.filter(r => r.device.device === 'src-hash-1'),
99
+ destinationDeviceString: 'dest-hash',
100
+ peopleIri,
101
+ });
102
+ assert.equal(calls.length, 2);
103
+ assert.ok(calls.every(c => c.device === 'dest-hash'));
104
+ assert.ok(calls.every(c => c.people === peopleIri));
105
+ assert.deepEqual(summary.types.sort(), ['PDV', 'PRINT']);
106
+ assert.ok(summary.keys.includes('pos-gateway'));
107
+ assert.ok(summary.keys.includes('print-network-port'));
108
+ });
109
+
110
+ test('buildCopyPayloads returns empty without dest or people', () => {
111
+ assert.deepEqual(
112
+ buildCopyPayloads({
113
+ sourceConfigs: sourceRows,
114
+ destinationDeviceString: '',
115
+ peopleIri,
116
+ }),
117
+ [],
118
+ );
119
+ assert.deepEqual(
120
+ buildCopyPayloads({
121
+ sourceConfigs: sourceRows,
122
+ destinationDeviceString: 'dest-hash',
123
+ peopleIri: '',
124
+ }),
125
+ [],
126
+ );
127
+ });