@controleonline/ui-common 1.2.80 → 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 (90) hide show
  1. package/AGENTS.md +8 -0
  2. package/package.json +9 -3
  3. package/src/api/fetch.js +7 -7
  4. package/src/react/components/AddImportModal.js +6 -2
  5. package/src/react/components/BackgroundRuntimeBridge.js +201 -215
  6. package/src/react/components/ConfirmModal.js +44 -9
  7. package/src/react/components/ConfirmModal.styles.js +55 -41
  8. package/src/react/components/CopyDeviceConfigModal.js +193 -0
  9. package/src/react/components/CopyDeviceConfigModal.styles.js +123 -0
  10. package/src/react/components/DefaultProvider.native.js +22 -0
  11. package/src/react/components/DefaultProvider.web.js +25 -1
  12. package/src/react/components/EntityLogBranch.js +482 -0
  13. package/src/react/components/EntityLogCards.js +369 -0
  14. package/src/react/components/EntityLogContent.js +113 -794
  15. package/src/react/components/LinkedOrderProductsTab.js +3 -3
  16. package/src/react/components/PrintService.js +130 -409
  17. package/src/react/components/RemoteCheckoutService.js +2 -2
  18. package/src/react/components/RuntimeBottomNavigationBar.js +141 -146
  19. package/src/react/components/RuntimeFooterMarqueeText.js +137 -0
  20. package/src/react/components/RuntimeInfoFooter.js +12 -15
  21. package/src/react/components/SystemErrorToast.js +49 -29
  22. package/src/react/components/SystemErrorToast.styles.js +43 -38
  23. package/src/react/components/UserAvatar.js +120 -28
  24. package/src/react/config/deviceConfigBootstrap.js +330 -304
  25. package/src/react/hooks/usePrintSpoolEffects.js +161 -0
  26. package/src/react/pages/DeviceDetailPage.js +6 -2882
  27. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +233 -0
  28. package/src/react/pages/Devices/detail/DeviceDetailHeader.js +138 -0
  29. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +167 -0
  30. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +216 -0
  31. package/src/react/pages/Devices/detail/DeviceDetailPaymentSection.js +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/paymentDevices.js +6 -0
  62. package/src/react/utils/printManagedPrinter.js +45 -0
  63. package/src/react/utils/printRouting.js +63 -0
  64. package/src/react/utils/printSpoolUtils.js +119 -0
  65. package/src/react/utils/printerDevices.js +368 -368
  66. package/src/react/utils/runtimeFooter.js +10 -19
  67. package/src/react/utils/shopFranchises.js +186 -65
  68. package/src/react/utils/systemErrorMessage.js +71 -67
  69. package/src/store/device_config/customActions.js +119 -30
  70. package/src/tests/browser/manager/bottom-navigation-role-toolbar.spec.js +353 -0
  71. package/src/tests/browser/manager/device-detail-alias-save.spec.js +345 -0
  72. package/src/tests/browser/manager/device-detail-copy-config.spec.js +365 -0
  73. package/src/tests/browser/manager/runtime-footer-marquee.spec.js +262 -0
  74. package/src/tests/browser/manager/theme-confirm-toast.spec.js +236 -0
  75. package/src/tests/react/components/RuntimeFooterMarqueeText.test.js +171 -0
  76. package/src/tests/react/components/UserAvatar.test.js +6 -2
  77. package/src/tests/react/components/themeAwareModals.test.mjs +60 -0
  78. package/src/tests/react/config/deviceConfigBootstrap.test.js +104 -66
  79. package/src/tests/react/pages/deviceDetailDelete.test.js +78 -0
  80. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +48 -0
  81. package/src/tests/react/utils/bottomNavigationToolbar.test.js +222 -0
  82. package/src/tests/react/utils/copyDeviceConfigs.test.mjs +127 -0
  83. package/src/tests/react/utils/fileUrl.test.js +38 -0
  84. package/src/tests/react/utils/importStatus.test.js +26 -9
  85. package/src/tests/react/utils/passwordPolicy.test.js +27 -0
  86. package/src/tests/react/utils/printRouting.test.mjs +86 -0
  87. package/src/tests/react/utils/printSpoolUtils.test.mjs +52 -0
  88. package/src/tests/react/utils/runtimeFooter.test.js +7 -0
  89. package/src/tests/react/utils/shopFranchises.test.js +121 -115
  90. package/src/tests/react/utils/systemErrorMessage.test.js +32 -59
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { ActivityIndicator, Linking, Platform, RefreshControl, ScrollView, Text, TextInput, TouchableOpacity, View } from 'react-native';
2
+ import { ActivityIndicator, Platform, RefreshControl, ScrollView, Text, TouchableOpacity, View } from 'react-native';
3
3
  import { SafeAreaView } from 'react-native-safe-area-context';
4
4
  import { useFocusEffect } from '@react-navigation/native';
5
5
  import Icon from 'react-native-vector-icons/Feather';
@@ -8,160 +8,43 @@ import { api } from '@controleonline/ui-common/src/api';
8
8
  import useToastMessage from '@controleonline/ui-crm/src/react/hooks/useToastMessage';
9
9
  import { useStore } from '@store';
10
10
  import { colors } from '@controleonline/../../src/styles/colors';
