@controleonline/ui-common 1.2.87 → 1.2.89

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 (77) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +7 -156
  3. package/.github/workflows/publish-npm.yml +73 -27
  4. package/.github/workflows/pull-request-checks.yml +7 -5
  5. package/docs/technical/Device-Detail-Alias-UI-Sync.md +141 -0
  6. package/package.json +13 -8
  7. package/src/api/localDB.js +86 -131
  8. package/src/react/components/BottomNavigationBar.js +178 -174
  9. package/src/react/components/BottomNavigationBar.styles.js +139 -118
  10. package/src/react/components/DefaultProvider.native.js +427 -421
  11. package/src/react/components/DefaultProvider.web.js +131 -125
  12. package/src/react/components/RemoteCheckoutService.js +3 -3
  13. package/src/react/components/RuntimeFooterMarqueeText.js +88 -36
  14. package/src/react/components/RuntimeInfoFooter.js +76 -43
  15. package/src/react/components/RuntimeInfoFooter.styles.js +22 -14
  16. package/src/react/config/deviceConfigBootstrap.js +71 -233
  17. package/src/react/config/deviceConfigValues.js +238 -0
  18. package/src/react/pages/Devices/detail/DeviceChargePermissionSection.js +32 -0
  19. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +4 -22
  20. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +6 -2
  21. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +4 -0
  22. package/src/react/pages/Devices/detail/DeviceDetailPdvConfigSection.js +7 -0
  23. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +24 -335
  24. package/src/react/pages/Devices/detail/createDeviceDetailContext.js +20 -0
  25. package/src/react/pages/Devices/detail/deviceDetailHelpers.js +2 -11
  26. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +11 -36
  27. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +129 -37
  28. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +11 -38
  29. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +9 -28
  30. package/src/react/pages/IntegrationConfigFields.js +1 -0
  31. package/src/react/pages/IntegrationConfigPage.js +101 -45
  32. package/src/react/pages/IntegrationConfigPage.utils.js +21 -13
  33. package/src/react/pages/fetchPeopleConfigs.js +21 -0
  34. package/src/react/pages/integrationsCatalog.js +15 -110
  35. package/src/react/services/Cielo/Cielo.js +9 -9
  36. package/src/react/services/Getnet/Getnet.web.js +25 -0
  37. package/src/react/utils/companyAuthority.js +36 -0
  38. package/src/react/utils/confirmAction.js +12 -0
  39. package/src/react/utils/fileUrl.js +14 -5
  40. package/src/react/utils/paymentDevices.js +375 -270
  41. package/src/react/utils/printerDevices.js +2 -2
  42. package/src/react/utils/runtimeFooter.js +178 -224
  43. package/src/react/utils/runtimeFooterText.js +160 -0
  44. package/src/react/utils/runtimeLanguage.js +5 -5
  45. package/src/react/utils/runtimeZoom.js +29 -5
  46. package/src/react/utils/shopFranchises.js +167 -12
  47. package/src/react/utils/storeColumns.js +235 -217
  48. package/src/react/utils/systemErrorMessage.js +2 -1
  49. package/src/store/integration/index.js +43 -0
  50. package/src/tests/browser/manager/device-detail-apply-config-defined.spec.js +31 -0
  51. package/src/tests/browser/manager/device-detail-composition.spec.js +295 -0
  52. package/src/tests/browser/manager/device-detail-copy-config.spec.js +11 -1
  53. package/src/tests/browser/manager/device-detail-pdv-current-company.spec.js +273 -0
  54. package/src/tests/browser/manager/runtime-footer-rotation.spec.js +397 -0
  55. package/src/tests/react/components/DefaultProviderAuthority.test.js +28 -0
  56. package/src/tests/react/components/RuntimeFooterMarqueeText.build.test.js +29 -0
  57. package/src/tests/react/config/deviceConfigBootstrap.test.js +24 -1
  58. package/src/tests/react/pages/DeviceChargePermissionSection.test.js +42 -0
  59. package/src/tests/react/pages/applyCurrentDeviceConfig.test.js +85 -0
  60. package/src/tests/react/pages/deviceDetailComposition.test.js +65 -0
  61. package/src/tests/react/pages/deviceDetailHookDependencies.test.mjs +31 -0
  62. package/src/tests/react/pages/deviceDetailLoaderDependencies.test.mjs +28 -0
  63. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +27 -6
  64. package/src/tests/react/services/Cielo.test.js +2 -2
  65. package/src/tests/react/utils/companyAuthority.test.js +53 -0
  66. package/src/tests/react/utils/formatter.test.mjs +6 -0
  67. package/src/tests/react/utils/paymentDevices.test.js +72 -0
  68. package/src/tests/react/utils/runtimeFooter.test.js +35 -0
  69. package/src/tests/react/utils/runtimeFooterText.test.js +88 -0
  70. package/src/tests/react/utils/runtimeLanguage.test.js +4 -4
  71. package/src/tests/react/utils/runtimeZoom.test.js +20 -0
  72. package/src/tests/react/utils/shopFranchises.test.js +32 -13
  73. package/src/tests/react/utils/translate.test.mjs +6 -6
  74. package/src/utils/formatter.js +7 -3
  75. package/src/utils/translate.js +16 -16
  76. package/src/utils/translatePending.js +6 -6
  77. package/src/utils/translateStorage.js +2 -2
