@controleonline/ui-common 1.2.87 → 1.2.88
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.
- package/.github/workflows/integration-source-gate.yml +68 -0
- package/.github/workflows/manager-worker.yml +7 -156
- package/.github/workflows/publish-npm.yml +73 -27
- package/.github/workflows/pull-request-checks.yml +7 -5
- package/docs/technical/Device-Detail-Alias-UI-Sync.md +141 -0
- package/package.json +13 -8
- package/src/react/components/BottomNavigationBar.js +178 -174
- package/src/react/components/BottomNavigationBar.styles.js +139 -118
- package/src/react/components/DefaultProvider.native.js +427 -421
- package/src/react/components/DefaultProvider.web.js +131 -125
- package/src/react/components/RemoteCheckoutService.js +3 -3
- package/src/react/components/RuntimeFooterMarqueeText.js +88 -36
- package/src/react/components/RuntimeInfoFooter.js +76 -43
- package/src/react/components/RuntimeInfoFooter.styles.js +22 -14
- package/src/react/config/deviceConfigBootstrap.js +3 -3
- package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +4 -22
- package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +6 -2
- package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +4 -0
- package/src/react/pages/Devices/detail/DeviceDetailScreen.js +23 -338
- package/src/react/pages/Devices/detail/createDeviceDetailContext.js +20 -0
- package/src/react/pages/Devices/detail/useDeviceDetailActions.js +11 -36
- package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +129 -37
- package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +11 -38
- package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +9 -28
- package/src/react/pages/IntegrationConfigFields.js +1 -0
- package/src/react/pages/IntegrationConfigPage.js +101 -45
- package/src/react/pages/IntegrationConfigPage.utils.js +21 -13
- package/src/react/pages/fetchPeopleConfigs.js +21 -0
- package/src/react/pages/integrationsCatalog.js +15 -110
- package/src/react/services/Cielo/Cielo.js +9 -9
- package/src/react/services/Getnet/Getnet.web.js +25 -0
- package/src/react/utils/companyAuthority.js +36 -0
- package/src/react/utils/fileUrl.js +14 -5
- package/src/react/utils/paymentDevices.js +375 -270
- package/src/react/utils/printerDevices.js +2 -2
- package/src/react/utils/runtimeFooter.js +178 -224
- package/src/react/utils/runtimeFooterText.js +160 -0
- package/src/react/utils/runtimeLanguage.js +5 -5
- package/src/react/utils/runtimeZoom.js +29 -5
- package/src/react/utils/shopFranchises.js +167 -12
- package/src/react/utils/storeColumns.js +235 -217
- package/src/react/utils/systemErrorMessage.js +2 -1
- package/src/store/integration/index.js +43 -0
- package/src/tests/browser/manager/device-detail-apply-config-defined.spec.js +31 -0
- package/src/tests/browser/manager/device-detail-composition.spec.js +295 -0
- package/src/tests/browser/manager/device-detail-copy-config.spec.js +11 -1
- package/src/tests/browser/manager/device-detail-pdv-current-company.spec.js +273 -0
- package/src/tests/browser/manager/runtime-footer-rotation.spec.js +397 -0
- package/src/tests/react/components/DefaultProviderAuthority.test.js +28 -0
- package/src/tests/react/components/RuntimeFooterMarqueeText.build.test.js +29 -0
- package/src/tests/react/config/deviceConfigBootstrap.test.js +24 -1
- package/src/tests/react/pages/applyCurrentDeviceConfig.test.js +85 -0
- package/src/tests/react/pages/deviceDetailComposition.test.js +65 -0
- package/src/tests/react/pages/deviceDetailHookDependencies.test.mjs +31 -0
- package/src/tests/react/pages/deviceDetailLoaderDependencies.test.mjs +28 -0
- package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +27 -6
- package/src/tests/react/services/Cielo.test.js +2 -2
- package/src/tests/react/utils/companyAuthority.test.js +53 -0
- package/src/tests/react/utils/formatter.test.mjs +6 -0
- package/src/tests/react/utils/paymentDevices.test.js +72 -0
- package/src/tests/react/utils/runtimeFooter.test.js +35 -0
- package/src/tests/react/utils/runtimeFooterText.test.js +88 -0
- package/src/tests/react/utils/runtimeLanguage.test.js +4 -4
- package/src/tests/react/utils/runtimeZoom.test.js +20 -0
- package/src/tests/react/utils/shopFranchises.test.js +32 -13
- package/src/tests/react/utils/translate.test.mjs +6 -6
- package/src/utils/formatter.js +7 -3
- package/src/utils/translate.js +16 -16
- package/src/utils/translatePending.js +6 -6
- 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 = (
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ({
|