11
+ import { resolveThemePalette, withOpacity } from '@controleonline/../../src/styles/branding';
12
+ import { getIntegrationConfig, getIntegrationByKey, parseIntegrationCollection } from './integrationsCatalog';
13
+ import IntegrationConfigFields from './IntegrationConfigFields';
11
14
  import {
12
- resolveThemePalette,
13
- withOpacity,
14
- } from '@controleonline/../../src/styles/branding';
15
-
16
- import {
17
- getIntegrationConfig,
18
- getIntegrationByKey,
19
- parseIntegrationCollection,
20
- } from './integrationsCatalog';
21
- import DefaultUpload from '@controleonline/ui-default/src/react/components/upload/DefaultUpload';
22
- import { extractFileId, toFileIri, uploadFileToApi } from '@controleonline/ui-default/src/react/components/upload/fileUpload';
15
+ ROUTE_PROVIDER_MAP,
16
+ buildFieldValues,
17
+ extractAuthorizationUrl,
18
+ formatApiError,
19
+ formatUberOAuthError,
20
+ getConfigFields,
21
+ isConnectedValue,
22
+ normalizeTextValue,
23
+ openAuthorizationUrl,
24
+ resolveFallbackConfigs,
25
+ resolveProviderId,
26
+ routeNameToPath,
27
+ toConfigRequestValue,
28
+ } from './IntegrationConfigPage.utils';
23
29
  import styles from './IntegrationConfigPage.styles';
24
30
 
25
- const ROUTE_PROVIDER_MAP = {
26
- UberIntegrationPage: 'uber',
27
- AsaasIntegrationPage: 'asaas',
28
- ClickSignIntegrationPage: 'clicksign',
29
- };
30
-
31
31
  const shadowStyle = Platform.select({
32
- ios: {
33
- shadowColor: '#0F172A',
34
- shadowOffset: { width: 0, height: 8 },
35
- shadowOpacity: 0.08,
36
- shadowRadius: 16,
37
- },
32
+ ios: { shadowColor: '#0F172A', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.08, shadowRadius: 16 },
38
33
  android: { elevation: 3 },
39
34
  web: { boxShadow: '0 10px 24px rgba(15,23,42,0.08)' },
40
35
  });
41
36
 