@@ -22,7 +22,10 @@ export const normalizeRuntimeZoomPercent = value => {
22
22
  export const resolveRuntimeZoomScale = value =>
23
23
  normalizeRuntimeZoomPercent(value) / 100;
24
24
 
25
- export const buildRuntimeZoomStyle = (value, {isWeb = false} = {}) => {
25
+ export const buildRuntimeZoomStyle = (
26
+ value,
27
+ {isWeb = false, viewport = null} = {},
28
+ ) => {
26
29
  const scale = resolveRuntimeZoomScale(value);
27
30
 
28
31
  if (scale === 1) {
@@ -33,11 +36,32 @@ export const buildRuntimeZoomStyle = (value, {isWeb = false} = {}) => {
33
36
  return {zoom: scale};
34
37
  }
35
38
 
36
- const inverseSize = `${100 / scale}%`;
39
+ const inverseScale = 1 / scale;
40
+ const nativeBase = {
41
+ transform: [{scale}],
42
+ transformOrigin: 'top left',
43
+ alignSelf: 'flex-start',
44
+ };
45
+
46
+ const viewportWidth = Number(viewport?.width);
47
+ const viewportHeight = Number(viewport?.height);
48
+
49
+ if (
50
+ Number.isFinite(viewportWidth) &&
51
+ Number.isFinite(viewportHeight) &&
52
+ viewportWidth > 0 &&
53
+ viewportHeight > 0
54
+ ) {
55
+ return {
56
+ ...nativeBase,
57
+ width: viewportWidth * inverseScale,
58
+ height: viewportHeight * inverseScale,
59
+ };
60
+ }
37
61
 
38
62
  return {
39
- height: inverseSize,
40
- transform: [{scale}],
41
- width: inverseSize,
63
+ ...nativeBase,
64
+ width: `${100 * inverseScale}%`,
65
+ height: `${100 * inverseScale}%`,
42
66
  };
43
67
  };
@@ -8,17 +8,108 @@ import {normalizeShopEntityId} from '@controleonline/ui-common/src/react/utils/s
8
8
  export const SHOP_FRANCHISE_LINK_TYPE = 'franchisee';
9
9
  export const SHOP_FRANCHISE_PAGE_SIZE = 50;
10
10
 
11
+ /** people.enable must be 1/true for franchise to appear in settings Maps list (#815). */
12
+ export const isPeopleEnabled = people => {
13
+ if (!people || typeof people !== 'object') {
14
+ return false;
15
+ }
16
+ // API may expose enable or enabled (GeneralTab uses both).
17
+ const value = people.enable ?? people.enabled;
18
+ if (value === true || value === 1 || value === '1') {
19
+ return true;
20
+ }
21
+ if (value === false || value === 0 || value === '0') {
22
+ return false;
23
+ }
24
+ return false;
25
+ };
26
+
27
+ /** Load people.enable from API so people_links embeds without enable are accurate (#815). */
28
+ const hydratePeopleEnableFlags = async companies => {
29
+ const list = Array.isArray(companies) ? companies : [];
30
+ const hydrated = await Promise.all(
31
+ list.map(async company => {
32
+ const peopleId = normalizeShopEntityId(company);
33
+ if (!peopleId) {
34
+ return company;
35
+ }
36
+ // Always read enable from people table — people_links embed can be stale/wrong (#815)
37
+ try {
38
+ const person = await api.fetch(`people/${peopleId}`);
39
+ if (person && typeof person === 'object') {
40
+ return {
41
+ ...company,
42
+ enable: person.enable ?? person.enabled,
43
+ enabled: person.enabled ?? person.enable,
44
+ };
45
+ }
46
+ } catch (_) {}
47
+ return company;
48
+ }),
49
+ );
50
+ return hydrated;
51
+ };
52
+
53
+
11
54
  const normalizeItemsPerPage = value =>
12
55
  Math.max(1, Math.min(SHOP_FRANCHISE_PAGE_SIZE, Number(value) || SHOP_FRANCHISE_PAGE_SIZE));
13
56
 
14
- const normalizeFranchiseDirectoryItem = company => ({
15
- ...company,
16
- shopAddresses: Array.isArray(company?.shopAddresses)
57
+ const parseCoordValue = value => {
58
+ if (value === null || value === undefined || value === '') {
59
+ return null;
60
+ }
61
+ const n = Number(value);
62
+ return Number.isFinite(n) ? n : null;
63
+ };
64
+
65
+ /** Coords may live on the address root or under address.map (API / DefaultAddress). */
66
+ export const resolveFranchiseAddressCoords = address => {
67
+ if (!address || typeof address !== 'object') {
68
+ return {latitude: null, longitude: null};
69
+ }
70
+ const latitude = parseCoordValue(
71
+ address.latitude ??
72
+ address.lat ??
73
+ address?.map?.latitude ??
74
+ address?.map?.lat ??
75
+ address?.geo?.latitude,
76
+ );
77
+ const longitude = parseCoordValue(
78
+ address.longitude ??
79
+ address.lng ??
80
+ address.lon ??
81
+ address?.map?.longitude ??
82
+ address?.map?.lng ??
83
+ address?.map?.lon ??
84
+ address?.geo?.longitude,
85
+ );
86
+ return {latitude, longitude};
87
+ };
88
+
89
+ const normalizeFranchiseAddress = address => {
90
+ if (!address || typeof address !== 'object') {
91
+ return address;
92
+ }
93
+ const {latitude, longitude} = resolveFranchiseAddressCoords(address);
94
+ return {
95
+ ...address,
96
+ latitude,
97
+ longitude,
98
+ };
99
+ };
100
+
101
+ const normalizeFranchiseDirectoryItem = company => {
102
+ const rawAddresses = Array.isArray(company?.shopAddresses)
17
103
  ? company.shopAddresses
18
104
  : Array.isArray(company?.address)
19
105
  ? company.address
20
- : [],
21
- });
106
+ : [];
107
+
108
+ return {
109
+ ...company,
110
+ shopAddresses: rawAddresses.map(normalizeFranchiseAddress),
111
+ };
112
+ };
22
113
 
23
114
  const sortByLabel = (left, right) =>
24
115
  String(left || '')
@@ -162,6 +253,10 @@ export const resolveFranchiseCompanyLabel = company =>
162
253
  */
163
254
  export const extractFranchiseCompanyFromLink = (link, viewerCompanyId) => {
164
255
  const viewerId = normalizeShopEntityId(viewerCompanyId);
256
+ if (!viewerId || link?.linkType !== SHOP_FRANCHISE_LINK_TYPE) {
257
+ return null;
258
+ }
259
+
165
260
  const companySide = link?.company;
166
261
  const peopleSide = link?.people;
167
262
  const companyId = normalizeShopEntityId(companySide);
@@ -173,8 +268,11 @@ export const extractFranchiseCompanyFromLink = (link, viewerCompanyId) => {
173
268
  if (viewerId && peopleId === viewerId) {
174
269
  return companySide;
175
270
  }
176
- // Fallback: franchisee is typically the `people` side of linkType=franchisee.
177
- return peopleSide || companySide || null;
271
+
272
+ // Never trust an API response that does not prove the current company is
273
+ // one side of the link. This prevents a broad/ignored people_links filter
274
+ // from leaking another tenant's franchise or its addresses.
275
+ return null;
178
276
  };
179
277
 
180
278
  const fetchFranchiseLinksPage = async ({
@@ -189,13 +287,23 @@ const fetchFranchiseLinksPage = async ({
189
287
  return [];
190
288
  }
191
289
 
290
+ // Align with FranchiseLinksTab (ui-customers): numeric company/people id + enable.
192
291
  const params = {
193
292
  page: Math.max(1, Number(page) || 1),
194
293
  itemsPerPage: normalizeItemsPerPage(itemsPerPage),
195
- linkType: SHOP_FRANCHISE_LINK_TYPE,
196
- [side]: toEntityIri(id, 'people'),
294
+ // linkType must be array — API rejects string
295
+ // Do NOT filter enable=true: many franchisee rows omit/null enable and would vanish (staging company=1 had 4 franchisees, 0 with enable=true).
296
+ linkType: [String(SHOP_FRANCHISE_LINK_TYPE)],
197
297
  };
198
298
 
299
+ if (side === 'company') {
300
+ params.company = id;
301
+ } else if (side === 'people') {
302
+ params.people = id;
303
+ } else {
304
+ params[side] = toEntityIri(id, 'people');
305
+ }
306
+
199
307
  if (String(search || '').trim()) {
200
308
  params.search = String(search).trim();
201
309
  }
@@ -221,8 +329,15 @@ const fetchFranchiseCompaniesFromLinks = async ({
221
329
  const pageSize = normalizeItemsPerPage(itemsPerPage);
222
330
  const byId = new Map();
223
331
 
224
- for (const side of ['company', 'people']) {
332
+ // Primary path matches FranchiseLinksTab: company=<id>&linkType[]=franchisee
333
+ // Fallback dual-side only if no authorized company-side link is returned
334
+ // (inverted associations). The API response is filtered again below because
335
+ // authorization cannot depend on a query parameter being honored remotely.
336
+ const sidesToTry = ['company', 'people'];
337
+
338
+ for (const side of sidesToTry) {
225
339
  let page = 1;
340
+ let gotAuthorizedAny = false;
226
341
  while (true) {
227
342
  const links = await fetchFranchiseLinksPage({
228
343
  companyId: viewerId,
@@ -232,13 +347,13 @@ const fetchFranchiseCompaniesFromLinks = async ({
232
347
  search,
233
348
  });
234
349
  const pageLinks = Array.isArray(links) ? links : [];
235
-
236
350
  pageLinks.forEach(link => {
237
351
  const franchise = extractFranchiseCompanyFromLink(link, viewerId);
238
352
  const franchiseId = normalizeShopEntityId(franchise);
239
353
  if (!franchiseId || franchiseId === viewerId) {
240
354
  return;
241
355
  }
356
+ gotAuthorizedAny = true;
242
357
  if (!byId.has(franchiseId)) {
243
358
  byId.set(
244
359
  franchiseId,
@@ -254,6 +369,12 @@ const fetchFranchiseCompaniesFromLinks = async ({
254
369
  }
255
370
  page += 1;
256
371
  }
372
+
373
+ // If company-side returned an authorized franchise, skip people-side
374
+ // (avoids duplicate requests while preserving the inverted-link fallback).
375
+ if (side === 'company' && gotAuthorizedAny) {
376
+ break;
377
+ }
257
378
  }
258
379
 
259
380
  return Array.from(byId.values())
@@ -344,11 +465,45 @@ export const fetchAllShopFranchiseDirectory = async ({
344
465
  );
345
466
  }
346
467
 
347
- return fetchFranchiseCompaniesFromLinks({
468
+ const linkedCompanies = await fetchFranchiseCompaniesFromLinks({
348
469
  companyId,
349
470
  search,
350
471
  itemsPerPage,
351
472
  });
473
+ // Hydrate enable from /people/{id} when people_links embed omits the flag (#815)
474
+ const companies = (await hydratePeopleEnableFlags(linkedCompanies)).filter(
475
+ isPeopleEnabled,
476
+ );
477
+
478
+ // Always load addresses from /addresses so lat/long (and map.*) are present.
479
+ // people_link embeds are often stubs without coordinates.
480
+ const enriched = await Promise.all(
481
+ companies.map(async company => {
482
+ const peopleId = normalizeShopEntityId(company);
483
+ if (!peopleId) {
484
+ return normalizeFranchiseDirectoryItem(company);
485
+ }
486
+ try {
487
+ const addresses = await fetchShopFranchiseAddresses({peopleId});
488
+ if (Array.isArray(addresses) && addresses.length > 0) {
489
+ return normalizeFranchiseDirectoryItem({
490
+ ...company,
491
+ shopAddresses: addresses,
492
+ });
493
+ }
494
+ } catch {
495
+ // fall through to embedded addresses
496
+ }
497
+ return normalizeFranchiseDirectoryItem(company);
498
+ }),
499
+ );
500
+
501
+ return enriched.sort((left, right) =>
502
+ sortByLabel(
503
+ resolveFranchiseCompanyLabel(left),
504
+ resolveFranchiseCompanyLabel(right),
505
+ ),
506
+ );
352
507
  };
353
508
 
354
509
  export const fetchShopFranchiseAddresses = async ({