@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,6 +1,10 @@
1
1
  import { getDeviceTypeLabel } from '@controleonline/ui-common/src/react/utils/printerDevices';
2
2
 
3
3
  const DEVICE_RUNTIME_FOOTER_TEXT_CONFIG_KEY = 'device-runtime-footer-text';
4
+ const RUNTIME_FOOTER_HIDDEN_ROUTES = new Set(['PaylistPage']);
5
+
6
+ const shouldShowRuntimeFooter = currentRouteName =>
7
+ !RUNTIME_FOOTER_HIDDEN_ROUTES.has(String(currentRouteName || '').trim());
4
8
 
5
9
  const safeTrim = value => String(value || '').trim();
6
10
 
@@ -489,5 +493,6 @@ module.exports = {
489
493
  getRuntimeFooterWebIdentifierCandidates,
490
494
  getRuntimeFooterText,
491
495
  getRuntimeFooterWebHost,
492
- normalizeRuntimeFooterText,
496
+ normalizeRuntimeFooterText,
497
+ shouldShowRuntimeFooter,
493
498
  };
@@ -1,10 +1,10 @@
1
- import {api} from '@controleonline/ui-common/src/api';
2
- import {
3
- extractCollectionItems,
4
- toEntityIri,
5
- } from '@controleonline/ui-common/src/react/utils/commercialDocumentOrders';
1
+ import {api} from '@controleonline/ui-common/src/api';
2
+ import {
3
+ extractCollectionItems,
4
+ toEntityIri,
5
+ } from '@controleonline/ui-common/src/react/utils/commercialDocumentOrders';
6
6
  import {normalizeShopEntityId} from '@controleonline/ui-common/src/react/utils/shopConfig';
7
-
7
+
8
8
  export const SHOP_FRANCHISE_LINK_TYPE = 'franchisee';
9
9
  export const SHOP_FRANCHISE_PAGE_SIZE = 50;
10
10
 
@@ -150,39 +150,113 @@ export const filterShopFranchiseDirectory = ({
150
150
  })
151
151
  .filter(Boolean);
152
152
  };
