@controleonline/ui-common 1.2.80 → 1.2.82

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 (90) 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 +44 -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 +495 -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 +489 -0
  43. package/src/react/pages/Devices/detail/useDeviceDetailStateB.js +98 -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/paymentDevices.js +6 -0
  62. package/src/react/utils/printManagedPrinter.js +45 -0
  63. package/src/react/utils/printRouting.js +63 -0
  64. package/src/react/utils/printSpoolUtils.js +119 -0
  65. package/src/react/utils/printerDevices.js +368 -368
  66. package/src/react/utils/runtimeFooter.js +10 -19
  67. package/src/react/utils/shopFranchises.js +186 -65
  68. package/src/react/utils/systemErrorMessage.js +71 -67
  69. package/src/store/device_config/customActions.js +119 -30
  70. package/src/tests/browser/manager/bottom-navigation-role-toolbar.spec.js +353 -0
  71. package/src/tests/browser/manager/device-detail-alias-save.spec.js +345 -0
  72. package/src/tests/browser/manager/device-detail-copy-config.spec.js +365 -0
  73. package/src/tests/browser/manager/runtime-footer-marquee.spec.js +262 -0
  74. package/src/tests/browser/manager/theme-confirm-toast.spec.js +236 -0
  75. package/src/tests/react/components/RuntimeFooterMarqueeText.test.js +171 -0
  76. package/src/tests/react/components/UserAvatar.test.js +6 -2
  77. package/src/tests/react/components/themeAwareModals.test.mjs +60 -0
  78. package/src/tests/react/config/deviceConfigBootstrap.test.js +104 -66
  79. package/src/tests/react/pages/deviceDetailDelete.test.js +78 -0
  80. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +48 -0
  81. package/src/tests/react/utils/bottomNavigationToolbar.test.js +222 -0
  82. package/src/tests/react/utils/copyDeviceConfigs.test.mjs +127 -0
  83. package/src/tests/react/utils/fileUrl.test.js +38 -0
  84. package/src/tests/react/utils/importStatus.test.js +26 -9
  85. package/src/tests/react/utils/passwordPolicy.test.js +27 -0
  86. package/src/tests/react/utils/printRouting.test.mjs +86 -0
  87. package/src/tests/react/utils/printSpoolUtils.test.mjs +52 -0
  88. package/src/tests/react/utils/runtimeFooter.test.js +7 -0
  89. package/src/tests/react/utils/shopFranchises.test.js +121 -115
  90. package/src/tests/react/utils/systemErrorMessage.test.js +32 -59
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Copy device_config.configs from a source physical device to a destination
3
+ * physical device without changing destination identity (id/hash/hardware).
4
+ *
5
+ * Copyable keys = free-form keys present in the source `configs` JSON
6
+ * (discovered at runtime). Entity fields (id, device, people, type) are never
7
+ * copied.
8
+ *
9
+ * Known keys observed in the codebase (non-exhaustive; actual copy uses
10
+ * whatever keys exist on the source row):
11
+ * - pos-operation-mode, pos-gateway, printer-enabled, payment-type-ids
12
+ * - pos-product-showcase-id, pos-delivery-enabled, pos-auto-print-enabled
13
+ * - pos-cash-management-mode, check-order-type, check-order-management-mode
14
+ * - android-kiosk-enabled, android-launcher-enabled
15
+ * - order-payment-device, order-payment-devices, order-payment-device-change-allowed
16
+ * - display-id, printer, display-auto-print-product, display-allow-printer-change
17
+ * - display-size, display-side-break, print-mode
18
+ * - print-network-port, print-network-columns, print-network-code-page
19
+ * - camera-network-*, notification-sound-*, pos-order-visibility
20
+ * - device-runtime-debug-info-enabled, device-runtime-footer-text
21
+ */
22
+
23
+ const normalizeDeviceId = value => String(value || '').trim();
24
+
25
+ const normalizeEntityId = value =>
26
+ String(value?.id || value || '')
27
+ .replace(/\D/g, '')
28
+ .trim();
29
+
30
+ const parseConfigsObject = configs => {
31
+ if (!configs) {
32
+ return {};
33
+ }
34
+ if (typeof configs === 'string') {
35
+ try {
36
+ const parsed = JSON.parse(configs);
37
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
38
+ ? parsed
39
+ : {};
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+ if (typeof configs === 'object' && !Array.isArray(configs)) {
45
+ return configs;
46
+ }
47
+ return {};
48
+ };
49
+
50
+ export const listCopyableConfigKeys = configs =>
51
+ Object.keys(parseConfigsObject(configs) || {}).sort();
52
+
53
+ export const getDeviceConfigDeviceString = deviceConfig =>
54
+ normalizeDeviceId(
55
+ deviceConfig?.device?.device ||
56
+ deviceConfig?.device?.id ||
57
+ deviceConfig?.device ||
58
+ '',
59
+ );
60
+
61
+ export const getDeviceConfigType = deviceConfig =>
62
+ String(deviceConfig?.type || deviceConfig?.device?.type || '')
63
+ .trim()
64
+ .toUpperCase();
65
+
66
+ export const getDeviceConfigLabel = deviceConfig => {
67
+ const alias = String(
68
+ deviceConfig?.device?.alias ||
69
+ deviceConfig?.alias ||
70
+ getDeviceConfigDeviceString(deviceConfig) ||
71
+ '',
72
+ ).trim();
73
+ const type = getDeviceConfigType(deviceConfig);
74
+ if (alias && type) {
75
+ return `${alias} (${type})`;
76
+ }
77
+ return alias || type || 'Device';
78
+ };
79
+
80
+ const filterByCompany = (deviceConfigs, companyId) => {
81
+ const normalizedCompanyId = normalizeEntityId(companyId);
82
+ return (Array.isArray(deviceConfigs) ? deviceConfigs : []).filter(
83
+ deviceConfig =>
84
+ !normalizedCompanyId ||
85
+ normalizeEntityId(deviceConfig?.people?.id || deviceConfig?.people) ===
86
+ normalizedCompanyId,
87
+ );
88
+ };
89
+
90
+ /**
91
+ * Group company device_config rows by physical device string, excluding the
92
+ * destination device. Used to build the "Copiar de…" picker.
93
+ */
94
+ export const buildSourceDeviceOptions = ({
95
+ companyDeviceConfigs = [],
96
+ companyId,
97
+ destinationDeviceString,
98
+ } = {}) => {
99
+ const dest = normalizeDeviceId(destinationDeviceString);
100
+ const scoped = filterByCompany(companyDeviceConfigs, companyId);
101
+ const byDevice = new Map();
102
+
103
+ scoped.forEach(row => {
104
+ const deviceString = getDeviceConfigDeviceString(row);
105
+ if (!deviceString || (dest && deviceString === dest)) {
106
+ return;
107
+ }
108
+ if (!byDevice.has(deviceString)) {
109
+ byDevice.set(deviceString, {
110
+ deviceString,
111
+ alias: String(row?.device?.alias || deviceString).trim(),
112
+ configs: [],
113
+ });
114
+ }
115
+ byDevice.get(deviceString).configs.push(row);
116
+ });
117
+
118
+ return Array.from(byDevice.values()).sort((a, b) =>
119
+ String(a.alias).localeCompare(String(b.alias), 'pt-BR'),
120
+ );
121
+ };
122
+
123
+ /**
124
+ * Build POST payloads for addDeviceConfigs: one per type present on source.
125
+ */
126
+ export const buildCopyPayloads = ({
127
+ sourceConfigs = [],
128
+ destinationDeviceString,
129
+ peopleIri,
130
+ } = {}) => {
131
+ const dest = normalizeDeviceId(destinationDeviceString);
132
+ const people = String(peopleIri || '').trim();
133
+ if (!dest || !people) {
134
+ return [];
135
+ }
136
+
137
+ const byType = new Map();
138
+ (Array.isArray(sourceConfigs) ? sourceConfigs : []).forEach(row => {
139
+ const type = getDeviceConfigType(row);
140
+ if (!type) {
141
+ return;
142
+ }
143
+ const configs = parseConfigsObject(row?.configs);
144
+ byType.set(type, {
145
+ device: dest,
146
+ people,
147
+ type,
148
+ configs: JSON.stringify(configs || {}),
149
+ keys: listCopyableConfigKeys(configs),
150
+ });
151
+ });
152
+
153
+ return Array.from(byType.values());
154
+ };
155
+
156
+ /**
157
+ * Execute copy: for each payload, call addDeviceConfigs.
158
+ */
159
+ export const executeCopyDeviceConfigs = async ({
160
+ addDeviceConfigs,
161
+ sourceConfigs = [],
162
+ destinationDeviceString,
163
+ peopleIri,
164
+ } = {}) => {
165
+ if (typeof addDeviceConfigs !== 'function') {
166
+ throw new Error('addDeviceConfigs is required');
167
+ }
168
+
169
+ const payloads = buildCopyPayloads({
170
+ sourceConfigs,
171
+ destinationDeviceString,
172
+ peopleIri,
173
+ });
174
+
175
+ if (payloads.length === 0) {
176
+ return {types: [], keys: [], results: []};
177
+ }
178
+
179
+ const results = [];
180
+ const allKeys = new Set();
181
+
182
+ for (const payload of payloads) {
183
+ (payload.keys || []).forEach(key => allKeys.add(key));
184
+ const result = await addDeviceConfigs({
185
+ device: payload.device,
186
+ people: payload.people,
187
+ type: payload.type,
188
+ configs: payload.configs,
189
+ });
190
+ results.push({type: payload.type, keys: payload.keys, result});
191
+ }
192
+
193
+ return {
194
+ types: payloads.map(p => p.type),
195
+ keys: Array.from(allKeys).sort(),
196
+ results,
197
+ };
198
+ };
@@ -81,6 +81,44 @@ const resolveDownloadHost = ({company = null, appDomain = ''} = {}) => {
81
81
  return resolveCompanyDomain(company, fallbackDomain);
82
82
  };
83
83
 
84
+ /**
85
+ * Environment / platform hosts (staging, dev, localhost) are not tenant domains.
86
+ * Injecting them into /{host}/files/{id}/download produces URLs like
87
+ * https://s.controleonline.com/staging.controleonline.com/files/... → 403.
88
+ * Keep app-domain header only; do not path-inject environment hosts.
89
+ * app-community#432
90
+ */
91
+ const isEnvironmentLikeHost = host => {
92
+ const normalized = normalizeText(host).toLowerCase();
93
+ if (!normalized) return false;
94
+ if (
95
+ normalized === 'localhost' ||
96
+ normalized.startsWith('localhost:') ||
97
+ normalized === '127.0.0.1' ||
98
+ normalized.startsWith('127.0.0.1:')
99
+ ) {
100
+ return true;
101
+ }
102
+ // staging.*, dev.*, *.staging.*, etc.
103
+ if (
104
+ /^(staging|dev|local|test|qa)([.-]|$)/i.test(normalized) ||
105
+ /\.(staging|dev|local|test)\./i.test(normalized) ||
106
+ normalized.endsWith('.local')
107
+ ) {
108
+ return true;
109
+ }
110
+ // Explicit known platform hosts used as ENV DOMAIN, not company domains
111
+ if (
112
+ normalized === 'staging.controleonline.com' ||
113
+ normalized === 'dev.controleonline.com' ||
114
+ normalized === 'manager.controleonline.com' ||
115
+ normalized === 'pos.controleonline.com'
116
+ ) {
117
+ return true;
118
+ }
119
+ return false;
120
+ };
121
+
84
122
  const buildBackendDownloadUrl = (fileId, options = {}) => {
85
123
  const normalizedId = String(fileId || '').trim();
86
124
  if (!normalizedId) {
@@ -100,6 +138,11 @@ const buildTenantDownloadUrl = (url, host) => {
100
138
  return absoluteUrl;
101
139
  }
102
140
 
141
+ // Do not inject environment hosts into the path (app-community#432).
142
+ if (isEnvironmentLikeHost(normalizedHost)) {
143
+ return absoluteUrl;
144
+ }
145
+
103
146
  if (/^https?:\/\//i.test(absoluteUrl)) {
104
147
  const match = stripDomainQueryParams(absoluteUrl).match(
105
148
  /^(https?:\/\/[^/?#]+)\/(?:[^/]+\/)?files\/([^/?#]+)\/download([^#]*)(#.*)?$/i,
@@ -73,18 +73,26 @@ function getImportErrorDetail(item) {
73
73
  return candidates.find(candidate => String(candidate || '').trim()) || ''
74
74
  }
75
75
 
76
- function resolveImportLabels(translate) {
77
- return {
76
+ function resolveImportLabels(translate) {
77
+ return {
78
78
  refreshLabel: translate?.('imports', 'button', 'refresh'),
79
79
  processingLabel: translate?.('imports', 'status', 'processing'),
80
80
  errorLabel: translate?.('imports', 'status', 'error'),
81
81
  doneLabel: translate?.('imports', 'status', 'done'),
82
82
  noStatusLabel: translate?.('imports', 'status', 'no_status'),
83
83
  processingHelpLabel: translate?.('imports', 'message', 'processing_after_upload'),
84
- }
85
- }
86
-
87
- function resolveImportStatusLabel(statusMeta, labels = {}) {
84
+ }
85
+ }
86
+
87
+ function resolveImportTranslator(translator = globalThis?.t) {
88
+ if (typeof translator?.t !== 'function') {
89
+ return undefined
90
+ }
91
+
92
+ return translator.t.bind(translator)
93
+ }
94
+
95
+ function resolveImportStatusLabel(statusMeta, labels = {}) {
88
96
  if (statusMeta?.labelKey === 'processing') return labels.processingLabel
89
97
  if (statusMeta?.labelKey === 'error') return labels.errorLabel
90
98
  if (statusMeta?.labelKey === 'done') return labels.doneLabel
@@ -94,7 +102,8 @@ function resolveImportStatusLabel(statusMeta, labels = {}) {
94
102
  module.exports = {
95
103
  getImportErrorDetail,
96
104
  getImportStatusMeta,
97
- normalizeImportStatus,
98
- resolveImportLabels,
99
- resolveImportStatusLabel,
100
- }
105
+ normalizeImportStatus,
106
+ resolveImportLabels,
107
+ resolveImportTranslator,
108
+ resolveImportStatusLabel,
109
+ }
@@ -7,25 +7,25 @@ export const LOG_POLICY_ITEMS = [
7
7
  key: 'backend_error',
8
8
  title: 'Erros do backend',
9
9
  description:
10
- 'Registra excecoes e falhas HTTP do backend. E o grupo principal para erros 500.',
10
+ 'Registra exceções e falhas HTTP do backend. É o grupo principal para erros 500.',
11
11
  },
12
12
  {
13
13
  key: 'entity',
14
- title: 'Historico de entidades',
14
+ title: 'Histórico de entidades',
15
15
  description:
16
- 'Mantem os diffs de criacao, alteracao e remocao das entidades do sistema.',
16
+ 'Mantém os diffs de criação, alteração e remoção das entidades do sistema.',
17
17
  },
18
18
  {
19
19
  key: 'generic',
20
- title: 'Logs genericos',
20
+ title: 'Logs genéricos',
21
21
  description:
22
- 'Agrupa logs tecnicos de integracao, workers e eventos que nao sao timeline de entidade.',
22
+ 'Agrupa logs técnicos de integração, workers e eventos que não são timeline de entidade.',
23
23
  },
24
24
  {
25
25
  key: 'operation_patterns',
26
- title: 'Padroes operacionais',
26
+ title: 'Padrões operacionais',
27
27
  description:
28
- 'Mantem registros tecnicos ligados a padroes e diagnosticos operacionais do sistema.',
28
+ 'Mantém registros técnicos ligados a padrões e diagnósticos operacionais do sistema.',
29
29
  },
30
30
  {
31
31
  key: 'frontend_debug',
@@ -9,6 +9,22 @@ export const MAINTENANCE_ROUTINE_ITEMS = [
9
9
  defaultEnabled: true,
10
10
  defaultCronExpression: '* * * * *',
11
11
  },
12
+ {
13
+ key: 'cleanup_ephemeral_integrations',
14
+ title: 'Limpeza de integracoes efemeras',
15
+ description:
16
+ 'Remove Websocket e PushNotification abertos ha mais de 24 horas.',
17
+ defaultEnabled: false,
18
+ defaultCronExpression: '0 * * * *',
19
+ },
20
+ {
21
+ key: 'open_overdue_opportunities',
22
+ title: 'Oportunidades vencidas para aberto',
23
+ description:
24
+ 'Move oportunidades de CRM de pendente para aberto quando a data de retorno ja passou.',
25
+ defaultEnabled: true,
26
+ defaultCronExpression: '* * * * *',
27
+ },
12
28
  ];
13
29
 
14
30
  export const normalizeMaintenanceRoutines = value => {
@@ -2,9 +2,11 @@ import {Platform} from 'react-native';
2
2
 
3
3
  export const PAYMENT_GATEWAY_CIELO = 'cielo';
4
4
  export const PAYMENT_GATEWAY_INFINITE_PAY = 'infinite-pay';
5
+ export const PAYMENT_GATEWAY_GETNET = 'getnet';
5
6
  export const PAYMENT_GATEWAYS = [
6
7
  PAYMENT_GATEWAY_CIELO,
7
8
  PAYMENT_GATEWAY_INFINITE_PAY,
9
+ PAYMENT_GATEWAY_GETNET,
8
10
  ];
9
11
  export const POS_GATEWAY_CONFIG_KEY = 'pos-gateway';
10
12
  export const PDV_PRINTER_ENABLED_CONFIG_KEY = 'printer-enabled';
@@ -152,6 +154,9 @@ export const normalizePaymentGateway = value => {
152
154
  if (compactGateway === PAYMENT_GATEWAY_CIELO) {
153
155
  return PAYMENT_GATEWAY_CIELO;
154
156
  }
157
+ if (compactGateway === 'getnet' || compactGateway === PAYMENT_GATEWAY_GETNET) {
158
+ return PAYMENT_GATEWAY_GETNET;
159
+ }
155
160
 
156
161
  return compactGateway;
157
162
  };
@@ -309,6 +314,7 @@ export const getPaymentDeviceLabel = deviceConfig =>
309
314
  export const getPaymentGatewayLabel = gateway => {
310
315
  if (gateway === PAYMENT_GATEWAY_CIELO) return 'Cielo';
311
316
  if (gateway === PAYMENT_GATEWAY_INFINITE_PAY) return 'Infinite Pay';
317
+ if (gateway === PAYMENT_GATEWAY_GETNET) return 'Getnet';
312
318
  return 'Gateway nao configurado';
313
319
  };
314
320
 
@@ -0,0 +1,45 @@
1
+ import {
2
+ DEFAULT_NETWORK_PRINTER_PORT,
3
+ DEFAULT_NETWORK_PRINTER_CODE_PAGE,
4
+ getPrinterHost,
5
+ NETWORK_PRINTER_CODE_PAGE_CONFIG_KEY,
6
+ NETWORK_PRINTER_PORT_CONFIG_KEY,
7
+ normalizePrinterPort,
8
+ } from '@controleonline/ui-common/src/react/utils/printerDevices';
9
+ import {
10
+ decodeNetworkPrinterPayload,
11
+ printOnNetworkPrinter,
12
+ } from '@controleonline/ui-common/src/react/services/NetworkPrinterService';
13
+
14
+ /**
15
+ * Print spool content on a managed network printer when available.
16
+ * @returns {Promise<boolean>} true if managed printer handled the job
17
+ */
18
+ export const printOnManagedNetworkPrinter = async ({
19
+ managedPrinter,
20
+ content,
21
+ }) => {
22
+ if (!managedPrinter) {
23
+ return false;
24
+ }
25
+
26
+ const printerHost = getPrinterHost(managedPrinter);
27
+ const printerPort = normalizePrinterPort(
28
+ managedPrinter?.configs?.[NETWORK_PRINTER_PORT_CONFIG_KEY] ||
29
+ DEFAULT_NETWORK_PRINTER_PORT,
30
+ );
31
+ const printerCodePage =
32
+ managedPrinter?.configs?.[NETWORK_PRINTER_CODE_PAGE_CONFIG_KEY] ||
33
+ DEFAULT_NETWORK_PRINTER_CODE_PAGE;
34
+
35
+ await printOnNetworkPrinter({
36
+ host: printerHost,
37
+ port: printerPort,
38
+ codePage: printerCodePage,
39
+ payload: decodeNetworkPrinterPayload(content, {
40
+ codePage: printerCodePage,
41
+ }),
42
+ });
43
+
44
+ return true;
45
+ };
@@ -0,0 +1,63 @@
1
+ export const COMPANY_DEVICE_CONFIGS_CACHE_TTL_MS = 30 * 1000;
2
+
3
+ export const PRINT_ROUTE_MISSING_DESTINATION =
4
+ 'Não foi possível rotear a impressão: nenhum device de impressão configurado.';
5
+
6
+ const normalizeDeviceId = value => String(value || '').trim();
7
+ const normalizeDeviceType = value => String(value || '').trim().toUpperCase();
8
+
9
+ export const PDV_RUNTIME_TYPE = 'PDV';
10
+ export const DISPLAY_RUNTIME_TYPE = 'DISPLAY';
11
+
12
+ export const resolvePrinterDeviceId = printer =>
13
+ normalizeDeviceId(printer?.device || printer?.deviceId);
14
+
15
+ /**
16
+ * Device ids whose open spools this runtime should poll/print.
17
+ * Managed network printers expose `device` (not only `deviceId`).
18
+ */
19
+ export const resolveSpoolDeviceIdsForRuntime = ({
20
+ runtimeDeviceId = '',
21
+ runtimeDeviceType = '',
22
+ isWebRuntime = false,
23
+ managedPrinters = [],
24
+ includeLocalDevice = true,
25
+ } = {}) => {
26
+ const normalizedRuntimeId = normalizeDeviceId(runtimeDeviceId);
27
+ if (!normalizedRuntimeId || isWebRuntime) {
28
+ return [];
29
+ }
30
+
31
+ const managedIds = (Array.isArray(managedPrinters) ? managedPrinters : [])
32
+ .map(resolvePrinterDeviceId)
33
+ .filter(Boolean);
34
+
35
+ const type = normalizeDeviceType(runtimeDeviceType);
36
+
37
+ if (type === PDV_RUNTIME_TYPE) {
38
+ return Array.from(
39
+ new Set(
40
+ [
41
+ ...(includeLocalDevice ? [normalizedRuntimeId] : []),
42
+ ...managedIds,
43
+ ].filter(Boolean),
44
+ ),
45
+ );
46
+ }
47
+
48
+ if (type === DISPLAY_RUNTIME_TYPE) {
49
+ return Array.from(new Set(managedIds));
50
+ }
51
+
52
+ return [];
53
+ };
54
+
55
+ export const resolvePrintRoutingError = ({
56
+ targetDeviceId = '',
57
+ selectedPrinter = null,
58
+ } = {}) => {
59
+ const resolvedTarget = normalizeDeviceId(
60
+ targetDeviceId || selectedPrinter?.device || selectedPrinter?.deviceId,
61
+ );
62
+ return resolvedTarget ? '' : PRINT_ROUTE_MISSING_DESTINATION;
63
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Pure helpers for print spool routing / queue handling.
3
+ * Kept small so PrintService stays under the 500-line absolute limit.
4
+ */
5
+
6
+ export const SOCKET_PRINT_POLL_INTERVAL_DISCONNECTED = 10000;
7
+ export const SOCKET_PRINT_POLL_DELAY_CONNECTED = 60000;
8
+ export const SPOOL_ACK_RETRY_DELAY_MS = 10000;
9
+
10
+ export const normalizeDeviceType = value =>
11
+ String(value || '').trim().toUpperCase();
12
+
13
+ export const extractCollectionMembers = data => {
14
+ if (Array.isArray(data)) {
15
+ return data;
16
+ }
17
+
18
+ if (Array.isArray(data?.member)) {
19
+ return data.member;
20
+ }
21
+
22
+ if (Array.isArray(data?.['hydra:member'])) {
23
+ return data['hydra:member'];
24
+ }
25
+
26
+ return [];
27
+ };
28
+
29
+ export const resolveSpoolId = value => {
30
+ if (!value) return null;
31
+ if (typeof value === 'number') return value;
32
+ if (typeof value === 'string') {
33
+ const numericValue = value.replace(/\D/g, '');
34
+ return numericValue ? Number(numericValue) : null;
35
+ }
36
+
37
+ return (
38
+ resolveSpoolId(value?.id) ||
39
+ resolveSpoolId(value?.spoolId) ||
40
+ resolveSpoolId(value?.spool) ||
41
+ resolveSpoolId(value?.['@id'])
42
+ );
43
+ };
44
+
45
+ /**
46
+ * Merge open-spool collections from multiple devices, dedupe by id, sort ascending.
47
+ */
48
+ export const mergeOpenSpoolCollections = spoolCollections => {
49
+ const mergedSpools = [];
50
+ const spoolIds = new Set();
51
+
52
+ (Array.isArray(spoolCollections) ? spoolCollections : [])
53
+ .flat()
54
+ .forEach(item => {
55
+ const spoolId = resolveSpoolId(item);
56
+ const spoolKey = spoolId ? String(spoolId) : JSON.stringify(item);
57
+
58
+ if (!spoolIds.has(spoolKey)) {
59
+ spoolIds.add(spoolKey);
60
+ mergedSpools.push(item);
61
+ }
62
+ });
63
+
64
+ mergedSpools.sort((left, right) => {
65
+ const leftId = resolveSpoolId(left) || 0;
66
+ const rightId = resolveSpoolId(right) || 0;
67
+ return leftId - rightId;
68
+ });
69
+
70
+ return mergedSpools;
71
+ };
72
+
73
+ /**
74
+ * Fetch open spools for a list of device ids (parallel).
75
+ * Returns merged, deduped, sorted list. Caller handles store updates / errors.
76
+ */
77
+ export const fetchOpenSpoolsForDevices = async (api, deviceIds) => {
78
+ const ids = Array.from(
79
+ new Set((Array.isArray(deviceIds) ? deviceIds : []).filter(Boolean)),
80
+ );
81
+
82
+ if (ids.length === 0) {
83
+ return [];
84
+ }
85
+
86
+ const spoolCollections = await Promise.all(
87
+ ids.map(deviceId =>
88
+ api
89
+ .fetch('spools', {
90
+ params: {
91
+ 'device.device': deviceId,
92
+ 'status.realStatus': 'open',
93
+ },
94
+ })
95
+ .then(extractCollectionMembers)
96
+ .catch(() => []),
97
+ ),
98
+ );
99
+
100
+ return mergeOpenSpoolCollections(spoolCollections);
101
+ };
102
+
103
+ export const resolveRequestedTargetDeviceId = (printJob, fallbackDeviceId) => {
104
+ const raw =
105
+ printJob?.targetDeviceId ||
106
+ printJob?.deviceId ||
107
+ printJob?.device?.device ||
108
+ printJob?.device ||
109
+ fallbackDeviceId;
110
+ return String(raw || '').trim();
111
+ };
112
+
113
+ export const resolveRequestedTargetDeviceTypeValue = printJob =>
114
+ normalizeDeviceType(
115
+ printJob?.targetDeviceType ||
116
+ printJob?.deviceType ||
117
+ printJob?.device?.type ||
118
+ '',
119
+ );