@controleonline/ui-common 1.2.98 → 1.2.99
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/package.json +1 -1
- package/src/api/index.js +40 -54
- package/src/react/components/AnimatedModal.js +6 -1
- package/src/react/components/BottomNavigationBar.js +19 -8
- package/src/react/components/BottomNavigationBar.soft.styles.js +29 -0
- package/src/react/components/DefaultProvider.styles.js +4 -0
- package/src/react/components/DefaultProvider.web.js +6 -3
- package/src/react/components/ProductCatalogCacheService.js +53 -45
- package/src/react/components/RuntimeBottomNavigationBar.js +12 -3
- package/src/react/components/RuntimeInfoFooter.home.spacing.js +5 -0
- package/src/react/components/RuntimeInfoFooter.js +8 -2
- package/src/react/components/UserAvatar.js +5 -14
- package/src/react/hooks/useBottomNavigationPreference.js +49 -0
- package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +6 -4
- package/src/react/pages/Devices/deviceTypes/shared.js +9 -4
- package/src/react/pages/Devices.js +184 -183
- package/src/react/utils/productCatalogCacheRevision.js +13 -0
- package/src/tests/react/utils/storedSessionToken.test.js +22 -0
- package/src/utils/storedSessionToken.js +18 -0
package/package.json
CHANGED
package/src/api/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {readStoredSessionToken} from '@controleonline/ui-common/src/utils/storedSessionToken';
|
|
1
2
|
import myFetch from '@controleonline/ui-common/src/api/fetch';
|
|
2
3
|
import axios from 'axios';
|
|
3
4
|
import {env as APP_ENV} from '@env';
|
|
@@ -215,8 +216,8 @@ async function requestSmokeUrl(url, options) {
|
|
|
215
216
|
export const api = {
|
|
216
217
|
device: JSON.parse(localStorage.getItem('device') || '{}'),
|
|
217
218
|
masterDevice: JSON.parse(localStorage.getItem('master-device') || '{}'),
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
|
|
220
|
+
|
|
220
221
|
upload: async function (uri, formData) {
|
|
221
222
|
const token = await this.getToken();
|
|
222
223
|
const appDomain = resolveAppDomain(APP_ENV.DOMAIN);
|
|
@@ -242,8 +243,8 @@ export const api = {
|
|
|
242
243
|
fetch: async function (uri, options = {}) {
|
|
243
244
|
if (typeof options.headers === 'undefined')
|
|
244
245
|
Object.assign(options, { headers: new Headers() });
|
|
245
|
-
|
|
246
|
-
// Always refresh device from localStorage in case it was set after module load
|
|
246
|
+
|
|
247
|
+
// Always refresh device from localStorage in case it was set after module load
|
|
247
248
|
try {
|
|
248
249
|
this.device = JSON.parse(localStorage.getItem('device') || '{}');
|
|
249
250
|
} catch (e) {
|
|
@@ -270,16 +271,16 @@ export const api = {
|
|
|
270
271
|
if (options.body) {
|
|
271
272
|
if (typeof options.body === 'object') {
|
|
272
273
|
bodyDeviceId = options.body.device || null;
|
|
273
|
-
} else if (typeof options.body === 'string') {
|
|
274
|
-
try {
|
|
275
|
-
const parsedBody = JSON.parse(options.body);
|
|
276
|
-
bodyDeviceId = parsedBody?.device || null;
|
|
277
|
-
} catch (e) {
|
|
278
|
-
bodyDeviceId = null;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
274
|
+
} else if (typeof options.body === 'string') {
|
|
275
|
+
try {
|
|
276
|
+
const parsedBody = JSON.parse(options.body);
|
|
277
|
+
bodyDeviceId = parsedBody?.device || null;
|
|
278
|
+
} catch (e) {
|
|
279
|
+
bodyDeviceId = null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
283
284
|
const requestDevice = resolveRequestDevice(
|
|
284
285
|
this.masterDevice,
|
|
285
286
|
this.device,
|
|
@@ -296,10 +297,10 @@ export const api = {
|
|
|
296
297
|
options.headers.set('DEVICE-TYPE', requestDeviceType);
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
|
-
if (options.responseType != 'text') {
|
|
300
|
-
options.headers.set('Content-Type', MIME_TYPE);
|
|
301
|
-
options.headers.set('Accept', MIME_TYPE);
|
|
302
|
-
}
|
|
300
|
+
if (options.responseType != 'text') {
|
|
301
|
+
options.headers.set('Content-Type', MIME_TYPE);
|
|
302
|
+
options.headers.set('Accept', MIME_TYPE);
|
|
303
|
+
}
|
|
303
304
|
options.headers.set('App-Domain', resolveAppDomain(APP_ENV.DOMAIN));
|
|
304
305
|
|
|
305
306
|
if (options.body && typeof options.body != 'string') {
|
|
@@ -310,28 +311,13 @@ export const api = {
|
|
|
310
311
|
if (options.params) {
|
|
311
312
|
uri = this.buildQueryString(uri, options);
|
|
312
313
|
}
|
|
313
|
-
return myFetch(uri, options).catch(e => {
|
|
314
|
-
throw e;
|
|
315
|
-
});
|
|
316
|
-
},
|
|
317
|
-
async getToken() {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (sessionString) {
|
|
321
|
-
try {
|
|
322
|
-
const cleanString =
|
|
323
|
-
typeof sessionString == 'string' &&
|
|
324
|
-
sessionString.startsWith('__q_objt|')
|
|
325
|
-
? sessionString.substring('__q_objt|'.length)
|
|
326
|
-
: sessionString;
|
|
327
|
-
session = JSON.parse(cleanString); // Transforma a string em objeto
|
|
328
|
-
} catch (e) {
|
|
329
|
-
console.error('Failed to parse session from localStorage', e);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return session?.token || session?.api_key;
|
|
334
|
-
},
|
|
314
|
+
return myFetch(uri, options).catch(e => {
|
|
315
|
+
throw e;
|
|
316
|
+
});
|
|
317
|
+
},
|
|
318
|
+
async getToken() {
|
|
319
|
+
return readStoredSessionToken();
|
|
320
|
+
},
|
|
335
321
|
serialize(obj, prefix = '') {
|
|
336
322
|
const pairs = [];
|
|
337
323
|
|
|
@@ -344,12 +330,12 @@ export const api = {
|
|
|
344
330
|
|
|
345
331
|
const value = obj[key];
|
|
346
332
|
let fullKey = prefix ? `${prefix}.${key}` : key; // Usa ponto para objetos
|
|
347
|
-
|
|
348
|
-
if (value === null || value === undefined) {
|
|
349
|
-
pairs.push(`${fullKey}=`);
|
|
350
|
-
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
351
|
-
// Objeto aninhado: chama serialize recursivamente com ponto
|
|
352
|
-
pairs.push(...this.serialize(value, fullKey));
|
|
333
|
+
|
|
334
|
+
if (value === null || value === undefined) {
|
|
335
|
+
pairs.push(`${fullKey}=`);
|
|
336
|
+
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
337
|
+
// Objeto aninhado: chama serialize recursivamente com ponto
|
|
338
|
+
pairs.push(...this.serialize(value, fullKey));
|
|
353
339
|
} else if (Array.isArray(value)) {
|
|
354
340
|
// Array: adiciona [] e itera sobre os valores
|
|
355
341
|
if (value.length === 0) {
|
|
@@ -364,14 +350,14 @@ export const api = {
|
|
|
364
350
|
});
|
|
365
351
|
}
|
|
366
352
|
} else {
|
|
367
|
-
// Valor simples
|
|
368
|
-
pairs.push(`${fullKey}=${encodeURIComponent(value)}`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return pairs;
|
|
373
|
-
},
|
|
374
|
-
|
|
353
|
+
// Valor simples
|
|
354
|
+
pairs.push(`${fullKey}=${encodeURIComponent(value)}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return pairs;
|
|
359
|
+
},
|
|
360
|
+
|
|
375
361
|
buildQueryString(uri, options) {
|
|
376
362
|
const safeUri = stripDomainQueryParams(uri);
|
|
377
363
|
|
|
@@ -38,6 +38,8 @@ export default function AnimatedModal({
|
|
|
38
38
|
onRequestClose,
|
|
39
39
|
children,
|
|
40
40
|
style,
|
|
41
|
+
overlayColor,
|
|
42
|
+
contentStyle,
|
|
41
43
|
}) {
|
|
42
44
|
const [showing, setShowing] = useState(visible);
|
|
43
45
|
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
@@ -125,6 +127,8 @@ export default function AnimatedModal({
|
|
|
125
127
|
const overlayStyle = [
|
|
126
128
|
styles.overlay,
|
|
127
129
|
style,
|
|
130
|
+
overlayColor,
|
|
131
|
+
contentStyle,
|
|
128
132
|
Platform.OS === 'android' ? {paddingBottom: keyboardHeight} : null,
|
|
129
133
|
];
|
|
130
134
|
|
|
@@ -152,11 +156,12 @@ export default function AnimatedModal({
|
|
|
152
156
|
style={[
|
|
153
157
|
StyleSheet.absoluteFillObject,
|
|
154
158
|
styles.overlayBg,
|
|
159
|
+
overlayColor ? {backgroundColor: overlayColor} : null,
|
|
155
160
|
{opacity: overlayOpacity},
|
|
156
161
|
]}
|
|
157
162
|
/>
|
|
158
163
|
<Animated.View
|
|
159
|
-
style={[styles.content, {transform: [{translateY: slideY}]}]}>
|
|
164
|
+
style={[styles.content, contentStyle, {transform: [{translateY: slideY}]}]}>
|
|
160
165
|
{normalizedChildren}
|
|
161
166
|
</Animated.View>
|
|
162
167
|
<Toasts
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import React, {useLayoutEffect, useMemo} from 'react';
|
|
2
2
|
import {Pressable, Text, View} from 'react-native';
|
|
3
|
-
import
|
|
3
|
+
import LegacyIcon from 'react-native-vector-icons/Feather';
|
|
4
|
+
import HomeIcon from '@controleonline/../../src/components/HomeIcon';
|
|
4
5
|
import {useTheme} from './DefaultProvider';
|
|
5
6
|
import RuntimeInfoFooter from './RuntimeInfoFooter';
|
|
6
7
|
import {resolveMenuRouteParams} from '../utils/menuNavigation';
|
|
7
8
|
import createStyles from './BottomNavigationBar.styles';
|
|
9
|
+
import softenStyles from './BottomNavigationBar.soft.styles';
|
|
10
|
+
import {resolveHomepagePalette} from '@controleonline/../../src/styles/homepagePalette';
|
|
8
11
|
|
|
9
12
|
const BottomNavigationBar = ({
|
|
10
13
|
navigation,
|
|
@@ -14,7 +17,11 @@ const BottomNavigationBar = ({
|
|
|
14
17
|
colors = {},
|
|
15
18
|
testID = 'bottom-navigation',
|
|
16
19
|
useModernWebChromeProps = false,
|
|
20
|
+
appearance = 'soft',
|
|
21
|
+
placement = 'overlay',
|
|
17
22
|
}) => {
|
|
23
|
+
const Icon = appearance === 'soft' ? HomeIcon : LegacyIcon;
|
|
24
|
+
colors = appearance === 'soft' ? resolveHomepagePalette(colors) : colors;
|
|
18
25
|
const theme = useTheme?.() || {};
|
|
19
26
|
const runtimeFooter = theme?.runtimeFooter || null;
|
|
20
27
|
const registerBottomNavigation =
|
|
@@ -31,14 +38,14 @@ const BottomNavigationBar = ({
|
|
|
31
38
|
|
|
32
39
|
const styles = useMemo(
|
|
33
40
|
() =>
|
|
34
|
-
createStyles({
|
|
41
|
+
softenStyles(createStyles({
|
|
35
42
|
activeBackground,
|
|
36
43
|
dockBackground,
|
|
37
44
|
dockBorder,
|
|
38
45
|
dockShadow,
|
|
39
46
|
activeBorder,
|
|
40
47
|
useModernWebChromeProps,
|
|
41
|
-
}),
|
|
48
|
+
}), colors, appearance),
|
|
42
49
|
[
|
|
43
50
|
activeBackground,
|
|
44
51
|
activeBorder,
|
|
@@ -46,6 +53,7 @@ const BottomNavigationBar = ({
|
|
|
46
53
|
dockBorder,
|
|
47
54
|
dockShadow,
|
|
48
55
|
useModernWebChromeProps,
|
|
56
|
+
appearance,
|
|
49
57
|
],
|
|
50
58
|
);
|
|
51
59
|
|
|
@@ -97,9 +105,7 @@ const BottomNavigationBar = ({
|
|
|
97
105
|
|
|
98
106
|
const routeItems = Array.isArray(items) ? items : [];
|
|
99
107
|
const knownRoute = routeItems.some(item => item?.route === activeRouteName);
|
|
100
|
-
const effectiveActiveRoute = knownRoute
|
|
101
|
-
? activeRouteName
|
|
102
|
-
: routeItems[0]?.route || '';
|
|
108
|
+
const effectiveActiveRoute = knownRoute ? activeRouteName : '';
|
|
103
109
|
|
|
104
110
|
const navigateTo = item => {
|
|
105
111
|
try {
|
|
@@ -116,15 +122,18 @@ const BottomNavigationBar = ({
|
|
|
116
122
|
? [styles.host, styles.hostPointerEventsBoxNone]
|
|
117
123
|
: styles.host;
|
|
118
124
|
const footerProps = {
|
|
125
|
+
appearance,
|
|
119
126
|
appVersion: runtimeFooter?.appVersion,
|
|
120
|
-
colors:
|
|
127
|
+
colors: appearance === 'soft'
|
|
128
|
+
? {...runtimeFooter?.colors, footerBackground: colors.footerBackground, footerText: colors.footerText}
|
|
129
|
+
: runtimeFooter?.colors || {},
|
|
121
130
|
mainCompany: runtimeFooter?.mainCompany,
|
|
122
131
|
device: runtimeFooter?.device,
|
|
123
132
|
useModernWebChromeProps,
|
|
124
133
|
};
|
|
125
134
|
|
|
126
135
|
return (
|
|
127
|
-
<View {...hostProps} style={hostStyle}>
|
|
136
|
+
<View {...hostProps} style={[hostStyle, placement === 'inline' ? {position: 'relative', flexShrink: 0, zIndex: 0} : null]}>
|
|
128
137
|
<View style={styles.stack}>
|
|
129
138
|
<View style={styles.dock} testID={testID}>
|
|
130
139
|
<View style={styles.itemsRow}>
|
|
@@ -141,6 +150,8 @@ const BottomNavigationBar = ({
|
|
|
141
150
|
<Pressable
|
|
142
151
|
key={item.route}
|
|
143
152
|
accessibilityRole="button"
|
|
153
|
+
accessibilityLabel={item.label}
|
|
154
|
+
accessibilityState={{selected: isActive, disabled: Boolean(isDisabled)}}
|
|
144
155
|
disabled={isDisabled}
|
|
145
156
|
onPress={() => navigateTo(item)}
|
|
146
157
|
style={({pressed}) => [
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {Platform} from 'react-native';
|
|
2
|
+
|
|
3
|
+
// Shared compact chrome on every route; explicit legacy consumers can opt out.
|
|
4
|
+
export default (styles, colors, appearance) => appearance !== 'soft' ? styles : {
|
|
5
|
+
...styles,
|
|
6
|
+
dock: [styles.dock, {
|
|
7
|
+
paddingHorizontal: 0,
|
|
8
|
+
paddingTop: 0,
|
|
9
|
+
paddingBottom: 0,
|
|
10
|
+
borderWidth: 0,
|
|
11
|
+
borderTopWidth: 1,
|
|
12
|
+
borderColor: colors.navigationBorder,
|
|
13
|
+
borderTopLeftRadius: 0,
|
|
14
|
+
borderTopRightRadius: 0,
|
|
15
|
+
elevation: 0,
|
|
16
|
+
shadowOpacity: 0,
|
|
17
|
+
...(Platform.OS === 'web' ? {boxShadow: 'none'} : {}),
|
|
18
|
+
}],
|
|
19
|
+
itemsRow: [styles.itemsRow, {minHeight: 56, paddingHorizontal: 8, paddingVertical: 4}],
|
|
20
|
+
item: [styles.item, {minHeight: 46, paddingVertical: 3, borderRadius: 10}],
|
|
21
|
+
iconWrap: [styles.iconWrap, {width: 22, height: 22, marginBottom: 2}],
|
|
22
|
+
itemLabel: [styles.itemLabel, {fontSize: 11, lineHeight: 14, fontWeight: '400'}],
|
|
23
|
+
footerSlot: [styles.footerSlot, {
|
|
24
|
+
borderTopWidth: 1,
|
|
25
|
+
borderColor: colors.navigationBorder,
|
|
26
|
+
paddingHorizontal: 0,
|
|
27
|
+
minHeight: 28,
|
|
28
|
+
}],
|
|
29
|
+
};
|
|
@@ -395,15 +395,18 @@ export const DefaultProvider = ({
|
|
|
395
395
|
|
|
396
396
|
useWebThemePalette({resolveDomainThemeColors, baseThemeColors, mainCompany, resolveThemePalette, runtimeColors, applyPaletteToRuntimeColors, applyThemeCssVariables, actions, currentCompany, isShopClientApp});
|
|
397
397
|
|
|
398
|
-
|
|
399
|
-
return <View style={providerStyles.loadingContainer} />;
|
|
400
|
-
}
|
|
398
|
+
const translationPending = requiresTranslateBootstrap && !isTranslateBootstrapReady;
|
|
401
399
|
|
|
402
400
|
return (
|
|
403
401
|
<ThemeContext.Provider value={themeContextValue}>
|
|
404
402
|
<View
|
|
403
|
+
pointerEvents={translationPending ? 'none' : 'auto'}
|
|
404
|
+
aria-hidden={translationPending}
|
|
405
405
|
style={[
|
|
406
406
|
providerStyles.shell,
|
|
407
|
+
// Keep navigation mounted while translations load. Unmounting it
|
|
408
|
+
// resets the login route and repeatedly registers the bottom bar.
|
|
409
|
+
translationPending && providerStyles.translationPending,
|
|
407
410
|
{
|
|
408
411
|
backgroundColor: colors?.background || runtimeColors.background,
|
|
409
412
|
},
|
|
@@ -1,45 +1,53 @@
|
|
|
1
|
-
import React, {useEffect} from 'react';
|
|
2
|
-
import {useStore} from '@store';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
messages
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
categoryActions.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
import React, {useEffect} from 'react';
|
|
2
|
+
import {useStore} from '@store';
|
|
3
|
+
import {invalidateProductCatalogCache} from '../utils/productCatalogCacheRevision';
|
|
4
|
+
|
|
5
|
+
const ProductCatalogCacheService = () => {
|
|
6
|
+
const categoriesStore = useStore('categories');
|
|
7
|
+
const productsStore = useStore('products');
|
|
8
|
+
|
|
9
|
+
const categoryActions = categoriesStore.actions;
|
|
10
|
+
const productActions = productsStore.actions;
|
|
11
|
+
|
|
12
|
+
const {messages, message} = categoriesStore.getters;
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (
|
|
16
|
+
messages &&
|
|
17
|
+
messages.length > 0 &&
|
|
18
|
+
(!message || Object.keys(message).length === 0)
|
|
19
|
+
) {
|
|
20
|
+
const nextMessages = [...messages];
|
|
21
|
+
categoryActions.setMessage(nextMessages.pop());
|
|
22
|
+
categoryActions.setMessages(nextMessages);
|
|
23
|
+
}
|
|
24
|
+
}, [categoryActions, message, messages]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!message || Object.keys(message).length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (message?.command !== 'clear-product-cache') {
|
|
32
|
+
categoryActions.setMessage(null);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
invalidateProductCatalogCache();
|
|
37
|
+
for (const context of ['products', 'supplies']) {
|
|
38
|
+
localStorage.removeItem(`categories:${context}`);
|
|
39
|
+
localStorage.removeItem(`categories-company:${context}`);
|
|
40
|
+
}
|
|
41
|
+
localStorage.setItem('categories', JSON.stringify([]));
|
|
42
|
+
localStorage.removeItem('categories-company');
|
|
43
|
+
categoryActions.setItems([]);
|
|
44
|
+
productActions.setItems([]);
|
|
45
|
+
categoryActions.setReload?.(true);
|
|
46
|
+
productActions.setReload?.(true);
|
|
47
|
+
categoryActions.setMessage(null);
|
|
48
|
+
}, [categoryActions, message, productActions]);
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default ProductCatalogCacheService;
|
|
@@ -19,6 +19,8 @@ const {
|
|
|
19
19
|
} = require('@controleonline/ui-common/src/react/utils/bottomNavigationToolbar');
|
|
20
20
|
|
|
21
21
|
const RuntimeBottomNavigationBar = ({
|
|
22
|
+
appearance = 'soft',
|
|
23
|
+
placement = 'overlay',
|
|
22
24
|
activeRouteName: activeRouteNameProp,
|
|
23
25
|
colors: colorsOverride,
|
|
24
26
|
disabled: disabledOverride,
|
|
@@ -41,13 +43,16 @@ const RuntimeBottomNavigationBar = ({
|
|
|
41
43
|
const peopleGetters = peopleStore?.getters || {};
|
|
42
44
|
const currentCompany = peopleGetters.currentCompany || {};
|
|
43
45
|
const themeColors = themeGetters.colors || {};
|
|
44
|
-
const [
|
|
46
|
+
const [menuState, setMenuState] = useState({scope: '', menus: []});
|
|
45
47
|
const preset = presetKey ? getBottomNavigationPreset(presetKey) : null;
|
|
46
48
|
const routeAliases = routeAliasesProp || preset?.routeAliases || {};
|
|
47
49
|
const appType = normalizeAppType(app_type);
|
|
50
|
+
const menuScope = JSON.stringify([appType, currentCompany?.id, menuType, presetKey]);
|
|
51
|
+
const runtimeMenus = menuState.scope === menuScope ? menuState.menus : [];
|
|
48
52
|
|
|
49
53
|
// Always consult menus-people for role-aware toolbar; preset is fallback only.
|
|
50
54
|
useEffect(() => {
|
|
55
|
+
const setRuntimeMenus = menus => setMenuState({scope: menuScope, menus});
|
|
51
56
|
if (!currentCompany?.id || !menuType) {
|
|
52
57
|
setRuntimeMenus(
|
|
53
58
|
resolveToolbarRuntimeMenus({
|
|
@@ -104,7 +109,7 @@ const RuntimeBottomNavigationBar = ({
|
|
|
104
109
|
return () => {
|
|
105
110
|
cancelled = true;
|
|
106
111
|
};
|
|
107
|
-
}, [appType, currentCompany?.id, menuType, preset, presetKey]);
|
|
112
|
+
}, [appType, currentCompany?.id, menuType, preset, presetKey, menuScope]);
|
|
108
113
|
|
|
109
114
|
const colors = useMemo(() => {
|
|
110
115
|
if (colorsOverride) {
|
|
@@ -150,15 +155,19 @@ const RuntimeBottomNavigationBar = ({
|
|
|
150
155
|
const knownRoute = navItems.some(item => item.route === resolvedActiveRoute);
|
|
151
156
|
const effectiveActiveRoute = knownRoute
|
|
152
157
|
? resolvedActiveRoute
|
|
153
|
-
:
|
|
158
|
+
: activeRouteName;
|
|
154
159
|
|
|
155
160
|
const isDisabled =
|
|
156
161
|
typeof disabledOverride === 'boolean'
|
|
157
162
|
? disabledOverride
|
|
158
163
|
: !currentCompany || Object.keys(currentCompany).length === 0;
|
|
159
164
|
|
|
165
|
+
if (!navItems.length) return null;
|
|
166
|
+
|
|
160
167
|
return (
|
|
161
168
|
<BottomNavigationBar
|
|
169
|
+
appearance={appearance}
|
|
170
|
+
placement={placement}
|
|
162
171
|
activeRouteName={effectiveActiveRoute}
|
|
163
172
|
colors={colors}
|
|
164
173
|
disabled={isDisabled}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '@controleonline/ui-common/src/react/utils/runtimeFooter';
|
|
23
23
|
import styles from './RuntimeInfoFooter.styles';
|
|
24
24
|
import RuntimeFooterMarqueeText from './RuntimeFooterMarqueeText';
|
|
25
|
+
import resolveHomeFooterSpacing from './RuntimeInfoFooter.home.spacing';
|
|
25
26
|
|
|
26
27
|
const ROTATION_INTERVAL_MS = 4000;
|
|
27
28
|
const COMPACT_BREAKPOINT = 720;
|
|
@@ -66,6 +67,7 @@ const RuntimeInfoFooter = ({
|
|
|
66
67
|
device,
|
|
67
68
|
colors,
|
|
68
69
|
useModernWebChromeProps = false,
|
|
70
|
+
appearance = 'legacy',
|
|
69
71
|
}) => {
|
|
70
72
|
const {width} = useWindowDimensions();
|
|
71
73
|
const insets = useSafeAreaInsets();
|
|
@@ -258,6 +260,7 @@ const RuntimeInfoFooter = ({
|
|
|
258
260
|
: inlineText
|
|
259
261
|
) || primaryText || device?.id || '';
|
|
260
262
|
const backgroundColor = colors?.footerBackground;
|
|
263
|
+
const homeSpacing = resolveHomeFooterSpacing(appearance, Platform.OS, insets.bottom);
|
|
261
264
|
const borderColor = colors?.footerBorder;
|
|
262
265
|
const textColor = resolveVisibleFooterTextColor(colors);
|
|
263
266
|
const loadingColor = colors?.footerLink || colors?.primary || textColor;
|
|
@@ -267,7 +270,9 @@ const RuntimeInfoFooter = ({
|
|
|
267
270
|
: styles.shell;
|
|
268
271
|
|
|
269
272
|
return (
|
|
270
|
-
<View {...shellProps} style={shellStyle
|
|
273
|
+
<View {...shellProps} style={[shellStyle, homeSpacing && {
|
|
274
|
+
paddingBottom: homeSpacing.safeAreaBottom, backgroundColor,
|
|
275
|
+
}]}>
|
|
271
276
|
<View
|
|
272
277
|
testID="runtime-info-footer"
|
|
273
278
|
style={[
|
|
@@ -276,7 +281,8 @@ const RuntimeInfoFooter = ({
|
|
|
276
281
|
{
|
|
277
282
|
backgroundColor,
|
|
278
283
|
borderTopColor: borderColor,
|
|
279
|
-
paddingBottom: bottomInset,
|
|
284
|
+
paddingBottom: homeSpacing ? homeSpacing.contentPadding : bottomInset,
|
|
285
|
+
...(homeSpacing && !showDebugInfo ? {paddingTop: homeSpacing.contentPadding} : {}),
|
|
280
286
|
},
|
|
281
287
|
]}>
|
|
282
288
|
<View style={styles.primaryRow}>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {readStoredSessionToken} from '../../utils/storedSessionToken';
|
|
1
2
|
import React, {useEffect, useMemo, useState} from 'react';
|
|
2
3
|
import {Image, Platform, StyleSheet, Text, View} from 'react-native';
|
|
3
4
|
import {env as APP_ENV} from '@env';
|
|
@@ -14,18 +15,6 @@ const isBackendDownloadUrl = uri =>
|
|
|
14
15
|
/** Session cache: gravatar URL → true (ok) | false (missing). Avoids repeat probes. */
|
|
15
16
|
const gravatarProbeCache = new Map();
|
|
16
17
|
|
|
17
|
-
const readSessionToken = () => {
|
|
18
|
-
if (typeof localStorage === 'undefined' || !localStorage?.getItem) {
|
|
19
|
-
return '';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const session = JSON.parse(localStorage.getItem('session') || '{}');
|
|
24
|
-
return normalizeUrl(session?.api_key || session?.token);
|
|
25
|
-
} catch {
|
|
26
|
-
return '';
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
18
|
|
|
30
19
|
/**
|
|
31
20
|
* Probe Gravatar (d=404) via fetch BEFORE exposing URL to <Image>.
|
|
@@ -86,7 +75,7 @@ const useDisplayUri = uri => {
|
|
|
86
75
|
return;
|
|
87
76
|
}
|
|
88
77
|
|
|
89
|
-
const token =
|
|
78
|
+
const token = readStoredSessionToken();
|
|
90
79
|
|
|
91
80
|
// Authenticated backend download on web
|
|
92
81
|
if (isBackendDownloadUrl(next) && Platform.OS === 'web' && token) {
|
|
@@ -156,6 +145,7 @@ const useDisplayUri = uri => {
|
|
|
156
145
|
|
|
157
146
|
const UserAvatar = ({
|
|
158
147
|
imageUrl,
|
|
148
|
+
imageHeaders,
|
|
159
149
|
email,
|
|
160
150
|
name,
|
|
161
151
|
size = 40,
|
|
@@ -215,7 +205,8 @@ const UserAvatar = ({
|
|
|
215
205
|
<View style={containerStyle}>
|
|
216
206
|
{displayUri ? (
|
|
217
207
|
<Image
|
|
218
|
-
source={
|
|
208
|
+
source={imageHeaders && Platform.OS !== 'web' && displayUri === imageUrl
|
|
209
|
+
? {uri: displayUri, headers: imageHeaders} : {uri: displayUri}}
|
|
219
210
|
style={styles.image}
|
|
220
211
|
onError={() => setSourceIndex(index => index + 1)}
|
|
221
212
|
/>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import {Platform} from 'react-native';
|
|
3
|
+
|
|
4
|
+
const nativeStorage = () => {
|
|
5
|
+
const module = require('@react-native-async-storage/async-storage');
|
|
6
|
+
return module.default || module;
|
|
7
|
+
};
|
|
8
|
+
const deviceStorage = {
|
|
9
|
+
getItem: key => Platform.OS === 'web' ? globalThis.localStorage?.getItem(key) : nativeStorage().getItem(key),
|
|
10
|
+
setItem: (key, value) => Platform.OS === 'web' ? globalThis.localStorage.setItem(key, value) : nativeStorage().setItem(key, value),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Visibility is personal device state. It never changes server menu permissions.
|
|
14
|
+
export default function useBottomNavigationPreference({enabled, principal, appType, storage = deviceStorage}) {
|
|
15
|
+
const key = enabled && principal && appType
|
|
16
|
+
? `co.bottom-navigation.v1:${encodeURIComponent(appType)}:${encodeURIComponent(principal)}` : '';
|
|
17
|
+
const current = useRef(key);
|
|
18
|
+
current.current = key;
|
|
19
|
+
const [state, setState] = useState({key: '', ready: false, remembered: true, session: null, error: ''});
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
current.current = key;
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
setState({key, ready: false, remembered: true, session: null, error: ''});
|
|
24
|
+
if (key) Promise.resolve().then(() => storage.getItem(key)).then(value => {
|
|
25
|
+
if (!cancelled) setState({key, ready: true, remembered: value !== 'hidden', session: null, error: ''});
|
|
26
|
+
}).catch(() => {
|
|
27
|
+
if (!cancelled) setState({key, ready: true, remembered: true, session: null, error: 'Não foi possível recuperar a preferência da barra.'});
|
|
28
|
+
});
|
|
29
|
+
return () => {cancelled = true; if (current.current === key) current.current = null;};
|
|
30
|
+
}, [key, storage]);
|
|
31
|
+
const ready = Boolean(key && state.key === key && state.ready);
|
|
32
|
+
const setSessionVisible = visible => {
|
|
33
|
+
if (ready) setState(previous => ({...previous, session: Boolean(visible), error: ''}));
|
|
34
|
+
};
|
|
35
|
+
const setRememberedVisible = async visible => {
|
|
36
|
+
if (!ready) return false;
|
|
37
|
+
try {
|
|
38
|
+
await storage.setItem(key, visible ? 'visible' : 'hidden');
|
|
39
|
+
if (current.current !== key) return false;
|
|
40
|
+
setState(previous => ({...previous, remembered: Boolean(visible), session: null, error: ''}));
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
if (current.current === key) setState(previous => ({...previous, error: 'Não foi possível lembrar a preferência neste aparelho.'}));
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return {ready, visible: ready && (state.session ?? state.remembered), rememberedVisible: ready ? state.remembered : true,
|
|
48
|
+
error: state.key === key ? state.error : '', setSessionVisible, setRememberedVisible};
|
|
49
|
+
}
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
parseConfigsObject,
|
|
25
25
|
tt,
|
|
26
26
|
} from './deviceListHelpers';
|
|
27
|
-
import
|
|
27
|
+
import legacyStyles from '../../Devices.styles';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Normalize DefaultTable card `item` into a device group shape.
|
|
@@ -65,6 +65,7 @@ const normalizeDeviceGroup = item => {
|
|
|
65
65
|
export default function DeviceGroupCard({
|
|
66
66
|
item,
|
|
67
67
|
brandColors,
|
|
68
|
+
themedStyles = null,
|
|
68
69
|
creatingPdv,
|
|
69
70
|
goToDetail,
|
|
70
71
|
handleCreateCurrentPdv,
|
|
@@ -74,6 +75,7 @@ export default function DeviceGroupCard({
|
|
|
74
75
|
runtimeDeviceType,
|
|
75
76
|
showCurrentPdvSetup,
|
|
76
77
|
}) {
|
|
78
|
+
const styles = themedStyles || legacyStyles;
|
|
77
79
|
const deviceGroup = normalizeDeviceGroup(item);
|
|
78
80
|
if (!deviceGroup) {
|
|
79
81
|
return null;
|
|
@@ -116,7 +118,7 @@ export default function DeviceGroupCard({
|
|
|
116
118
|
deviceGroup.device?.id ||
|
|
117
119
|
deviceGroup.key,
|
|
118
120
|
);
|
|
119
|
-
const accent = getDeviceTypeAccent(normalizedType);
|
|
121
|
+
const accent = brandColors.deviceAccent || getDeviceTypeAccent(normalizedType);
|
|
120
122
|
const metaChips = [];
|
|
121
123
|
|
|
122
124
|
if (isPdv && primaryConfig) {
|
|
@@ -196,7 +198,7 @@ export default function DeviceGroupCard({
|
|
|
196
198
|
<View style={styles.deviceConfigRow}>
|
|
197
199
|
{deviceConfigs.map(deviceConfig => {
|
|
198
200
|
const configType = getDeviceConfigType(deviceConfig);
|
|
199
|
-
const configAccent = getDeviceTypeAccent(configType);
|
|
201
|
+
const configAccent = brandColors.deviceAccent || getDeviceTypeAccent(configType);
|
|
200
202
|
const isSessionConfig =
|
|
201
203
|
isCurrentDevice &&
|
|
202
204
|
runtimeDeviceType &&
|
|
@@ -297,5 +299,5 @@ export default function DeviceGroupCard({
|
|
|
297
299
|
</View>
|
|
298
300
|
</View>
|
|
299
301
|
);
|
|
300
|
-
|
|
302
|
+
|
|
301
303
|
}
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
prioritizeCurrentDeviceGroups,
|
|
37
37
|
readStoredRuntimeDevice,
|
|
38
38
|
} from '../currentDevice';
|
|
39
|
-
import
|
|
39
|
+
import legacyStyles, {createStyles} from '../../Devices.styles';
|
|
40
40
|
import DeviceGroupCard from './DeviceGroupCard';
|
|
41
41
|
import {useDeviceNetworkConnectivity} from './useDeviceNetworkConnectivity';
|
|
42
42
|
import {
|
|
@@ -89,7 +89,7 @@ export const createDeviceTypeTab = ({
|
|
|
89
89
|
clientFilter = null,
|
|
90
90
|
offerCurrentPdvSetup = false,
|
|
91
91
|
}) => {
|
|
92
|
-
const DeviceTypeTab = () => {
|
|
92
|
+
const DeviceTypeTab = ({themePalette = null} = {}) => {
|
|
93
93
|
const navigation = useNavigation();
|
|
94
94
|
const peopleStore = useStore('people');
|
|
95
95
|
const deviceStore = useStore('device');
|
|
@@ -101,13 +101,15 @@ export const createDeviceTypeTab = ({
|
|
|
101
101
|
|
|
102
102
|
const brandColors = useMemo(
|
|
103
103
|
() =>
|
|
104
|
-
resolveThemePalette(
|
|
104
|
+
themePalette || resolveThemePalette(
|
|
105
105
|
{...themeColors, ...(currentCompany?.theme?.colors || {})},
|
|
106
106
|
colors,
|
|
107
107
|
),
|
|
108
|
-
[themeColors, currentCompany?.id],
|
|
108
|
+
[themePalette, themeColors, currentCompany?.id],
|
|
109
109
|
);
|
|
110
110
|
|
|
111
|
+
const styles = useMemo(() => themePalette ? createStyles(brandColors) : legacyStyles, [themePalette, brandColors]);
|
|
112
|
+
|
|
111
113
|
const [deviceConfigs, setDeviceConfigs] = useState([]);
|
|
112
114
|
const [loading, setLoading] = useState(false);
|
|
113
115
|
const [refreshing, setRefreshing] = useState(false);
|
|
@@ -372,6 +374,7 @@ export const createDeviceTypeTab = ({
|
|
|
372
374
|
<DeviceGroupCard
|
|
373
375
|
{...cardProps}
|
|
374
376
|
brandColors={brandColors}
|
|
377
|
+
themedStyles={themePalette ? styles : null}
|
|
375
378
|
creatingPdv={creatingPdv}
|
|
376
379
|
goToDetail={goToDetail}
|
|
377
380
|
handleCreateCurrentPdv={handleCreateCurrentPdv}
|
|
@@ -384,6 +387,8 @@ export const createDeviceTypeTab = ({
|
|
|
384
387
|
),
|
|
385
388
|
[
|
|
386
389
|
brandColors,
|
|
390
|
+
styles,
|
|
391
|
+
themePalette,
|
|
387
392
|
creatingPdv,
|
|
388
393
|
goToDetail,
|
|
389
394
|
handleCreateCurrentPdv,
|
|
@@ -1,183 +1,184 @@
|
|
|
1
|
-
import React, {useEffect, useMemo, useState} from 'react';
|
|
2
|
-
import {Text, TouchableOpacity, View} from 'react-native';
|
|
3
|
-
import {SafeAreaView} from 'react-native-safe-area-context';
|
|
4
|
-
import {useNavigation} from '@react-navigation/native';
|
|
5
|
-
import {useStore} from '@store';
|
|
6
|
-
import {resolveThemePalette} from '@controleonline/../../src/styles/branding';
|
|
7
|
-
import {colors} from '@controleonline/../../src/styles/colors';
|
|
8
|
-
import CompactFilterSelector from '@controleonline/ui-default/src/react/components/filters/CompactFilterSelector';
|
|
9
|
-
import {PDV_DEVICE_TYPE} from '@controleonline/ui-common/src/react/utils/printerDevices';
|
|
10
|
-
import Icon from 'react-native-vector-icons/Feather';
|
|
11
|
-
import {
|
|
12
|
-
ALL_DEVICE_FILTER,
|
|
13
|
-
getDeviceCreationAction,
|
|
14
|
-
getDeviceFilterAccent,
|
|
15
|
-
getDeviceFilterDefinition,
|
|
16
|
-
getDeviceFilterIcon,
|
|
17
|
-
getDeviceFilterOptions,
|
|
18
|
-
getPdvSubfilterOptions,
|
|
19
|
-
hasRegisteredDeviceFilter,
|
|
20
|
-
isPdvSubfilter,
|
|
21
|
-
} from './Devices/deviceTypes';
|
|
22
|
-
import { createStyles } from './Devices.styles';
|
|
23
|
-
|
|
24
|
-
const tt = (type, key) => global.t?.t('configs', type, key);
|
|
25
|
-
|
|
26
|
-
const Devices = () => {
|
|
27
|
-
const navigation = useNavigation();
|
|
28
|
-
const peopleStore = useStore('people');
|
|
29
|
-
const themeStore = useStore('theme');
|
|
30
|
-
|
|
31
|
-
const {currentCompany} = peopleStore.getters;
|
|
32
|
-
const {colors: themeColors} = themeStore.getters;
|
|
33
|
-
|
|
34
|
-
const [activeFilter, setActiveFilter] = useState(ALL_DEVICE_FILTER);
|
|
35
|
-
|
|
36
|
-
const brandColors = useMemo(
|
|
37
|
-
() =>
|
|
38
|
-
resolveThemePalette(
|
|
39
|
-
{...themeColors, ...(currentCompany?.theme?.colors || {})},
|
|
40
|
-
colors,
|
|
41
|
-
),
|
|
42
|
-
[themeColors, currentCompany?.id],
|
|
43
|
-
);
|
|
44
|
-
const styles = useMemo(() => createStyles(brandColors), [brandColors]);
|
|
45
|
-
const accentPalette = useMemo(
|
|
46
|
-
() => ({
|
|
47
|
-
primary: brandColors.info,
|
|
48
|
-
success: brandColors.success,
|
|
49
|
-
danger: brandColors.error,
|
|
50
|
-
warning: brandColors.warning,
|
|
51
|
-
}),
|
|
52
|
-
[brandColors.error, brandColors.info, brandColors.success, brandColors.warning],
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const filterOptions = useMemo(() => getDeviceFilterOptions(), []);
|
|
56
|
-
const pdvSubfilterOptions = useMemo(() => getPdvSubfilterOptions(), []);
|
|
57
|
-
const activeFilterDefinition = useMemo(
|
|
58
|
-
() => getDeviceFilterDefinition(activeFilter),
|
|
59
|
-
[activeFilter],
|
|
60
|
-
);
|
|
61
|
-
const topLevelActiveFilter = useMemo(
|
|
62
|
-
() => (isPdvSubfilter(activeFilter) ? PDV_DEVICE_TYPE : activeFilter),
|
|
63
|
-
[activeFilter],
|
|
64
|
-
);
|
|
65
|
-
const activeFilterAction = useMemo(
|
|
66
|
-
() => getDeviceCreationAction(topLevelActiveFilter),
|
|
67
|
-
[topLevelActiveFilter],
|
|
68
|
-
);
|
|
69
|
-
const deviceTypeSelectorOptions = useMemo(
|
|
70
|
-
() => filterOptions.map(option => ({
|
|
71
|
-
key: option.key,
|
|
72
|
-
label: option.label,
|
|
73
|
-
})),
|
|
74
|
-
[filterOptions],
|
|
75
|
-
);
|
|
76
|
-
const pdvGatewaySelectorOptions = useMemo(
|
|
77
|
-
() => [
|
|
78
|
-
{key: PDV_DEVICE_TYPE, label: tt('device_filter', 'all') || 'Todos'},
|
|
79
|
-
...pdvSubfilterOptions.map(option => ({
|
|
80
|
-
key: option.key,
|
|
81
|
-
label: option.label,
|
|
82
|
-
})),
|
|
83
|
-
],
|
|
84
|
-
[pdvSubfilterOptions],
|
|
85
|
-
);
|
|
86
|
-
const selectedDeviceTypeLabel = useMemo(
|
|
87
|
-
() =>
|
|
88
|
-
deviceTypeSelectorOptions.find(option => option.key === topLevelActiveFilter)?.label ||
|
|
89
|
-
tt('device_filter', 'allTypes') ||
|
|
90
|
-
'Todos os tipos',
|
|
91
|
-
[deviceTypeSelectorOptions, topLevelActiveFilter],
|
|
92
|
-
);
|
|
93
|
-
const selectedPdvGatewayLabel = useMemo(
|
|
94
|
-
() =>
|
|
95
|
-
pdvGatewaySelectorOptions.find(option => option.key === activeFilter)?.label ||
|
|
96
|
-
tt('device_filter', 'all') ||
|
|
97
|
-
'Todos',
|
|
98
|
-
[activeFilter, pdvGatewaySelectorOptions],
|
|
99
|
-
);
|
|
100
|
-
const ActiveTabComponent = activeFilterDefinition.TabComponent;
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
if (!hasRegisteredDeviceFilter(activeFilter)) {
|
|
104
|
-
setActiveFilter(ALL_DEVICE_FILTER);
|
|
105
|
-
}
|
|
106
|
-
}, [activeFilter]);
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<SafeAreaView
|
|
110
|
-
style={[styles.container, {backgroundColor: brandColors.background}]}>
|
|
111
|
-
<View style={styles.filtersBlock}>
|
|
112
|
-
<Text style={styles.filtersLabel}>{tt('device_filter', 'filterByType') || 'Filtrar por tipo'}</Text>
|
|
113
|
-
<CompactFilterSelector
|
|
114
|
-
icon={getDeviceFilterIcon(topLevelActiveFilter)}
|
|
115
|
-
label={selectedDeviceTypeLabel}
|
|
116
|
-
title={tt('device_filter', 'filterByType') || 'Filtrar por tipo'}
|
|
117
|
-
accentColor={getDeviceFilterAccent(topLevelActiveFilter, {
|
|
118
|
-
brandColors,
|
|
119
|
-
hex: accentPalette,
|
|
120
|
-
})}
|
|
121
|
-
active={topLevelActiveFilter !== ALL_DEVICE_FILTER}
|
|
122
|
-
options={deviceTypeSelectorOptions}
|
|
123
|
-
selectedKey={topLevelActiveFilter}
|
|
124
|
-
onSelect={optionKey => {
|
|
125
|
-
setActiveFilter(optionKey);
|
|
126
|
-
return true;
|
|
127
|
-
}}
|
|
128
|
-
/>
|
|
129
|
-
</View>
|
|
130
|
-
|
|
131
|
-
{topLevelActiveFilter === PDV_DEVICE_TYPE && pdvSubfilterOptions.length > 0 ? (
|
|
132
|
-
<View style={styles.filtersBlock}>
|
|
133
|
-
<Text style={styles.filtersLabel}>{tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}</Text>
|
|
134
|
-
<CompactFilterSelector
|
|
135
|
-
icon={getDeviceFilterIcon(activeFilter)}
|
|
136
|
-
label={selectedPdvGatewayLabel}
|
|
137
|
-
title={tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}
|
|
138
|
-
accentColor={getDeviceFilterAccent(activeFilter, {
|
|
139
|
-
brandColors,
|
|
140
|
-
hex: accentPalette,
|
|
141
|
-
})}
|
|
142
|
-
active={activeFilter !== PDV_DEVICE_TYPE}
|
|
143
|
-
options={pdvGatewaySelectorOptions}
|
|
144
|
-
selectedKey={activeFilter}
|
|
145
|
-
onSelect={optionKey => {
|
|
146
|
-
setActiveFilter(optionKey);
|
|
147
|
-
return true;
|
|
148
|
-
}}
|
|
149
|
-
/>
|
|
150
|
-
</View>
|
|
151
|
-
) : null}
|
|
152
|
-
|
|
153
|
-
{activeFilterAction ? (
|
|
154
|
-
<View style={styles.printerActionBlock}>
|
|
155
|
-
<TouchableOpacity
|
|
156
|
-
style={[
|
|
157
|
-
styles.createPrinterBtn,
|
|
158
|
-
{backgroundColor: brandColors.primary},
|
|
159
|
-
]}
|
|
160
|
-
activeOpacity={0.86}
|
|
161
|
-
onPress={() => navigation.navigate(activeFilterAction.routeName)}>
|
|
162
|
-
<Icon name="plus-circle" size={16} color={brandColors.white} />
|
|
163
|
-
<Text style={styles.createPrinterBtnText}>
|
|
164
|
-
{activeFilterAction.label}
|
|
165
|
-
</Text>
|
|
166
|
-
</TouchableOpacity>
|
|
167
|
-
|
|
168
|
-
<Text style={styles.helperText}>
|
|
169
|
-
{activeFilterAction.helperText}
|
|
170
|
-
</Text>
|
|
171
|
-
</View>
|
|
172
|
-
) : null}
|
|
173
|
-
|
|
174
|
-
{ActiveTabComponent ? (
|
|
175
|
-
<ActiveTabComponent
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
1
|
+
import React, {useEffect, useMemo, useState} from 'react';
|
|
2
|
+
import {Text, TouchableOpacity, View} from 'react-native';
|
|
3
|
+
import {SafeAreaView} from 'react-native-safe-area-context';
|
|
4
|
+
import {useNavigation} from '@react-navigation/native';
|
|
5
|
+
import {useStore} from '@store';
|
|
6
|
+
import {resolveThemePalette} from '@controleonline/../../src/styles/branding';
|
|
7
|
+
import {colors} from '@controleonline/../../src/styles/colors';
|
|
8
|
+
import CompactFilterSelector from '@controleonline/ui-default/src/react/components/filters/CompactFilterSelector';
|
|
9
|
+
import {PDV_DEVICE_TYPE} from '@controleonline/ui-common/src/react/utils/printerDevices';
|
|
10
|
+
import Icon from 'react-native-vector-icons/Feather';
|
|
11
|
+
import {
|
|
12
|
+
ALL_DEVICE_FILTER,
|
|
13
|
+
getDeviceCreationAction,
|
|
14
|
+
getDeviceFilterAccent,
|
|
15
|
+
getDeviceFilterDefinition,
|
|
16
|
+
getDeviceFilterIcon,
|
|
17
|
+
getDeviceFilterOptions,
|
|
18
|
+
getPdvSubfilterOptions,
|
|
19
|
+
hasRegisteredDeviceFilter,
|
|
20
|
+
isPdvSubfilter,
|
|
21
|
+
} from './Devices/deviceTypes';
|
|
22
|
+
import { createStyles } from './Devices.styles';
|
|
23
|
+
|
|
24
|
+
const tt = (type, key) => global.t?.t('configs', type, key);
|
|
25
|
+
|
|
26
|
+
const Devices = ({themePalette = null} = {}) => {
|
|
27
|
+
const navigation = useNavigation();
|
|
28
|
+
const peopleStore = useStore('people');
|
|
29
|
+
const themeStore = useStore('theme');
|
|
30
|
+
|
|
31
|
+
const {currentCompany} = peopleStore.getters;
|
|
32
|
+
const {colors: themeColors} = themeStore.getters;
|
|
33
|
+
|
|
34
|
+
const [activeFilter, setActiveFilter] = useState(ALL_DEVICE_FILTER);
|
|
35
|
+
|
|
36
|
+
const brandColors = useMemo(
|
|
37
|
+
() =>
|
|
38
|
+
themePalette || resolveThemePalette(
|
|
39
|
+
{...themeColors, ...(currentCompany?.theme?.colors || {})},
|
|
40
|
+
colors,
|
|
41
|
+
),
|
|
42
|
+
[themePalette, themeColors, currentCompany?.id],
|
|
43
|
+
);
|
|
44
|
+
const styles = useMemo(() => createStyles(brandColors), [brandColors]);
|
|
45
|
+
const accentPalette = useMemo(
|
|
46
|
+
() => ({
|
|
47
|
+
primary: brandColors.info,
|
|
48
|
+
success: brandColors.success,
|
|
49
|
+
danger: brandColors.error,
|
|
50
|
+
warning: brandColors.warning,
|
|
51
|
+
}),
|
|
52
|
+
[brandColors.error, brandColors.info, brandColors.success, brandColors.warning],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const filterOptions = useMemo(() => getDeviceFilterOptions(), []);
|
|
56
|
+
const pdvSubfilterOptions = useMemo(() => getPdvSubfilterOptions(), []);
|
|
57
|
+
const activeFilterDefinition = useMemo(
|
|
58
|
+
() => getDeviceFilterDefinition(activeFilter),
|
|
59
|
+
[activeFilter],
|
|
60
|
+
);
|
|
61
|
+
const topLevelActiveFilter = useMemo(
|
|
62
|
+
() => (isPdvSubfilter(activeFilter) ? PDV_DEVICE_TYPE : activeFilter),
|
|
63
|
+
[activeFilter],
|
|
64
|
+
);
|
|
65
|
+
const activeFilterAction = useMemo(
|
|
66
|
+
() => getDeviceCreationAction(topLevelActiveFilter),
|
|
67
|
+
[topLevelActiveFilter],
|
|
68
|
+
);
|
|
69
|
+
const deviceTypeSelectorOptions = useMemo(
|
|
70
|
+
() => filterOptions.map(option => ({
|
|
71
|
+
key: option.key,
|
|
72
|
+
label: option.label,
|
|
73
|
+
})),
|
|
74
|
+
[filterOptions],
|
|
75
|
+
);
|
|
76
|
+
const pdvGatewaySelectorOptions = useMemo(
|
|
77
|
+
() => [
|
|
78
|
+
{key: PDV_DEVICE_TYPE, label: tt('device_filter', 'all') || 'Todos'},
|
|
79
|
+
...pdvSubfilterOptions.map(option => ({
|
|
80
|
+
key: option.key,
|
|
81
|
+
label: option.label,
|
|
82
|
+
})),
|
|
83
|
+
],
|
|
84
|
+
[pdvSubfilterOptions],
|
|
85
|
+
);
|
|
86
|
+
const selectedDeviceTypeLabel = useMemo(
|
|
87
|
+
() =>
|
|
88
|
+
deviceTypeSelectorOptions.find(option => option.key === topLevelActiveFilter)?.label ||
|
|
89
|
+
tt('device_filter', 'allTypes') ||
|
|
90
|
+
'Todos os tipos',
|
|
91
|
+
[deviceTypeSelectorOptions, topLevelActiveFilter],
|
|
92
|
+
);
|
|
93
|
+
const selectedPdvGatewayLabel = useMemo(
|
|
94
|
+
() =>
|
|
95
|
+
pdvGatewaySelectorOptions.find(option => option.key === activeFilter)?.label ||
|
|
96
|
+
tt('device_filter', 'all') ||
|
|
97
|
+
'Todos',
|
|
98
|
+
[activeFilter, pdvGatewaySelectorOptions],
|
|
99
|
+
);
|
|
100
|
+
const ActiveTabComponent = activeFilterDefinition.TabComponent;
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!hasRegisteredDeviceFilter(activeFilter)) {
|
|
104
|
+
setActiveFilter(ALL_DEVICE_FILTER);
|
|
105
|
+
}
|
|
106
|
+
}, [activeFilter]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<SafeAreaView
|
|
110
|
+
style={[styles.container, {backgroundColor: brandColors.background}]}>
|
|
111
|
+
<View style={styles.filtersBlock}>
|
|
112
|
+
<Text style={styles.filtersLabel}>{tt('device_filter', 'filterByType') || 'Filtrar por tipo'}</Text>
|
|
113
|
+
<CompactFilterSelector
|
|
114
|
+
icon={getDeviceFilterIcon(topLevelActiveFilter)}
|
|
115
|
+
label={selectedDeviceTypeLabel}
|
|
116
|
+
title={tt('device_filter', 'filterByType') || 'Filtrar por tipo'}
|
|
117
|
+
accentColor={getDeviceFilterAccent(topLevelActiveFilter, {
|
|
118
|
+
brandColors,
|
|
119
|
+
hex: accentPalette,
|
|
120
|
+
})}
|
|
121
|
+
active={topLevelActiveFilter !== ALL_DEVICE_FILTER}
|
|
122
|
+
options={deviceTypeSelectorOptions}
|
|
123
|
+
selectedKey={topLevelActiveFilter}
|
|
124
|
+
onSelect={optionKey => {
|
|
125
|
+
setActiveFilter(optionKey);
|
|
126
|
+
return true;
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
</View>
|
|
130
|
+
|
|
131
|
+
{topLevelActiveFilter === PDV_DEVICE_TYPE && pdvSubfilterOptions.length > 0 ? (
|
|
132
|
+
<View style={styles.filtersBlock}>
|
|
133
|
+
<Text style={styles.filtersLabel}>{tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}</Text>
|
|
134
|
+
<CompactFilterSelector
|
|
135
|
+
icon={getDeviceFilterIcon(activeFilter)}
|
|
136
|
+
label={selectedPdvGatewayLabel}
|
|
137
|
+
title={tt('device_filter', 'pdvGateway') || 'Gateway do PDV'}
|
|
138
|
+
accentColor={getDeviceFilterAccent(activeFilter, {
|
|
139
|
+
brandColors,
|
|
140
|
+
hex: accentPalette,
|
|
141
|
+
})}
|
|
142
|
+
active={activeFilter !== PDV_DEVICE_TYPE}
|
|
143
|
+
options={pdvGatewaySelectorOptions}
|
|
144
|
+
selectedKey={activeFilter}
|
|
145
|
+
onSelect={optionKey => {
|
|
146
|
+
setActiveFilter(optionKey);
|
|
147
|
+
return true;
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
</View>
|
|
151
|
+
) : null}
|
|
152
|
+
|
|
153
|
+
{activeFilterAction ? (
|
|
154
|
+
<View style={styles.printerActionBlock}>
|
|
155
|
+
<TouchableOpacity
|
|
156
|
+
style={[
|
|
157
|
+
styles.createPrinterBtn,
|
|
158
|
+
{backgroundColor: brandColors.primary},
|
|
159
|
+
]}
|
|
160
|
+
activeOpacity={0.86}
|
|
161
|
+
onPress={() => navigation.navigate(activeFilterAction.routeName)}>
|
|
162
|
+
<Icon name="plus-circle" size={16} color={brandColors.white} />
|
|
163
|
+
<Text style={styles.createPrinterBtnText}>
|
|
164
|
+
{activeFilterAction.label}
|
|
165
|
+
</Text>
|
|
166
|
+
</TouchableOpacity>
|
|
167
|
+
|
|
168
|
+
<Text style={styles.helperText}>
|
|
169
|
+
{activeFilterAction.helperText}
|
|
170
|
+
</Text>
|
|
171
|
+
</View>
|
|
172
|
+
) : null}
|
|
173
|
+
|
|
174
|
+
{ActiveTabComponent ? (
|
|
175
|
+
<ActiveTabComponent
|
|
176
|
+
themePalette={themePalette}
|
|
177
|
+
key={`${activeFilter}-${currentCompany?.id || 'company'}`}
|
|
178
|
+
/>
|
|
179
|
+
) : null}
|
|
180
|
+
</SafeAreaView>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export default Devices;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import eventBus from '../components/EventBus';
|
|
2
|
+
|
|
3
|
+
const key = 'product-catalog-cache-revision';
|
|
4
|
+
let revision = '0';
|
|
5
|
+
let sequence = 0;
|
|
6
|
+
export const getProductCatalogCacheRevision = () => {
|
|
7
|
+
try { return localStorage.getItem(key) || revision; } catch { return revision; }
|
|
8
|
+
};
|
|
9
|
+
export const invalidateProductCatalogCache = () => {
|
|
10
|
+
revision = `${Date.now()}:${++sequence}`;
|
|
11
|
+
try { localStorage.setItem(key, revision); } catch { /* In-memory invalidation still applies. */ }
|
|
12
|
+
eventBus.emit('product-catalog-cache:cleared');
|
|
13
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const {test} = require('node:test');
|
|
3
|
+
const {readStoredSessionToken} = require('../../../utils/storedSessionToken');
|
|
4
|
+
|
|
5
|
+
for (const prefix of ['', '__q_objt|']) {
|
|
6
|
+
for (const field of ['token', 'api_key']) {
|
|
7
|
+
test(`reads ${field} from ${prefix ? 'wrapped' : 'plain'} session`, () => {
|
|
8
|
+
const storage = {getItem: key => key === 'session' ? prefix + JSON.stringify({[field]: 'synthetic-token'}) : null};
|
|
9
|
+
assert.equal(readStoredSessionToken(storage), 'synthetic-token');
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
test('prefers the canonical token field when both are present', () => {
|
|
14
|
+
assert.equal(readStoredSessionToken({getItem: () => JSON.stringify({token: 'current', api_key: 'old'})}), 'current');
|
|
15
|
+
});
|
|
16
|
+
test('missing, malformed and inaccessible sessions have no token', () => {
|
|
17
|
+
for (const raw of [null, '', 'broken', '__q_objt|broken', '{}', 'null']) {
|
|
18
|
+
assert.equal(readStoredSessionToken({getItem: () => raw}), '');
|
|
19
|
+
}
|
|
20
|
+
assert.equal(readStoredSessionToken({getItem: () => {throw new Error('unavailable');}}), '');
|
|
21
|
+
assert.equal(readStoredSessionToken(null), '');
|
|
22
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Quasar LocalStorage serializes objects with this marker; plain JSON is also used.
|
|
2
|
+
const OBJECT_PREFIX = '__q_objt|';
|
|
3
|
+
|
|
4
|
+
const readStoredSessionToken = (storage = typeof localStorage === 'undefined' ? null : localStorage) => {
|
|
5
|
+
try {
|
|
6
|
+
const raw = storage?.getItem?.('session');
|
|
7
|
+
if (!raw) return '';
|
|
8
|
+
const json = typeof raw === 'string' && raw.startsWith(OBJECT_PREFIX)
|
|
9
|
+
? raw.slice(OBJECT_PREFIX.length)
|
|
10
|
+
: raw;
|
|
11
|
+
const session = JSON.parse(json);
|
|
12
|
+
return session?.token || session?.api_key || '';
|
|
13
|
+
} catch {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = {readStoredSessionToken};
|