153
-
154
- export const resolveFranchiseCompanyLabel = company =>
155
- String(company?.alias || company?.name || '').trim() ||
156
- `Franquia #${normalizeShopEntityId(company) || ''}`.trim();
157
-
158
- export const fetchShopFranchiseCompanies = async ({
153
+
154
+ export const resolveFranchiseCompanyLabel = company =>
155
+ String(company?.alias || company?.name || '').trim() ||
156
+ `Franquia #${normalizeShopEntityId(company) || ''}`.trim();
157
+
158
+ /**
159
+ * Resolve the franchise company entity from a people_link relative to the
160
+ * viewed franchisor/company id. DB rows may store the viewed company as
161
+ * `company` or as `people` (inverted association).
162
+ */
163
+ export const extractFranchiseCompanyFromLink = (link, viewerCompanyId) => {
164
+ const viewerId = normalizeShopEntityId(viewerCompanyId);
165
+ const companySide = link?.company;
166
+ const peopleSide = link?.people;
167
+ const companyId = normalizeShopEntityId(companySide);
168
+ const peopleId = normalizeShopEntityId(peopleSide);
169
+
170
+ if (viewerId && companyId === viewerId) {
171
+ return peopleSide;
172
+ }
173
+ if (viewerId && peopleId === viewerId) {
174
+ return companySide;
175
+ }
176
+ // Fallback: franchisee is typically the `people` side of linkType=franchisee.
177
+ return peopleSide || companySide || null;
178
+ };
179
+
180
+ const fetchFranchiseLinksPage = async ({
159
181
  companyId,
160
- search = '',
182
+ side,
161
183
  page = 1,
162
184
  itemsPerPage = SHOP_FRANCHISE_PAGE_SIZE,
163
- publicDirectory = false,
164
- } = {}) => {
185
+ search = '',
186
+ }) => {
187
+ const id = normalizeShopEntityId(companyId);
188
+ if (!id) {
189
+ return [];
190
+ }
191
+
165
192
  const params = {
166
193
  page: Math.max(1, Number(page) || 1),
167
194
  itemsPerPage: normalizeItemsPerPage(itemsPerPage),
195
+ linkType: SHOP_FRANCHISE_LINK_TYPE,
196
+ [side]: toEntityIri(id, 'people'),
168
197
  };
169
198
 
170
199
  if (String(search || '').trim()) {
171
200
  params.search = String(search).trim();
172
201
  }
173
202
 
174
- const response = publicDirectory
175
- ? await api.fetch('/shop/franchises', {params})
176
- : await api.fetch('people', {
177
- params: {
178
- ...params,
179
- 'link.company': toEntityIri(companyId, 'people'),
180
- 'link.linkType': SHOP_FRANCHISE_LINK_TYPE,
181
- },
203
+ const response = await api.fetch('people_links', {params});
204
+ return extractCollectionItems(response);
205
+ };
206
+
207
+ /**
208
+ * Authenticated franchise directory via people_links (both sides).
209
+ * GET /people?link.company=… does not reliably return franchisee PJs.
210
+ */
211
+ const fetchFranchiseCompaniesFromLinks = async ({
212
+ companyId,
213
+ search = '',
214
+ itemsPerPage = SHOP_FRANCHISE_PAGE_SIZE,
215
+ } = {}) => {
216
+ const viewerId = normalizeShopEntityId(companyId);
217
+ if (!viewerId) {
218
+ return [];
219
+ }
220
+
221
+ const pageSize = normalizeItemsPerPage(itemsPerPage);
222
+ const byId = new Map();
223
+
224
+ for (const side of ['company', 'people']) {
225
+ let page = 1;
226
+ while (true) {
227
+ const links = await fetchFranchiseLinksPage({
228
+ companyId: viewerId,
229
+ side,
230
+ page,
231
+ itemsPerPage: pageSize,
232
+ search,
233
+ });
234
+ const pageLinks = Array.isArray(links) ? links : [];
235
+
236
+ pageLinks.forEach(link => {
237
+ const franchise = extractFranchiseCompanyFromLink(link, viewerId);
238
+ const franchiseId = normalizeShopEntityId(franchise);
239
+ if (!franchiseId || franchiseId === viewerId) {
240
+ return;
241
+ }
242
+ if (!byId.has(franchiseId)) {
243
+ byId.set(
244
+ franchiseId,
245
+ typeof franchise === 'object' && franchise
246
+ ? franchise
247
+ : {id: franchiseId},
248
+ );
249
+ }
182
250
  });
183
- const items = extractCollectionItems(response);
184
251
 
185
- return items
252
+ if (pageLinks.length < pageSize) {
253
+ break;
254
+ }
255
+ page += 1;
256
+ }
257
+ }
258
+
259
+ return Array.from(byId.values())
186
260
  .map(normalizeFranchiseDirectoryItem)
187
261
  .sort((left, right) =>
188
262
  sortByLabel(
@@ -192,67 +266,113 @@ export const fetchShopFranchiseCompanies = async ({
192
266
  );
193
267
  };
194
268
 
195
- export const fetchAllShopFranchiseDirectory = async ({
269
+ export const fetchShopFranchiseCompanies = async ({
196
270
  companyId,
197
- publicDirectory = false,
198
271
  search = '',
272
+ page = 1,
199
273
  itemsPerPage = SHOP_FRANCHISE_PAGE_SIZE,
274
+ publicDirectory = false,
200
275
  } = {}) => {
201
- const normalizedItemsPerPage = normalizeItemsPerPage(itemsPerPage);
202
- const items = [];
203
- let page = 1;
204
-
205
- while (true) {
206
- const pageItems = await fetchShopFranchiseCompanies({
207
- companyId,
208
- publicDirectory,
209
- search,
210
- page,
211
- itemsPerPage: normalizedItemsPerPage,
212
- });
213
- const normalizedPageItems = Array.isArray(pageItems) ? pageItems : [];
276
+ const params = {
277
+ page: Math.max(1, Number(page) || 1),
278
+ itemsPerPage: normalizeItemsPerPage(itemsPerPage),
279
+ };
214
280
 
215
- items.push(...normalizedPageItems);
281
+ if (String(search || '').trim()) {
282
+ params.search = String(search).trim();
283
+ }
284
+
285
+ if (publicDirectory) {
286
+ const response = await api.fetch('/shop/franchises', {params});
287
+ const items = extractCollectionItems(response);
288
+ return items
289
+ .map(normalizeFranchiseDirectoryItem)
290
+ .sort((left, right) =>
291
+ sortByLabel(
292
+ resolveFranchiseCompanyLabel(left),
293
+ resolveFranchiseCompanyLabel(right),
294
+ ),
295
+ );
296
+ }
297
+
298
+ // Management path: people_links dual-side (company + people).
299
+ // Page param is ignored for dual aggregation; callers that need full list
300
+ // should use fetchAllShopFranchiseDirectory.
301
+ const all = await fetchFranchiseCompaniesFromLinks({
302
+ companyId,
303
+ search,
304
+ itemsPerPage,
305
+ });
306
+ const start = (Math.max(1, Number(page) || 1) - 1) * normalizeItemsPerPage(itemsPerPage);
307
+ return all.slice(start, start + normalizeItemsPerPage(itemsPerPage));
308
+ };
216
309
 
217
- if (normalizedPageItems.length < normalizedItemsPerPage) {
218
- break;
310
+ export const fetchAllShopFranchiseDirectory = async ({
311
+ companyId,
312
+ publicDirectory = false,
313
+ search = '',
314
+ itemsPerPage = SHOP_FRANCHISE_PAGE_SIZE,
315
+ } = {}) => {
316
+ if (publicDirectory) {
317
+ const normalizedItemsPerPage = normalizeItemsPerPage(itemsPerPage);
318
+ const items = [];
319
+ let page = 1;
320
+
321
+ while (true) {
322
+ const pageItems = await fetchShopFranchiseCompanies({
323
+ companyId,
324
+ publicDirectory: true,
325
+ search,
326
+ page,
327
+ itemsPerPage: normalizedItemsPerPage,
328
+ });
329
+ const normalizedPageItems = Array.isArray(pageItems) ? pageItems : [];
330
+ items.push(...normalizedPageItems);
331
+ if (normalizedPageItems.length < normalizedItemsPerPage) {
332
+ break;
333
+ }
334
+ page += 1;
219
335
  }
220
336
 
221
- page += 1;
337
+ return items
338
+ .map(normalizeFranchiseDirectoryItem)
339
+ .sort((left, right) =>
340
+ sortByLabel(
341
+ resolveFranchiseCompanyLabel(left),
342
+ resolveFranchiseCompanyLabel(right),
343
+ ),
344
+ );
222
345
  }
223
346
 
224
- return items
225
- .map(normalizeFranchiseDirectoryItem)
226
- .sort((left, right) =>
227
- sortByLabel(
228
- resolveFranchiseCompanyLabel(left),
229
- resolveFranchiseCompanyLabel(right),
230
- ),
231
- );
347
+ return fetchFranchiseCompaniesFromLinks({
348
+ companyId,
349
+ search,
350
+ itemsPerPage,
351
+ });
232
352
  };
233
353
 
234
354
  export const fetchShopFranchiseAddresses = async ({
235
355
  peopleId,
236
356
  search = '',
237
357
  } = {}) => {
238
- if (!peopleId) {
239
- return [];
240
- }
241
-
358
+ if (!peopleId) {
359
+ return [];
360
+ }
361
+
242
362
  const params = {
243
363
  people: toEntityIri(peopleId, 'people'),
244
364
  itemsPerPage: SHOP_FRANCHISE_PAGE_SIZE,
245
365
  page: 1,
246
366
  };
247
-
248
- if (String(search || '').trim()) {
249
- params.search = String(search).trim();
250
- }
251
-
252
- const response = await api.fetch('addresses', {params});
253
- return extractCollectionItems(response);
254
- };
255
-
367
+
368
+ if (String(search || '').trim()) {
369
+ params.search = String(search).trim();
370
+ }
371
+
372
+ const response = await api.fetch('addresses', {params});
373
+ return extractCollectionItems(response);
374
+ };
375
+
256
376
  export const fetchShopFranchiseDirectory = async ({
257
377
  companyId,
258
378
  publicDirectory = false,
@@ -270,4 +390,5 @@ export const fetchShopFranchiseDirectory = async ({
270
390
 
271
391
  return companies.map(normalizeFranchiseDirectoryItem);
272
392
  };
393
+
273
394
  // TODO(store-first): quando este arquivo for mexido, mover a leitura para stores, remover api.fetch e evitar repassar dados em objetos quando o store ja resolver isso.
@@ -1,67 +1,71 @@
1
- const normalizeText = value => String(value || '').trim()
2
-
3
- const resolveMessageList = items =>
4
- (Array.isArray(items) ? items : [])
5
- .map(item =>
6
- normalizeText(
7
- item?.['hydra:description'] ||
8
- item?.['hydra:title'] ||
9
- item?.message ||
10
- item?.title ||
11
- item?.detail ||
12
- item?.description ||
13
- item,
14
- ),
15
- )
16
- .filter(Boolean)
17
- .join('\n')
18
- .trim()
19
-
20
- export const resolveSystemErrorMessage = error => {
21
- if (error === undefined || error === null) {
22
- return ''
23
- }
24
-
25
- if (typeof error === 'string') {
26
- return error.trim()
27
- }
28
-
29
- if (Array.isArray(error)) {
30
- return resolveMessageList(error)
31
- }
32
-
33
- if (Array.isArray(error?.message)) {
34
- return resolveMessageList(error.message)
35
- }
36
-
37
- if (Array.isArray(error?.violations)) {
38
- return resolveMessageList(error.violations)
39
- }
40
-
41
- if (error?.response?.data) {
42
- const responseMessage = resolveSystemErrorMessage(error.response.data)
43
- if (responseMessage) {
44
- return responseMessage
45
- }
46
- }
47
-
48
- if (error?.body) {
49
- const bodyMessage = resolveSystemErrorMessage(error.body)
50
- if (bodyMessage) {
51
- return bodyMessage
52
- }
53
- }
54
-
55
- return normalizeText(
56
- error?.['hydra:description'] ||
57
- error?.['hydra:title'] ||
58
- error?.detail ||
59
- error?.description ||
60
- error?.errmsg ||
61
- error?.error ||
62
- error?.message ||
63
- error?.title,
64
- )
65
- }
66
-
67
- export default resolveSystemErrorMessage
1
+ import { mapPasswordErrorMessage } from './passwordPolicy'
2
+
3
+ const normalizeText = value => String(value || '').trim()
4
+
5
+ const resolveMessageList = items =>
6
+ (Array.isArray(items) ? items : [])
7
+ .map(item =>
8
+ normalizeText(
9
+ item?.['hydra:description'] ||
10
+ item?.['hydra:title'] ||
11
+ item?.message ||
12
+ item?.title ||
13
+ item?.detail ||
14
+ item?.description ||
15
+ item,
16
+ ),
17
+ )
18
+ .filter(Boolean)
19
+ .join('\n')
20
+ .trim()
21
+
22
+ export const resolveSystemErrorMessage = error => {
23
+ if (error === undefined || error === null) {
24
+ return ''
25
+ }
26
+
27
+ if (typeof error === 'string') {
28
+ return mapPasswordErrorMessage(error.trim())
29
+ }
30
+
31
+ if (Array.isArray(error)) {
32
+ return mapPasswordErrorMessage(resolveMessageList(error))
33
+ }
34
+
35
+ if (Array.isArray(error?.message)) {
36
+ return mapPasswordErrorMessage(resolveMessageList(error.message))
37
+ }
38
+
39
+ if (Array.isArray(error?.violations)) {
40
+ return mapPasswordErrorMessage(resolveMessageList(error.violations))
41
+ }
42
+
43
+ if (error?.response?.data) {
44
+ const responseMessage = resolveSystemErrorMessage(error.response.data)
45
+ if (responseMessage) {
46
+ return responseMessage
47
+ }
48
+ }
49
+
50
+ if (error?.body) {
51
+ const bodyMessage = resolveSystemErrorMessage(error.body)
52
+ if (bodyMessage) {
53
+ return bodyMessage
54
+ }
55
+ }
56
+
57
+ return mapPasswordErrorMessage(
58
+ normalizeText(
59
+ error?.['hydra:description'] ||
60
+ error?.['hydra:title'] ||
61
+ error?.detail ||
62
+ error?.description ||
63
+ error?.errmsg ||
64
+ error?.error ||
65
+ error?.message ||
66
+ error?.title,
67
+ ),
68
+ )
69
+ }
70
+
71
+ export default resolveSystemErrorMessage
@@ -2,6 +2,9 @@ import {api} from '@controleonline/ui-common/src/api';
2
2
  import * as types from '@controleonline/ui-default/src/store/default/mutation_types';
3
3
  import {isWebRuntimeDevice} from '@controleonline/ui-common/src/react/utils/deviceRuntime';
4
4
 
5
+ /** Max age for company device_configs cache used by print routing (ms). */
6
+ export const COMPANY_DEVICE_CONFIGS_CACHE_TTL_MS = 30 * 1000;
7
+
5
8
  const normalizePeopleKey = value =>
6
9
  String(value || '')
7
10
  .replace(/\D+/g, '')
@@ -34,16 +37,84 @@ const getStoredDevice = () => {
34
37
  return {};
35
38
  };
36
39
 
40
+ const resolveConfigId = config => {
41
+ if (!config) {
42
+ return '';
43
+ }
44
+ if (config.id != null && String(config.id).trim() !== '') {
45
+ return String(config.id).trim();
46
+ }
47
+ const iri = String(config['@id'] || '').trim();
48
+ if (!iri) {
49
+ return '';
50
+ }
51
+ const parts = iri.split('/');
52
+ return String(parts[parts.length - 1] || '').trim();
53
+ };
54
+
55
+ /**
56
+ * Merge a saved device_config into the company list and clear loadedKey so the
57
+ * next ensureCompanyDeviceConfigsLoaded can refresh (print routing picks up
58
+ * manager/destination changes without app rebuild).
59
+ */
60
+ const mergeSavedConfigIntoCompanyItems = (commit, getters, saved) => {
61
+ if (!saved) {
62
+ return;
63
+ }
64
+
65
+ const currentItems = Array.isArray(getters.items) ? getters.items : [];
66
+ const savedId = resolveConfigId(saved);
67
+ const savedDeviceId = String(
68
+ saved?.device?.device || saved?.device?.id || saved?.device || '',
69
+ ).trim();
70
+ const savedType = String(saved?.type || '').trim().toUpperCase();
71
+
72
+ let replaced = false;
73
+ const nextItems = currentItems.map(item => {
74
+ const itemId = resolveConfigId(item);
75
+ if (savedId && itemId && savedId === itemId) {
76
+ replaced = true;
77
+ return {...item, ...saved};
78
+ }
79
+
80
+ const itemDeviceId = String(
81
+ item?.device?.device || item?.device?.id || item?.device || '',
82
+ ).trim();
83
+ const itemType = String(item?.type || '').trim().toUpperCase();
84
+ if (
85
+ savedDeviceId &&
86
+ itemDeviceId &&
87
+ savedDeviceId === itemDeviceId &&
88
+ savedType &&
89
+ itemType === savedType
90
+ ) {
91
+ replaced = true;
92
+ return {...item, ...saved};
93
+ }
94
+
95
+ return item;
96
+ });
97
+
98
+ if (!replaced && savedId) {
99
+ nextItems.push(saved);
100
+ }
101
+
102
+ commit(types.SET_ITEMS, nextItems);
103
+ // Force next ensure to re-fetch when TTL allows (other runtimes still refresh via TTL).
104
+ commit(types.SET_LOADED_KEY, '');
105
+ commit(types.SET_LOADED_AT, 0);
106
+ };
107
+
37
108
  export const addDeviceConfigs = ({commit, getters}, params) => {
38
109
  let configsObj = {};
39
110
  if (params.configs) {
40
111
  if (typeof params.configs === 'string') {
41
112
  try {
42
113
  configsObj = JSON.parse(params.configs);
43
- } catch (e) {
44
- configsObj = {};
45
- }
46
- } else if (typeof params.configs === 'object') {
114
+ } catch (e) {
115
+ configsObj = {};
116
+ }
117
+ } else if (typeof params.configs === 'object') {
47
118
  configsObj = params.configs;
48
119
  }
49
120
  }
@@ -82,30 +153,31 @@ export const addDeviceConfigs = ({commit, getters}, params) => {
82
153
  if (isRuntimeWebDevice) {
83
154
  commit(types.SET_ITEM, nextItem);
84
155
  if (!canPersistRuntimeWebDevice) {
156
+ mergeSavedConfigIntoCompanyItems(commit, getters, nextItem);
85
157
  return Promise.resolve(nextItem);
86
158
  }
87
159
  }
88
160
 
89
161
  let options = {
90
162
  method: 'POST',
91
- body: updatedParams,
92
- };
93
- commit(types.SET_ISSAVING, true);
94
-
95
- return api
163
+ body: updatedParams,
164
+ };
165
+ commit(types.SET_ISSAVING, true);
166
+
167
+ return api
96
168
  .fetch(getters.resourceEndpoint + '/add-configs', options)
97
169
  .then(data => {
98
170
  let parsedConfigs = {};
99
- if (data?.configs) {
100
- if (typeof data.configs === 'string') {
101
- try {
102
- parsedConfigs = JSON.parse(data.configs);
103
- } catch (e) {
104
- parsedConfigs = {};
105
- }
106
- } else if (typeof data.configs === 'object') {
107
- parsedConfigs = data.configs;
108
- }
171
+ if (data?.configs) {
172
+ if (typeof data.configs === 'string') {
173
+ try {
174
+ parsedConfigs = JSON.parse(data.configs);
175
+ } catch (e) {
176
+ parsedConfigs = {};
177
+ }
178
+ } else if (typeof data.configs === 'object') {
179
+ parsedConfigs = data.configs;
180
+ }
109
181
  }
110
182
  const hasParsedConfigs =
111
183
  parsedConfigs && Object.keys(parsedConfigs).length > 0;
@@ -118,15 +190,22 @@ export const addDeviceConfigs = ({commit, getters}, params) => {
118
190
  : nextItem.configs || getters.item?.configs || {},
119
191
  };
120
192
  commit(types.SET_ITEM, d);
193
+ mergeSavedConfigIntoCompanyItems(commit, getters, d);
121
194
  return d;
122
195
  })
123
- .catch(e => {
124
- console.error('addDeviceConfigs API error:', e);
125
- commit(types.SET_ERROR, e.message);
126
- throw e;
127
- })
128
- .finally(() => {
129
- commit(types.SET_ISSAVING, false);
196
+ .catch(e => {
197
+ const status = Number(e?.status || e?.code || e?.response?.status || 0);
198
+ // 403 is expected for non-tenant-admin users during runtime bootstrap.
199
+ if (status === 403) {
200
+ console.debug('addDeviceConfigs skipped (no tenant admin authority):', e?.message || e);
201
+ } else {
202
+ console.error('addDeviceConfigs API error:', e);
203
+ }
204
+ commit(types.SET_ERROR, e.message);
205
+ throw e;
206
+ })
207
+ .finally(() => {
208
+ commit(types.SET_ISSAVING, false);
130
209
  });
131
210
  };
132
211
 
@@ -174,10 +253,16 @@ export const ensureCompanyDeviceConfigsLoaded = (context, params = {}) => {
174
253
  return Promise.resolve([]);
175
254
  }
176
255
 
177
- if (
256
+ const force = params?.force === true;
257
+ const loadedAt = Number(context.getters.loadedAt) || 0;
258
+ const cacheAgeMs = loadedAt > 0 ? Date.now() - loadedAt : Number.POSITIVE_INFINITY;
259
+ const cacheFresh =
260
+ !force &&
178
261
  context.getters.loadedKey === peopleKey &&
179
- Array.isArray(context.getters.items)
180
- ) {
262
+ Array.isArray(context.getters.items) &&
263
+ cacheAgeMs < COMPANY_DEVICE_CONFIGS_CACHE_TTL_MS;
264
+
265
+ if (cacheFresh) {
181
266
  return Promise.resolve(context.getters.items);
182
267
  }
183
268
 
@@ -187,4 +272,8 @@ export const ensureCompanyDeviceConfigsLoaded = (context, params = {}) => {
187
272
 
188
273
  return getCompanyDeviceConfigs(context, params);
189
274
  };
190
-
275
+
276
+ export const invalidateCompanyDeviceConfigsCache = ({commit}) => {
277
+ commit(types.SET_LOADED_KEY, '');
278
+ commit(types.SET_LOADED_AT, 0);
279
+ };