42
- const routeNameToPath = routeName =>
43
- String(routeName || '')
44
- .replace(/([a-z])([A-Z])/g, '$1-$2')
45
- .toLowerCase();
46
-
47
- const formatApiError = error => {
48
- if (!error) return 'Nao foi possivel carregar a configuracao da integracao.';
49
- if (typeof error === 'string') return error;
50
- return error?.message || error?.description || error?.errmsg || 'Nao foi possivel carregar a configuracao da integracao.';
51
- };
52
-
53
- const normalizeSourceConfigs = source => {
54
- if (Array.isArray(source)) {
55
- return source.reduce((accumulator, item) => {
56
- const key = String(item?.configKey || '').trim();
57
- if (key) {
58
- accumulator[key] = item?.configValue;
59
- }
60
- return accumulator;
61
- }, {});
62
- }
63
-
64
- if (source && typeof source === 'object') {
65
- return source;
66
- }
67
-
68
- return {};
69
- };
70
-
71
- const normalizeTextValue = value => {
72
- let text = String(value ?? '').trim();
73
- // Strip accidental surrounding quotes from double-encoded config values
74
- if (
75
- (text.startsWith('"') && text.endsWith('"') && text.length >= 2) ||
76
- (text.startsWith("'") && text.endsWith("'") && text.length >= 2)
77
- ) {
78
- text = text.slice(1, -1).trim();
79
- }
80
- return text;
81
- };
82
-
83
- const isConnectedValue = value =>
84
- value === true ||
85
- value === 1 ||
86
- value === '1' ||
87
- String(value).trim().toLowerCase() === 'true';
88
-
89
- const toConfigRequestValue = value => {
90
- // Plain strings must NOT be JSON.stringify'd here — the HTTP JSON body
91
- // already serializes them. Stringify caused values to be stored with
92
- // literal surrounding quotes (e.g. ""whsec_...""), breaking webhook auth.
93
- if (value === undefined || value === null) {
94
- return '';
95
- }
96
-
97
- if (typeof value === 'string') {
98
- return normalizeTextValue(value);
99
- }
100
-
101
- if (typeof value === 'object') {
102
- return JSON.stringify(value);
103
- }
104
-
105
- return String(value);
106
- };
107
-
108
-
109
- const getConfigFields = providerConfig => {
110
- if (!providerConfig) return [];
111
- if (Array.isArray(providerConfig.tabs) && providerConfig.tabs.length > 0) {
112
- return providerConfig.tabs.flatMap(tab => tab.fields || []);
113
- }
114
- return providerConfig.fields || [];
115
- };
116
-
117
- const buildFieldValues = (providerConfig, source) => {
118
- const sourceMap = normalizeSourceConfigs(source);
119
-
120
- return getConfigFields(providerConfig).reduce((accumulator, field) => {
121
- accumulator[field.key] = normalizeTextValue(sourceMap[field.key]);
122
- return accumulator;
123
- }, {});
124
- };
125
-
126
- const extractAuthorizationUrl = response => {
127
- const candidate =
128
- response?.member?.[0]?.authorization_url ||
129
- response?.member?.[0]?.auth_url ||
130
- response?.member?.[0]?.url ||
131
- response?.authorization_url ||
132
- response?.auth_url ||
133
- response?.url ||
134
- response?.data?.authorization_url ||
135
- response?.data?.auth_url ||
136
- response?.data?.url;
137
-
138
- return normalizeTextValue(candidate);
139
- };
140
-
141
- const formatUberOAuthError = error => {
142
- const normalized = normalizeTextValue(error).toLowerCase();
143
-
144
- if (normalized === 'invalid_scope') {
145
- return 'O Uber nao liberou o scope pos_provisioning para este app. Esse app precisa estar aprovado/whitelisted no dashboard do Uber.';
146
- }
147
-
148
- if (normalized === 'access_denied') {
149
- return 'O login do Uber foi cancelado.';
150
- }
151
-
152
- return normalizeTextValue(error) || 'Nao foi possivel concluir a conexao com o Uber.';
153
- };
154
-
155
- const openAuthorizationUrl = async authUrl => {
156
- if (Platform.OS === 'web' && typeof window !== 'undefined' && typeof window.location?.assign === 'function') {
157
- window.location.assign(authUrl);
158
- return;
159
- }
160
-
161
- await Linking.openURL(authUrl);
162
- };
163
-
164
- export default function IntegrationConfigPage({ route, navigation, embedded = false }) {
37
+ const CenterState = ({ icon, iconColor, title, text, backgroundColor, spinnerColor }) => (
38
+ <SafeAreaView style={[styles.container, backgroundColor ? { backgroundColor } : null]} edges={['bottom']}>
39
+ <View style={styles.centerState}>
40
+ {spinnerColor ? <ActivityIndicator size="large" color={spinnerColor} /> : <Icon name={icon} size={32} color={iconColor} />}
41
+ <Text style={styles.centerStateTitle}>{title}</Text>
42
+ <Text style={styles.centerStateText}>{text}</Text>
43
+ </View>
44
+ </SafeAreaView>
45
+ );
46
+
47
+ export default function IntegrationConfigPage({ route, embedded = false }) {
165
48
  const peopleStore = useStore('people');
166
49
  const themeStore = useStore('theme');
167
50
  const configsStore = useStore('configs');
@@ -173,21 +56,13 @@ export default function IntegrationConfigPage({ route, navigation, embedded = fa
173
56
  const oauthNoticeRef = useRef('');
174
57
 
175
58
  const providerKey = useMemo(
176
- () =>
177
- route?.params?.providerKey ||
178
- ROUTE_PROVIDER_MAP[route?.name] ||
179
- '',
59
+ () => route?.params?.providerKey || ROUTE_PROVIDER_MAP[route?.name] || '',
180
60
  [route?.name, route?.params?.providerKey],
181
61
  );
182
- const providerConfig = useMemo(
183
- () => getIntegrationConfig(providerKey),
184
- [providerKey],
185
- );
62
+ const providerConfig = useMemo(() => getIntegrationConfig(providerKey), [providerKey]);
186
63
  const configFields = useMemo(() => getConfigFields(providerConfig), [providerConfig]);
187
64
  const fiscalTabs = providerConfig?.tabs || [];
188
- const [activeFiscalTab, setActiveFiscalTab] = useState(
189
- () => fiscalTabs[0]?.key || 'general',
190
- );
65
+ const [activeFiscalTab, setActiveFiscalTab] = useState(() => fiscalTabs[0]?.key || 'general');
191
66
  const activeTabDef = useMemo(
192
67
  () => fiscalTabs.find(tab => tab.key === activeFiscalTab) || fiscalTabs[0] || null,
193
68
  [activeFiscalTab, fiscalTabs],
@@ -198,82 +73,51 @@ export default function IntegrationConfigPage({ route, navigation, embedded = fa
198
73
  );
199
74
 
200
75
  useEffect(() => {
201
- if (fiscalTabs.length && !fiscalTabs.some(tab => tab.key === activeFiscalTab)) {
202
- setActiveFiscalTab(fiscalTabs[0].key);
203
- }
76
+ if (fiscalTabs.length && !fiscalTabs.some(tab => tab.key === activeFiscalTab)) setActiveFiscalTab(fiscalTabs[0].key);
204
77
  }, [activeFiscalTab, fiscalTabs]);
205
- const returnPath = useMemo(() => {
206
- const routePath = normalizeTextValue(
207
- route?.params?.return_path || route?.params?.returnPath || '',
208
- );
209
-
210
- if (routePath) {
211
- return routePath.startsWith('/') ? routePath : `/${routePath}`;
212
- }
213
78
 
214
- const normalizedRoutePath = routeNameToPath(route?.name);
215
- return normalizedRoutePath ? `/${normalizedRoutePath}` : '/uber-integration-page';
79
+ const returnPath = useMemo(() => {
80
+ const routePath = normalizeTextValue(route?.params?.return_path || route?.params?.returnPath || '');
81
+ if (routePath) return routePath.startsWith('/') ? routePath : `/${routePath}`;
82
+ const normalized = routeNameToPath(route?.name);
83
+ return normalized ? `/${normalized}` : '/uber-integration-page';
216
84
  }, [route?.name, route?.params?.returnPath, route?.params?.return_path]);
217
85
 
218
86
  const brandColors = useMemo(
219
- () =>
220
- resolveThemePalette(
221
- {
222
- ...themeColors,
223
- ...(currentCompany?.theme?.colors || {}),
224
- },
225
- colors,
226
- ),
87
+ () => resolveThemePalette({ ...themeColors, ...(currentCompany?.theme?.colors || {}) }, colors),
227
88
  [themeColors, currentCompany?.id],
228
89
  );
229
-
230
90
  const [loading, setLoading] = useState(true);
231
91
  const [refreshing, setRefreshing] = useState(false);
232
92
  const [authLoading, setAuthLoading] = useState(false);
233
93
  const [configValues, setConfigValues] = useState({});
234
94
  const [integrationSummary, setIntegrationSummary] = useState(null);
235
95
 
236
- const providerId = currentCompany?.id;
237
- const providerIri = useMemo(
238
- () => (providerId ? `/people/${String(providerId).replace(/\D/g, '')}` : ''),
239
- [providerId],
96
+ // Company details can edit a company that is not the globally selected one.
97
+ // The embedded route id is authoritative for all fiscal reads and writes.
98
+ const providerId = useMemo(
99
+ () => resolveProviderId({ route, currentCompany }),
100
+ [route?.params?.companyId, currentCompany?.id],
240
101
  );
241
-
242
- const syncConfigValues = useCallback(
243
- source => {
244
- if (!providerConfig) {
245
- setConfigValues({});
246
- return;
247
- }
248
-
249
- setConfigValues(buildFieldValues(providerConfig, source));
250
- },
251
- [providerConfig],
102
+ const providerIri = useMemo(() => (providerId ? `/people/${providerId}` : ''), [providerId]);
103
+ const fallbackConfigs = useMemo(
104
+ () => resolveFallbackConfigs({ providerId, currentCompany }),
105
+ [providerId, currentCompany?.id, currentCompany?.configs],
252
106
  );
253
107
 
254
- useEffect(() => {
255
- syncConfigValues(currentCompany?.configs);
256
- }, [currentCompany?.configs, syncConfigValues]);
108
+ const syncConfigValues = useCallback(source => {
109
+ setConfigValues(providerConfig ? buildFieldValues(providerConfig, source) : {});
110
+ }, [providerConfig]);
257
111
 
112
+ useEffect(() => syncConfigValues(fallbackConfigs), [fallbackConfigs, syncConfigValues]);
258
113
  useEffect(() => {
259
114
  const oauthStatus = normalizeTextValue(route?.params?.oauth_status).toLowerCase();
260
115
  const oauthError = normalizeTextValue(route?.params?.oauth_error);
261
116
  const oauthKey = `${oauthStatus}|${oauthError}`;
262
-
263
- if (!oauthStatus || oauthNoticeRef.current === oauthKey) {
264
- return;
265
- }
266
-
117
+ if (!oauthStatus || oauthNoticeRef.current === oauthKey) return;
267
118
  oauthNoticeRef.current = oauthKey;
268
-
269
- if (oauthStatus === 'success') {
270
- showSuccess('Uber conectado com sucesso.');
271
- return;
272
- }
273
-
274
- if (oauthStatus === 'error') {
275
- showError(formatUberOAuthError(oauthError));
276
- }
119
+ if (oauthStatus === 'success') showSuccess('Uber conectado com sucesso.');
120
+ if (oauthStatus === 'error') showError(formatUberOAuthError(oauthError));
277
121
  }, [route?.params?.oauth_error, route?.params?.oauth_status, showError, showSuccess]);
278
122
 
279
123
  const loadPageData = useCallback(async ({ showLoading = true } = {}) => {
@@ -282,119 +126,61 @@ export default function IntegrationConfigPage({ route, navigation, embedded = fa
282
126
  setLoading(false);
283
127
  return;
284
128
  }
285
-
286
- if (showLoading) {
287
- setLoading(true);
288
- }
289
-
129
+ if (showLoading) setLoading(true);
290
130
  try {
291
- const integrationPromise = api.fetch('/marketplace/integrations', {
292
- params: {
293
- provider_id: providerId,
294
- },
295
- });
296
-
297
- if (configFields.length > 0) {
131
+ const integrationPromise = api.fetch('/marketplace/integrations', { params: { provider_id: providerId } });
132
+ if (configFields.length) {
298
133
  const [configResponse, integrationResponse] = await Promise.all([
299
- api.fetch('/configs', {
300
- params: {
301
- people: providerIri,
302
- },
303
- }),
134
+ api.fetch('/configs', { params: { people: providerIri } }),
304
135
  integrationPromise,
305
136
  ]);
306
-
307
137
  syncConfigValues(parseIntegrationCollection(configResponse));
308
- setIntegrationSummary(
309
- getIntegrationByKey(integrationResponse, providerConfig.key),
310
- );
311
- return;
138
+ setIntegrationSummary(getIntegrationByKey(integrationResponse, providerConfig.key));
139
+ } else {
140
+ setIntegrationSummary(getIntegrationByKey(await integrationPromise, providerConfig.key));
312
141
  }
313
-
314
- const integrationResponse = await integrationPromise;
315
- setIntegrationSummary(
316
- getIntegrationByKey(integrationResponse, providerConfig.key),
317
- );
318
142
  } catch (error) {
319
143
  showError(formatApiError(error));
320
- syncConfigValues(currentCompany?.configs);
144
+ syncConfigValues(fallbackConfigs);
321
145
  setIntegrationSummary(null);
322
146
  } finally {
323
- if (showLoading) {
324
- setLoading(false);
325
- }
147
+ if (showLoading) setLoading(false);
326
148
  }
327
- }, [
328
- configFields.length,
329
- currentCompany?.configs,
330
- providerConfig,
331
- providerIri,
332
- providerId,
333
- showError,
334
- syncConfigValues,
335
- ]);
336
-
337
- useFocusEffect(
338
- useCallback(() => {
339
- loadPageData();
340
- }, [loadPageData]),
341
- );
149
+ }, [configFields.length, fallbackConfigs, providerConfig, providerIri, providerId, showError, syncConfigValues]);
342
150
 
151
+ useFocusEffect(useCallback(() => { loadPageData(); }, [loadPageData]));
343
152
  const onRefresh = useCallback(async () => {
344
153
  setRefreshing(true);
345
- try {
346
- await loadPageData({ showLoading: false });
347
- } finally {
348
- setRefreshing(false);
349
- }
154
+ try { await loadPageData({ showLoading: false }); } finally { setRefreshing(false); }
350
155
  }, [loadPageData]);
351
-
352
156
  const updateField = useCallback((fieldKey, value) => {
353
- setConfigValues(currentValues => ({
354
- ...currentValues,
355
- [fieldKey]: value,
356
- }));
157
+ setConfigValues(current => ({ ...current, [fieldKey]: value }));
357
158
  }, []);
358
159
 
359
160
  const handleOAuthConnect = useCallback(async () => {
360
- if (!providerIri || !providerConfig || !providerConfig.oauthConnect) {
161
+ if (!providerIri || !providerConfig?.oauthConnect) {
361
162
  showError('Nao foi possivel identificar a integracao selecionada.');
362
163
  return;
363
164
  }
364
-
365
165
  setAuthLoading(true);
366
166
  try {
367
167
  const response = await api.fetch(providerConfig.authorizationEndpoint, {
368
- method: 'POST',
369
- body: {
370
- provider_id: providerId,
371
- return_path: returnPath,
372
- },
168
+ method: 'POST', body: { provider_id: providerId, return_path: returnPath },
373
169
  });
374
-
375
170
  const authUrl = extractAuthorizationUrl(response);
376
- if (!authUrl) {
377
- showError('Nao foi possivel iniciar o login do Uber.');
378
- return;
379
- }
380
-
171
+ if (!authUrl) return showError('Nao foi possivel iniciar o login do Uber.');
381
172
  await openAuthorizationUrl(authUrl);
382
173
  showSuccess('Abrindo login do Uber.');
383
174
  } catch (error) {
384
175
  showError(error?.message || 'Nao foi possivel iniciar o login do Uber.');
385
- } finally {
386
- setAuthLoading(false);
387
- }
176
+ } finally { setAuthLoading(false); }
388
177
  }, [providerConfig, providerId, providerIri, returnPath, showError, showSuccess]);
389
178
 
390
179
  const requiredKeys = providerConfig?.requiredKeys || [];
391
180
  const connected = integrationSummary && typeof integrationSummary.connected !== 'undefined'
392
181
  ? isConnectedValue(integrationSummary.connected)
393
- : requiredKeys.length > 0
394
- ? requiredKeys.every(fieldKey => normalizeTextValue(configValues[fieldKey]) !== '')
395
- : false;
182
+ : requiredKeys.length > 0 && requiredKeys.every(key => normalizeTextValue(configValues[key]) !== '');
396
183
  const statusTone = connected ? '#16A34A' : '#e67e22';
397
- const statusText = connected ? 'Conectado' : 'Pendente';
398
184
  const editable = Boolean(providerIri && providerConfig && !isSaving && !loading && !providerConfig.oauthConnect);
399
185
  const actionLoading = providerConfig?.oauthConnect ? authLoading : isSaving;
400
186
  const actionDisabled = providerConfig?.oauthConnect ? authLoading || loading : !editable;
@@ -404,337 +190,82 @@ export default function IntegrationConfigPage({ route, navigation, embedded = fa
404
190
  showError('Nao foi possivel identificar a integracao selecionada.');
405
191
  return;
406
192
  }
407
-
408
193
  const configs = configFields.map(field => ({
409
194
  configKey: field.key,
410
195
  configValue: toConfigRequestValue(normalizeTextValue(configValues[field.key])),
411
196
  }));
412
-
413
197
  try {
414
- await configActions.addManyConfigs({
415
- configs,
416
- people: providerIri,
417
- module: 4,
418
- visibility: 'public',
419
- });
420
-
198
+ await configActions.addManyConfigs({ configs, people: providerIri, module: 4, visibility: 'public' });
421
199
  showSuccess(`${providerConfig.label} salvo com sucesso.`);
422
200
  await loadPageData({ showLoading: false });
423
- } catch (error) {
424
- showError(error?.message || 'Nao foi possivel salvar a integracao.');
425
- }
426
- }, [
427
- configActions,
428
- configFields,
429
- configValues,
430
- loadPageData,
431
- providerConfig,
432
- providerIri,
433
- showError,
434
- showSuccess,
435
- ]);
201
+ } catch (error) { showError(error?.message || 'Nao foi possivel salvar a integracao.'); }
202
+ }, [configActions, configFields, configValues, loadPageData, providerConfig, providerIri, showError, showSuccess]);
436
203
 
437
- if (!providerConfig) {
438
- return (
439
- <SafeAreaView style={styles.container} edges={['bottom']}>
440
- <View style={styles.centerState}>
441
- <Icon name="alert-triangle" size={32} color="#e67e22" />
442
- <Text style={styles.centerStateTitle}>Integracao indisponivel</Text>
443
- <Text style={styles.centerStateText}>
444
- A tela solicitada nao possui configuracao cadastrada.
445
- </Text>
446
- </View>
447
- </SafeAreaView>
448
- );
449
- }
450
-
451
- if (!providerId) {
452
- return (
453
- <SafeAreaView style={styles.container} edges={['bottom']}>
454
- <View style={styles.centerState}>
455
- <Icon name="building" size={32} color="#94A3B8" />
456
- <Text style={styles.centerStateTitle}>Selecione uma empresa</Text>
457
- <Text style={styles.centerStateText}>
458
- A configuracao da integracao depende da empresa ativa.
459
- </Text>
460
- </View>
461
- </SafeAreaView>
462
- );
463
- }
464
-
465
- if (loading) {
466
- return (
467
- <SafeAreaView style={[styles.container, { backgroundColor: brandColors.background }]} edges={['bottom']}>
468
- <View style={styles.centerState}>
469
- <ActivityIndicator size="large" color={providerConfig.accent} />
470
- <Text style={styles.centerStateTitle}>Carregando integracao</Text>
471
- <Text style={styles.centerStateText}>
472
- Buscando as credenciais salvas para a empresa ativa.
473
- </Text>
474
- </View>
475
- </SafeAreaView>
476
- );
477
- }
204
+ if (!providerConfig) return <CenterState icon="alert-triangle" iconColor="#e67e22" title="Integracao indisponivel" text="A tela solicitada nao possui configuracao cadastrada." />;
205
+ if (!providerId) return <CenterState icon="building" iconColor="#94A3B8" title="Selecione uma empresa" text="A configuracao da integracao depende da empresa ativa." />;
206
+ if (loading) return <CenterState backgroundColor={brandColors.background} spinnerColor={providerConfig.accent} title="Carregando integracao" text="Buscando as credenciais salvas para a empresa ativa." />;
478
207
 
479
208
  return (
480
209
  <SafeAreaView style={[styles.container, { backgroundColor: brandColors.background }]} edges={['bottom']}>
481
- <ScrollView
482
- contentContainerStyle={styles.scroll}
483
- showsVerticalScrollIndicator={false}
484
- refreshControl={
485
- <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={providerConfig.accent} />
486
- }>
210
+ <ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}
211
+ refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={providerConfig.accent} />}>
487
212
  {!embedded ? (
488
- <View style={[styles.heroCard, shadowStyle, { backgroundColor: providerConfig.accent }]}>
489
- <View style={styles.heroCopy}>
490
- <Text style={styles.heroEyebrow}>INTEGRACAO</Text>
491
- <Text style={styles.heroTitle}>{providerConfig.label}</Text>
492
- <Text style={styles.heroText}>
493
- {providerConfig.description}
494
- </Text>
495
- </View>
496
- <View style={styles.heroBadge}>
497
- <Icon name={providerConfig.icon} size={22} color={providerConfig.accent} />
498
- </View>
499
- </View>
500
- ) : (
501
- <View style={[styles.embeddedHeader, shadowStyle]}>
502
- <Text style={styles.embeddedTitle}>Configuracoes fiscais</Text>
213
+ <View style={[styles.heroCard, shadowStyle, { backgroundColor: providerConfig.accent }]}>
214
+ <View style={styles.heroCopy}>
215
+ <Text style={styles.heroEyebrow}>INTEGRACAO</Text>
216
+ <Text style={styles.heroTitle}>{providerConfig.label}</Text>
217
+ <Text style={styles.heroText}>{providerConfig.description}</Text>
218
+ </View>
219
+ <View style={styles.heroBadge}><Icon name={providerConfig.icon} size={22} color={providerConfig.accent} /></View>
503
220
  </View>
504
- )}
221
+ ) : <View style={[styles.embeddedHeader, shadowStyle]}><Text style={styles.embeddedTitle}>Configuracoes fiscais</Text></View>}
505
222
 
506
223
  {!embedded ? (
507
- <View style={[styles.statusCard, shadowStyle]}>
508
- <View style={styles.statusHeader}>
509
- <View style={styles.statusCopy}>
510
- <Text style={styles.sectionTitle}>Status</Text>
511
- <Text style={styles.sectionSubtitle}>
512
- {providerConfig.oauthConnect
224
+ <View style={[styles.statusCard, shadowStyle]}>
225
+ <View style={styles.statusHeader}>
226
+ <View style={styles.statusCopy}>
227
+ <Text style={styles.sectionTitle}>Status</Text>
228
+ <Text style={styles.sectionSubtitle}>{providerConfig.oauthConnect
513
229
  ? 'A integracao fica conectada quando o login do Uber termina e o store e salvo automaticamente.'
514
- : 'A integracao so aparece como conectada quando todos os campos obrigatorios foram salvos na empresa ativa.'}
515
- </Text>
516
- </View>
517
-
518
- <View
519
- style={[
520
- styles.statusBadge,
521
- { backgroundColor: withOpacity(statusTone, 0.12) },
522
- ]}>
523
- <Text style={[styles.statusBadgeText, { color: statusTone }]}>
524
- {statusText}
525
- </Text>
230
+ : 'A integracao so aparece como conectada quando todos os campos obrigatorios foram salvos na empresa ativa.'}</Text>
231
+ </View>
232
+ <View style={[styles.statusBadge, { backgroundColor: withOpacity(statusTone, 0.12) }]}>
233
+ <Text style={[styles.statusBadgeText, { color: statusTone }]}>{connected ? 'Conectado' : 'Pendente'}</Text>
234
+ </View>
526
235
  </View>
527
236
  </View>
528
- </View>
529
237
  ) : null}
530
238
 
531
239
  <View style={[styles.formCard, shadowStyle]}>
532
- <Text style={styles.cardTitle}>
533
- {providerConfig.oauthConnect
534
- ? 'Conexao'
535
- : fiscalTabs.length
536
- ? (activeTabDef?.label || 'Configuracoes')
537
- : 'Credenciais'}
538
- </Text>
539
- {!embedded ? (
540
- <Text style={styles.cardSubtitle}>
541
- {providerConfig.oauthConnect
542
- ? 'Use o login oficial do Uber. A store sera localizada e gravada automaticamente na empresa ativa.'
543
- : 'Salve as credenciais na empresa ativa. O hub de integracoes volta a mostrar o status correto quando voce retornar para a lista.'}
544
- </Text>
545
- ) : null}
240
+ <Text style={styles.cardTitle}>{providerConfig.oauthConnect ? 'Conexao' : fiscalTabs.length ? activeTabDef?.label || 'Configuracoes' : 'Credenciais'}</Text>
241
+ {!embedded ? <Text style={styles.cardSubtitle}>{providerConfig.oauthConnect
242
+ ? 'Use o login oficial do Uber. A store sera localizada e gravada automaticamente na empresa ativa.'
243
+ : 'Salve as credenciais na empresa ativa. O hub de integracoes volta a mostrar o status correto quando voce retornar para a lista.'}</Text> : null}
546
244
 
547
245
  {providerConfig.oauthConnect ? (
548
- <View style={styles.fieldList}>
549
- <View style={styles.fieldGroup}>
550
- <Text style={styles.fieldLabel}>Uber OAuth</Text>
551
- <Text style={styles.fieldKey}>
552
- Nao ha campos manuais. O login autoriza o app e salva o store automaticamente.
553
- </Text>
554
- </View>
555
- </View>
246
+ <View style={styles.fieldList}><View style={styles.fieldGroup}>
247
+ <Text style={styles.fieldLabel}>Uber OAuth</Text>
248
+ <Text style={styles.fieldKey}>Nao ha campos manuais. O login autoriza o app e salva o store automaticamente.</Text>
249
+ </View></View>
556
250
  ) : (
557
251
  <View style={styles.fieldList}>
558
- {fiscalTabs.length > 0 ? (
559
- <View style={styles.subTabRow}>
560
- {fiscalTabs.map(tab => {
561
- const selected = tab.key === (activeTabDef?.key || activeFiscalTab);
562
- return (
563
- <TouchableOpacity
564
- key={tab.key}
565
- style={[styles.subTabButton, selected && styles.subTabButtonActive]}
566
- activeOpacity={0.85}
567
- onPress={() => setActiveFiscalTab(tab.key)}>
568
- <Text
569
- style={[
570
- styles.subTabLabel,
571
- selected && styles.subTabLabelActive,
572
- ]}>
573
- {tab.label}
574
- </Text>
575
- </TouchableOpacity>
576
- );
577
- })}
578
- </View>
579
- ) : null}
580
- {activeTabDef?.description ? (
581
- <Text style={styles.tabDescription}>{activeTabDef.description}</Text>
582
- ) : null}
583
- {visibleFields.map(field => (
584
- <View key={field.key} style={styles.fieldGroup}>
585
- <Text style={styles.fieldLabel}>{field.label}</Text>
586
- {!embedded ? <Text style={styles.fieldKey}>{field.key}</Text> : null}
587
- {field.type === 'select' ? (
588
- <View style={styles.selectList}>
589
- {(field.options || []).map(option => {
590
- const selected = String(configValues[field.key] || '') === String(option.value);
591
- return (
592
- <TouchableOpacity
593
- key={String(option.value)}
594
- style={[
595
- styles.selectOption,
596
- selected && styles.selectOptionActive,
597
- !editable && styles.inputDisabled,
598
- ]}
599
- disabled={!editable}
600
- activeOpacity={0.85}
601
- onPress={() => updateField(field.key, String(option.value))}>
602
- <Text
603
- style={[
604
- styles.selectOptionText,
605
- selected && styles.selectOptionTextActive,
606
- ]}>
607
- {option.label}
608
- </Text>
609
- </TouchableOpacity>
610
- );
611
- })}
612
- </View>
613
- ) : field.type === 'file' ? (
614
- <View style={styles.fileFieldWrap}>
615
- {configValues[field.key] ? (
616
- <Text style={styles.fieldHint}>
617
- Arquivo vinculado (id: {String(configValues[field.key]).replace(/\D/g, '') || configValues[field.key]})
618
- </Text>
619
- ) : (
620
- <Text style={styles.fieldHint}>Nenhum certificado vinculado.</Text>
621
- )}
622
- <DefaultUpload
623
- relationStoreName="people"
624
- relationField="people"
625
- relationResource="people"
626
- entityId={providerId}
627
- companyId={providerId}
628
- context={field.fileContext || 'company_certificate'}
629
- libraryContexts={[field.fileContext || 'company_certificate']}
630
- acceptedTypes={field.accept || '.pfx,.p12,application/x-pkcs12'}
631
- fileType=""
632
- fileTypeLabel="certificado"
633
- title={field.label}
634
- triggerLabel="Gerenciar certificado"
635
- managerTitle="Gerenciador de arquivos"
636
- searchPlaceholder="Buscar certificado"
637
- uploadButtonLabel="Enviar certificado"
638
- emptyAttachmentLabel="Nenhum certificado anexado."
639
- emptyLibraryLabel="Nenhum arquivo encontrado."
640
- uploadSuccessMessage="Certificado enviado."
641
- attachSuccessMessage="Certificado vinculado."
642
- removeSuccessMessage="Certificado removido."
643
- showInlineContent={false}
644
- uploadResultAlreadyAttached
645
- requireEntity={false}
646
- onUploadFile={async ({ file, companyId, context, entityId }) => {
647
- const uploaded = await uploadFileToApi({
648
- file,
649
- context: context || field.fileContext || 'company_certificate',
650
- peopleId: companyId || providerId,
651
- entityId: entityId || providerId,
652
- });
653
- const id = extractFileId(uploaded);
654
- const iri = toFileIri(uploaded);
655
- const value = id ? String(id) : iri || '';
656
- if (!value) {
657
- throw new Error('Upload sem identificador de arquivo.');
658
- }
659
- updateField(field.key, value);
660
- return uploaded;
661
- }}
662
- onAttachFile={async fileObj => {
663
- const id = extractFileId(fileObj);
664
- const iri = toFileIri(fileObj);
665
- const value = id ? String(id) : iri || '';
666
- if (!value) {
667
- throw new Error('Arquivo sem identificador.');
668
- }
669
- updateField(field.key, value);
670
- return fileObj;
671
- }}
672
- onRemoveAttachment={async () => {
673
- updateField(field.key, '');
674
- return true;
675
- }}
676
- renderTrigger={({ openManager, uploading }) => (
677
- <TouchableOpacity
678
- style={[
679
- styles.filePickerButton,
680
- !editable && styles.inputDisabled,
681
- ]}
682
- disabled={!editable || uploading}
683
- activeOpacity={0.85}
684
- onPress={openManager}>
685
- {uploading ? (
686
- <ActivityIndicator color="#166534" />
687
- ) : (
688
- <Icon name="folder" size={16} color="#166534" />
689
- )}
690
- <Text style={styles.filePickerButtonText}>
691
- {configValues[field.key]
692
- ? 'Trocar certificado (gerenciador)'
693
- : 'Selecionar / enviar certificado'}
694
- </Text>
695
- </TouchableOpacity>
696
- )}
697
- />
698
- </View>
699
- ) : (
700
- <TextInput
701
- style={[
702
- styles.input,
703
- !editable && styles.inputDisabled,
704
- ]}
705
- value={configValues[field.key] || ''}
706
- onChangeText={value => updateField(field.key, value)}
707
- editable={editable}
708
- autoCapitalize="none"
709
- autoCorrect={false}
710
- secureTextEntry={Boolean(field.secureTextEntry)}
711
- placeholder={field.placeholder}
712
- />
713
- )}
714
- </View>
715
- ))}
252
+ {fiscalTabs.length ? <View style={styles.subTabRow}>{fiscalTabs.map(tab => {
253
+ const selected = tab.key === (activeTabDef?.key || activeFiscalTab);
254
+ return <TouchableOpacity key={tab.key} style={[styles.subTabButton, selected && styles.subTabButtonActive]}
255
+ activeOpacity={0.85} onPress={() => setActiveFiscalTab(tab.key)}>
256
+ <Text style={[styles.subTabLabel, selected && styles.subTabLabelActive]}>{tab.label}</Text>
257
+ </TouchableOpacity>;
258
+ })}</View> : null}
259
+ {activeTabDef?.description ? <Text style={styles.tabDescription}>{activeTabDef.description}</Text> : null}
260
+ <IntegrationConfigFields fields={visibleFields} configValues={configValues} editable={editable}
261
+ embedded={embedded} providerId={providerId} updateField={updateField} />
716
262
  </View>
717
263
  )}
718
264
 
719
- <TouchableOpacity
720
- style={[
721
- styles.saveButton,
722
- { backgroundColor: providerConfig.accent },
723
- actionDisabled && styles.saveButtonDisabled,
724
- ]}
725
- disabled={actionDisabled}
726
- activeOpacity={0.9}
727
- onPress={providerConfig.oauthConnect ? handleOAuthConnect : saveIntegration}>
728
- {actionLoading ? (
729
- <ActivityIndicator color="#FFFFFF" />
730
- ) : (
731
- <Icon name={providerConfig.oauthConnect ? 'log-in' : 'save'} size={16} color="#FFFFFF" />
732
- )}
733
- <Text style={styles.saveButtonText}>
734
- {providerConfig.oauthConnect
735
- ? providerConfig.connectLabel || 'Conectar'
736
- : providerConfig.saveLabel}
737
- </Text>
265
+ <TouchableOpacity style={[styles.saveButton, { backgroundColor: providerConfig.accent }, actionDisabled && styles.saveButtonDisabled]}
266
+ disabled={actionDisabled} activeOpacity={0.9} onPress={providerConfig.oauthConnect ? handleOAuthConnect : saveIntegration}>
267
+ {actionLoading ? <ActivityIndicator color="#FFFFFF" /> : <Icon name={providerConfig.oauthConnect ? 'log-in' : 'save'} size={16} color="#FFFFFF" />}
268
+ <Text style={styles.saveButtonText}>{providerConfig.oauthConnect ? providerConfig.connectLabel || 'Conectar' : providerConfig.saveLabel}</Text>
738
269
  </TouchableOpacity>
739
270
  </View>
740
271
  </ScrollView>