@controleonline/ui-common 1.2.86 → 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.
Files changed (73) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/manager-worker.yml +7 -156
  3. package/.github/workflows/publish-npm.yml +73 -27
  4. package/.github/workflows/pull-request-checks.yml +7 -5
  5. package/docs/technical/Device-Detail-Alias-UI-Sync.md +141 -0
  6. package/package.json +13 -8
  7. package/src/react/components/AddImportModal.js +61 -34
  8. package/src/react/components/BottomNavigationBar.js +178 -174
  9. package/src/react/components/BottomNavigationBar.styles.js +139 -118
  10. package/src/react/components/DefaultProvider.native.js +427 -421
  11. package/src/react/components/DefaultProvider.web.js +131 -125
  12. package/src/react/components/RemoteCheckoutService.js +3 -3
  13. package/src/react/components/RuntimeFooterMarqueeText.js +88 -36
  14. package/src/react/components/RuntimeInfoFooter.js +76 -43
  15. package/src/react/components/RuntimeInfoFooter.styles.js +22 -14
  16. package/src/react/config/deviceConfigBootstrap.js +3 -3
  17. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +4 -22
  18. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +6 -2
  19. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +4 -0
  20. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +23 -338
  21. package/src/react/pages/Devices/detail/createDeviceDetailContext.js +20 -0
  22. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +11 -36
  23. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +129 -37
  24. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +11 -38
  25. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +9 -28
  26. package/src/react/pages/Imports.js +13 -11
  27. package/src/react/pages/IntegrationConfigFields.js +1 -0
  28. package/src/react/pages/IntegrationConfigPage.js +101 -45
  29. package/src/react/pages/IntegrationConfigPage.utils.js +21 -13
  30. package/src/react/pages/fetchPeopleConfigs.js +21 -0
  31. package/src/react/pages/integrationsCatalog.js +15 -110
  32. package/src/react/services/Cielo/Cielo.js +9 -9
  33. package/src/react/services/Getnet/Getnet.web.js +25 -0
  34. package/src/react/utils/companyAuthority.js +36 -0
  35. package/src/react/utils/fileUrl.js +14 -5
  36. package/src/react/utils/paymentDevices.js +375 -270
  37. package/src/react/utils/printerDevices.js +2 -2
  38. package/src/react/utils/runtimeFooter.js +178 -224
  39. package/src/react/utils/runtimeFooterText.js +160 -0
  40. package/src/react/utils/runtimeLanguage.js +5 -5
  41. package/src/react/utils/runtimeZoom.js +29 -5
  42. package/src/react/utils/shopFranchises.js +167 -12
  43. package/src/react/utils/storeColumns.js +235 -217
  44. package/src/react/utils/systemErrorMessage.js +2 -1
  45. package/src/store/imports/customActions.js +42 -2
  46. package/src/store/integration/index.js +43 -0
  47. package/src/tests/browser/manager/device-detail-apply-config-defined.spec.js +31 -0
  48. package/src/tests/browser/manager/device-detail-composition.spec.js +295 -0
  49. package/src/tests/browser/manager/device-detail-copy-config.spec.js +11 -1
  50. package/src/tests/browser/manager/device-detail-pdv-current-company.spec.js +273 -0
  51. package/src/tests/browser/manager/runtime-footer-rotation.spec.js +397 -0
  52. package/src/tests/react/components/DefaultProviderAuthority.test.js +28 -0
  53. package/src/tests/react/components/RuntimeFooterMarqueeText.build.test.js +29 -0
  54. package/src/tests/react/config/deviceConfigBootstrap.test.js +24 -1
  55. package/src/tests/react/pages/applyCurrentDeviceConfig.test.js +85 -0
  56. package/src/tests/react/pages/deviceDetailComposition.test.js +65 -0
  57. package/src/tests/react/pages/deviceDetailHookDependencies.test.mjs +31 -0
  58. package/src/tests/react/pages/deviceDetailLoaderDependencies.test.mjs +28 -0
  59. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +27 -6
  60. package/src/tests/react/services/Cielo.test.js +2 -2
  61. package/src/tests/react/utils/companyAuthority.test.js +53 -0
  62. package/src/tests/react/utils/formatter.test.mjs +6 -0
  63. package/src/tests/react/utils/paymentDevices.test.js +72 -0
  64. package/src/tests/react/utils/runtimeFooter.test.js +35 -0
  65. package/src/tests/react/utils/runtimeFooterText.test.js +88 -0
  66. package/src/tests/react/utils/runtimeLanguage.test.js +4 -4
  67. package/src/tests/react/utils/runtimeZoom.test.js +20 -0
  68. package/src/tests/react/utils/shopFranchises.test.js +32 -13
  69. package/src/tests/react/utils/translate.test.mjs +6 -6
  70. package/src/utils/formatter.js +7 -3
  71. package/src/utils/translate.js +16 -16
  72. package/src/utils/translatePending.js +6 -6
  73. package/src/utils/translateStorage.js +2 -2
@@ -1,44 +1,17 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { useNavigation, useRoute } from '@react-navigation/native';
3
- import { useStore } from '@store';
4
- import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
5
- import { resolveThemePalette } from '@controleonline/../../src/styles/branding';
6
- import { colors } from '@controleonline/../../src/styles/colors';
7
- import packageJson from '@package';
8
- import {
9
- canDisplayChangePrinter, DEVICE_ANDROID_KIOSK_ENABLED_CONFIG_KEY, DEVICE_ANDROID_LAUNCHER_ENABLED_CONFIG_KEY,
10
- DISPLAY_AUTO_PRINT_PRODUCT_CONFIG_KEY, DISPLAY_ALLOW_PRINTER_CHANGE_CONFIG_KEY, DEVICE_ALERT_SOUND_ENABLED_KEY,
11
- DEVICE_ALERT_SOUND_URL_KEY, DEVICE_ORDER_VISIBILITY_COMPANY, DEVICE_ORDER_VISIBILITY_DEVICE, DEVICE_ORDER_VISIBILITY_KEY,
12
- DEVICE_RUNTIME_DEBUG_INFO_ENABLED_KEY, isPosAutoPrintEnabled, isPosCashRegisterOpen, isTruthyValue, parseConfigsObject,
13
- POS_AUTO_PRINT_ENABLED_CONFIG_KEY, POS_CASH_MANAGEMENT_MODE_CASH_REGISTER, POS_CASH_MANAGEMENT_MODE_CONFIG_KEY,
14
- POS_CASH_MANAGEMENT_MODE_DAILY, POS_CHECK_ORDER_MANAGEMENT_MODE_CONFIG_KEY, POS_CHECK_ORDER_MANAGEMENT_MODE_EXISTING_ONLY,
15
- POS_CHECK_ORDER_MANAGEMENT_MODE_MANAGE, POS_CHECK_ORDER_TYPE_CONFIG_KEY, POS_CHECK_ORDER_TYPE_NONE, POS_CHECK_ORDER_TYPE_TAB,
16
- POS_CHECK_ORDER_TYPE_TABLE, POS_CHECK_ORDER_TYPE_STAMP, POS_DELIVERY_ENABLED_CONFIG_KEY, POS_OPERATION_MODE_COUNTER,
17
- POS_OPERATION_MODE_CONFIG_KEY, POS_OPERATION_MODE_OPTIONS, POS_PRODUCT_SHOWCASE_CONFIG_KEY, POS_PRINT_MODE_FORM,
18
- POS_PRINT_MODE_ORDER, getPosOperationModeOption, isAndroidKioskEnabled, isAndroidLauncherEnabled, isPosDeliveryEnabled,
19
- resolvePosCheckOrderManagementMode, resolvePosCheckOrderType, resolvePosCheckOrderTypeForShop, resolveDeviceOrderVisibility,
20
- resolvePosCashManagementMode, resolvePosOperationMode, resolvePosPrintMode,
21
- } from '@controleonline/ui-common/src/react/config/deviceConfigBootstrap';
22
- import {
23
- filterDeviceConfigsByCompany, getCompanyPaymentDeviceOptions, getPaymentGatewayFromConfigs, getPaymentGatewayLabel,
24
- isPaymentCapableDeviceConfig, isPdvPrinterEnabled, normalizeDeviceId, normalizeEntityId, PAYMENT_TYPE_IDS_CONFIG_KEY,
25
- PAYMENT_GATEWAY_CIELO, PDV_PRINTER_ENABLED_CONFIG_KEY, ORDER_PAYMENT_DEVICE_CONFIG_KEY, POS_GATEWAY_CONFIG_KEY,
26
- } from '@controleonline/ui-common/src/react/utils/paymentDevices';
27
- import { normalizeBooleanConfig, SHOP_LOYALTY_COUPONS_ENABLED_CONFIG_KEY } from '@controleonline/ui-common/src/react/utils/shopConfig';
28
- import { getPrinterOptionValue, getDeviceTypeLabel, getPrinterLabel, getPrinterOptions } from '@controleonline/ui-common/src/react/utils/printerDevices';
29
- import { hex, DISPLAY_DEVICE_TYPE, PDV_DEVICE_TYPE, DISPLAY_DEVICE_LINK_CONFIG_KEY, DISPLAY_DEVICE_PRINTER_CONFIG_KEY,
30
- PDV_TAB_OPERATION, PDV_TAB_ORDERS, PDV_TAB_DEVICE, PDV_TAB_PAYMENT_TYPES, PDV_TAB_MOVEMENT, PDV_DETAIL_TABS, tt,
31
- } from './deviceDetailConstants';
32
- import { paymentIcon, formatApiError, getDisplayLabel, getProductShowcaseLabel, getIsOpen, confirm } from './deviceDetailHelpers';
33
- import { api } from '@controleonline/ui-common/src/api';
34
- import { buildDeviceAliasStoreUpdates } from '@controleonline/ui-common/src/react/utils/deviceAliasSync';
35
- import { createDeviceDetailRenderers } from './DeviceDetailRenderers';
36
- import { Alert, Platform } from 'react-native';
1
+ import { useCallback, useMemo } from 'react';
2
+
3
+ import { DEVICE_ANDROID_LAUNCHER_ENABLED_CONFIG_KEY, DISPLAY_AUTO_PRINT_PRODUCT_CONFIG_KEY, DISPLAY_ALLOW_PRINTER_CHANGE_CONFIG_KEY, DEVICE_ALERT_SOUND_ENABLED_KEY, DEVICE_ALERT_SOUND_URL_KEY, DEVICE_ORDER_VISIBILITY_DEVICE, DEVICE_ORDER_VISIBILITY_KEY, DEVICE_RUNTIME_DEBUG_INFO_ENABLED_KEY, POS_DELIVERY_ENABLED_CONFIG_KEY } from '@controleonline/ui-common/src/react/config/deviceConfigBootstrap';
4
+ import { normalizeDeviceId, PAYMENT_GATEWAY_CIELO } from '@controleonline/ui-common/src/react/utils/paymentDevices';
37
5
 
6
+ import { hex, DISPLAY_DEVICE_LINK_CONFIG_KEY, DISPLAY_DEVICE_PRINTER_CONFIG_KEY, PDV_TAB_OPERATION, PDV_TAB_ORDERS, PDV_TAB_DEVICE, PDV_TAB_PAYMENT_TYPES, PDV_TAB_MOVEMENT } from './deviceDetailConstants';
7
+ import { confirm } from './deviceDetailHelpers';
8
+
9
+ import { createDeviceDetailRenderers } from './DeviceDetailRenderers';
38
10
 
39
11
  export default function useDeviceDetailSaves(deps) {
40
12
  const {
41
- navigation, deviceId, deviceConfigStore, messageApi, websocketActions, runtimeCompanyConfigs, showSystemError, loyaltyCouponsEnabled, currentDevice, currentDeviceConfig, deviceString, deviceType,
13
+ themeColors,
14
+ navigation, deviceId, deviceConfigStore, messageApi, websocketActions, runtimeCompanyConfigs, showSystemError, loyaltyCouponsEnabled, currentCompany, currentDevice, currentDeviceConfig, deviceString, deviceType,
42
15
  normalizedInitialConfigs, initialAlias, isDisplayDevice, isPdvDevice, actionsRef, stampAutoDisableSignatureRef, brandColors, products, setProducts, productShowcases, setProductShowcases, companyDeviceConfigs,
43
16
  setCompanyDeviceConfigs, inflowData, setInflowData, configs, setConfigs, loadingConfigData, setLoadingConfigData, loadingCompanyDeviceConfigs, setLoadingCompanyDeviceConfigs, loadingMovementData, setLoadingMovementData, actionLoading,
44
17
  setActionLoading, activePdvTab, setActivePdvTab, savingPaymentTarget, setSavingPaymentTarget, savingPdvSettings, setSavingPdvSettings, savingPaymentTypes, setSavingPaymentTypes, savingPosOperationMode, setSavingPosOperationMode, savingProductShowcase,
@@ -401,8 +374,8 @@ export default function useDeviceDetailSaves(deps) {
401
374
  const { renderProduct, renderHelpButton, renderSwitchRow, renderOptionButtons } =
402
375
  createDeviceDetailRenderers({ themeColors, palette: brandColors });
403
376
 
404
-
405
377
  return {
378
+ renderProduct, renderHelpButton, renderSwitchRow, renderOptionButtons,
406
379
  saveLauncherMode,
407
380
  saveDeviceAlertSoundConfig,
408
381
  saveDeviceOrderVisibility,
@@ -5,36 +5,14 @@ import { useMessage } from '@controleonline/ui-common/src/react/components/Messa
5
5
  import { resolveThemePalette } from '@controleonline/../../src/styles/branding';
6
6
  import { colors } from '@controleonline/../../src/styles/colors';
7
7
  import packageJson from '@package';
8
- import {
9
- canDisplayChangePrinter, DEVICE_ANDROID_KIOSK_ENABLED_CONFIG_KEY, DEVICE_ANDROID_LAUNCHER_ENABLED_CONFIG_KEY,
10
- DISPLAY_AUTO_PRINT_PRODUCT_CONFIG_KEY, DISPLAY_ALLOW_PRINTER_CHANGE_CONFIG_KEY, DEVICE_ALERT_SOUND_ENABLED_KEY,
11
- DEVICE_ALERT_SOUND_URL_KEY, DEVICE_ORDER_VISIBILITY_COMPANY, DEVICE_ORDER_VISIBILITY_DEVICE, DEVICE_ORDER_VISIBILITY_KEY,
12
- DEVICE_RUNTIME_DEBUG_INFO_ENABLED_KEY, isPosAutoPrintEnabled, isPosCashRegisterOpen, isTruthyValue, parseConfigsObject,
13
- POS_AUTO_PRINT_ENABLED_CONFIG_KEY, POS_CASH_MANAGEMENT_MODE_CASH_REGISTER, POS_CASH_MANAGEMENT_MODE_CONFIG_KEY,
14
- POS_CASH_MANAGEMENT_MODE_DAILY, POS_CHECK_ORDER_MANAGEMENT_MODE_CONFIG_KEY, POS_CHECK_ORDER_MANAGEMENT_MODE_EXISTING_ONLY,
15
- POS_CHECK_ORDER_MANAGEMENT_MODE_MANAGE, POS_CHECK_ORDER_TYPE_CONFIG_KEY, POS_CHECK_ORDER_TYPE_NONE, POS_CHECK_ORDER_TYPE_TAB,
16
- POS_CHECK_ORDER_TYPE_TABLE, POS_CHECK_ORDER_TYPE_STAMP, POS_DELIVERY_ENABLED_CONFIG_KEY, POS_OPERATION_MODE_COUNTER,
17
- POS_OPERATION_MODE_CONFIG_KEY, POS_OPERATION_MODE_OPTIONS, POS_PRODUCT_SHOWCASE_CONFIG_KEY, POS_PRINT_MODE_FORM,
18
- POS_PRINT_MODE_ORDER, getPosOperationModeOption, isAndroidKioskEnabled, isAndroidLauncherEnabled, isPosDeliveryEnabled,
19
- resolvePosCheckOrderManagementMode, resolvePosCheckOrderType, resolvePosCheckOrderTypeForShop, resolveDeviceOrderVisibility,
20
- resolvePosCashManagementMode, resolvePosOperationMode, resolvePosPrintMode,
21
- } from '@controleonline/ui-common/src/react/config/deviceConfigBootstrap';
22
- import {
23
- filterDeviceConfigsByCompany, getCompanyPaymentDeviceOptions, getPaymentGatewayFromConfigs, getPaymentGatewayLabel,
24
- isPaymentCapableDeviceConfig, isPdvPrinterEnabled, normalizeDeviceId, normalizeEntityId, PAYMENT_TYPE_IDS_CONFIG_KEY,
25
- PAYMENT_GATEWAY_CIELO, PDV_PRINTER_ENABLED_CONFIG_KEY, ORDER_PAYMENT_DEVICE_CONFIG_KEY, POS_GATEWAY_CONFIG_KEY,
26
- } from '@controleonline/ui-common/src/react/utils/paymentDevices';
8
+ import { canDisplayChangePrinter, DISPLAY_AUTO_PRINT_PRODUCT_CONFIG_KEY, DEVICE_ALERT_SOUND_ENABLED_KEY, DEVICE_ALERT_SOUND_URL_KEY, DEVICE_RUNTIME_DEBUG_INFO_ENABLED_KEY, isPosAutoPrintEnabled, isTruthyValue, parseConfigsObject, POS_PRODUCT_SHOWCASE_CONFIG_KEY, getPosOperationModeOption, isAndroidKioskEnabled, isAndroidLauncherEnabled, isPosDeliveryEnabled, resolvePosCheckOrderManagementMode, resolvePosCheckOrderTypeForShop, resolveDeviceOrderVisibility, resolvePosCashManagementMode, resolvePosOperationMode, resolvePosPrintMode } from '@controleonline/ui-common/src/react/config/deviceConfigBootstrap';
9
+ import { filterDeviceConfigsByCompany, getCompanyPaymentDeviceOptions, getPaymentGatewayFromConfigs, isPaymentCapableDeviceConfig, isPdvPrinterEnabled, normalizeDeviceId, normalizeEntityId, ORDER_PAYMENT_DEVICE_CONFIG_KEY } from '@controleonline/ui-common/src/react/utils/paymentDevices';
27
10
  import { normalizeBooleanConfig, SHOP_LOYALTY_COUPONS_ENABLED_CONFIG_KEY } from '@controleonline/ui-common/src/react/utils/shopConfig';
28
- import { getPrinterOptionValue, getDeviceTypeLabel, getPrinterLabel, getPrinterOptions } from '@controleonline/ui-common/src/react/utils/printerDevices';
29
- import { hex, DISPLAY_DEVICE_TYPE, PDV_DEVICE_TYPE, DISPLAY_DEVICE_LINK_CONFIG_KEY, DISPLAY_DEVICE_PRINTER_CONFIG_KEY,
30
- PDV_TAB_OPERATION, PDV_TAB_ORDERS, PDV_TAB_DEVICE, PDV_TAB_PAYMENT_TYPES, PDV_TAB_MOVEMENT, PDV_DETAIL_TABS, tt,
31
- } from './deviceDetailConstants';
32
- import { paymentIcon, formatApiError, getDisplayLabel, getProductShowcaseLabel, getIsOpen, confirm } from './deviceDetailHelpers';
33
- import { api } from '@controleonline/ui-common/src/api';
34
- import { buildDeviceAliasStoreUpdates } from '@controleonline/ui-common/src/react/utils/deviceAliasSync';
35
- import { createDeviceDetailRenderers } from './DeviceDetailRenderers';
36
- import { Alert, Platform } from 'react-native';
11
+ import { getPrinterOptions } from '@controleonline/ui-common/src/react/utils/printerDevices';
12
+ import { DISPLAY_DEVICE_TYPE, PDV_DEVICE_TYPE, DISPLAY_DEVICE_LINK_CONFIG_KEY, DISPLAY_DEVICE_PRINTER_CONFIG_KEY, PDV_TAB_OPERATION } from './deviceDetailConstants';
13
+ import { formatApiError, getIsOpen } from './deviceDetailHelpers';
37
14
 
15
+ import { Platform } from 'react-native';
38
16
 
39
17
  export default function useDeviceDetailStateA() {
40
18
  const route = useRoute();
@@ -485,5 +463,8 @@ export default function useDeviceDetailStateA() {
485
463
  runtimeDeviceId,
486
464
  runtimeDeviceType,
487
465
  currentCompany,
466
+ runtimeDevice, runtimeDeviceConfig, themeColors,
467
+ displayStore,
468
+ printerStore,
488
469
  };
489
470
  }
@@ -27,7 +27,7 @@ const {
27
27
 
28
28
  const formatDate = (dateString) => {
29
29
  const d = new Date(dateString);
30
- return d.toLocaleString(); // Formato local legível
30
+ return d.toLocaleString();
31
31
  };
32
32
 
33
33
  const Imports = ({ context = {}, onClose }) => {
@@ -57,6 +57,12 @@ const Imports = ({ context = {}, onClose }) => {
57
57
  const importType = context.context;
58
58
  const title = context.title;
59
59
  const searchPlaceholder = context.searchPlaceholder;
60
+ const allowedExtensions = Array.isArray(context.allowedExtensions)
61
+ ? context.allowedExtensions
62
+ : importType === 'invoice_tax' || importType === 'xml'
63
+ ? ['xml', 'zip']
64
+ : ['csv'];
65
+ const showCsvTemplate = allowedExtensions.includes('csv');
60
66
  const {
61
67
  refreshLabel,
62
68
  processingLabel,
@@ -255,11 +261,9 @@ const Imports = ({ context = {}, onClose }) => {
255
261
  });
256
262
 
257
263
  const csvText = typeof response === 'string' ? response : await response.text?.();
258
- console.log('CSV recebido:', csvText.slice(0, 200));
259
264
 
260
265
  if (!csvText) throw new Error('CSV vazio');
261
266
 
262
- // mobile: verifica se o diretório está disponível
263
267
  const dirUri = Directory?.cacheDocumentDirectory || Directory?.documentDirectory;
264
268
 
265
269
  if (dirUri) {
@@ -272,21 +276,18 @@ const Imports = ({ context = {}, onClose }) => {
272
276
  await Sharing.shareAsync(fileUri);
273
277
  }
274
278
 
275
- console.log(`Arquivo CSV criado em: ${fileUri}`);
276
279
  return fileUri;
277
280
  } else if (typeof window !== 'undefined') {
278
- // fallback web
279
281
  const blob = new Blob([csvText], { type: 'text/csv' });
280
282
  const link = document.createElement('a');
281
283
  link.href = URL.createObjectURL(blob);
282
284
  link.download = `modelo_${importType}.csv`;
283
285
  link.click();
284
286
  URL.revokeObjectURL(link.href);
285
- console.log('Download CSV disparado no web');
286
287
  return 'web-download';
287
- } else {
288
- throw new Error('FileSystem não disponível');
289
288
  }
289
+
290
+ throw new Error('FileSystem não disponível');
290
291
  } catch (err) {
291
292
  console.error('Erro detalhado ao baixar CSV:', err);
292
293
  throw new Error('Falha ao baixar o modelo CSV');
@@ -306,6 +307,7 @@ const Imports = ({ context = {}, onClose }) => {
306
307
  <View style={styles.subHeader}>
307
308
  <View style={styles.topRow}>
308
309
  <View style={styles.topRowActions}>
310
+ {showCsvTemplate ? (
309
311
  <TouchableOpacity style={styles.downloadButton} onPress={downloadTemplate}>
310
312
  <View
311
313
  style={{
@@ -323,6 +325,7 @@ const Imports = ({ context = {}, onClose }) => {
323
325
  <Text style={[styles.downloadText, { color: buttonPalette.buttonText }]}>Baixar Modelo</Text>
324
326
  </View>
325
327
  </TouchableOpacity>
328
+ ) : null}
326
329
 
327
330
  <TouchableOpacity
328
331
  style={[
@@ -397,16 +400,15 @@ const Imports = ({ context = {}, onClose }) => {
397
400
  visible={showAddImportModal}
398
401
  onClose={() => setShowAddImportModal(false)}
399
402
  context={context}
403
+ importType={importType}
404
+ allowedExtensions={allowedExtensions}
400
405
  onSuccess={() => {
401
406
  setCurrentPage(1);
402
407
  fetchImports(searchQuery, 1);
403
- if (onImportComplete) onImportComplete();
404
408
  }}
405
- onImportComplete={onImportComplete}
406
409
  />
407
410
  </View>
408
411
  );
409
412
  };
410
413
 
411
414
  export default Imports;
412
- // TODO(store-first): quando este arquivo for mexido, mover a leitura para stores, remover api.fetch e evitar repassar dados em objetos quando o store ja resolver isso.
@@ -23,6 +23,7 @@ const FileField = ({ field, value, editable, providerId, updateField }) => (
23
23
  acceptedTypes={field.accept || '.pfx,.p12,application/x-pkcs12'}
24
24
  fileType=""
25
25
  fileTypeLabel="certificado"
26
+ knownFileIds={[extractFileId(value)].filter(Boolean)}
26
27
  title={field.label}
27
28
  triggerLabel="Gerenciar certificado"
28
29
  managerTitle="Gerenciador de arquivos"
@@ -9,19 +9,20 @@ import useToastMessage from '@controleonline/ui-crm/src/react/hooks/useToastMess
9
9
  import { useStore } from '@store';
10
10
  import { colors } from '@controleonline/../../src/styles/colors';
11
11
  import { resolveThemePalette, withOpacity } from '@controleonline/../../src/styles/branding';
12
- import { getIntegrationConfig, getIntegrationByKey, parseIntegrationCollection } from './integrationsCatalog';
12
+ import { getIntegrationConfig, getIntegrationByKey } from './integrationsCatalog';
13
+ import { fetchPeopleConfigs } from './fetchPeopleConfigs';
13
14
  import IntegrationConfigFields from './IntegrationConfigFields';
14
15
  import {
15
16
  ROUTE_PROVIDER_MAP,
16
- buildFieldValues,
17
17
  extractAuthorizationUrl,
18
18
  formatApiError,
19
19
  formatUberOAuthError,
20
20
  getConfigFields,
21
21
  isConnectedValue,
22
+ isMethodNotAllowed,
23
+ mergeTabValues,
22
24
  normalizeTextValue,
23
25
  openAuthorizationUrl,
24
- resolveFallbackConfigs,
25
26
  resolveProviderId,
26
27
  routeNameToPath,
27
28
  toConfigRequestValue,
@@ -44,13 +45,13 @@ const CenterState = ({ icon, iconColor, title, text, backgroundColor, spinnerCol
44
45
  </SafeAreaView>
45
46
  );
46
47
 
47
- export default function IntegrationConfigPage({ route, embedded = false }) {
48
+ export default function IntegrationConfigPage({ route, embedded = false, fiscalTab = '', onlyFiscalTab = false }) {
48
49
  const peopleStore = useStore('people');
49
50
  const themeStore = useStore('theme');
50
51
  const configsStore = useStore('configs');
51
52
  const { currentCompany } = peopleStore.getters;
52
53
  const { colors: themeColors } = themeStore.getters;
53
- const configActions = configsStore.actions;
54
+ const configActions = configsStore.actions || {};
54
55
  const { isSaving } = configsStore.getters;
55
56
  const { showError, showSuccess } = useToastMessage();
56
57
  const oauthNoticeRef = useRef('');
@@ -62,7 +63,8 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
62
63
  const providerConfig = useMemo(() => getIntegrationConfig(providerKey), [providerKey]);
63
64
  const configFields = useMemo(() => getConfigFields(providerConfig), [providerConfig]);
64
65
  const fiscalTabs = providerConfig?.tabs || [];
65
- const [activeFiscalTab, setActiveFiscalTab] = useState(() => fiscalTabs[0]?.key || 'general');
66
+ const requestedFiscalTab = fiscalTab || route?.params?.fiscalTab || '';
67
+ const [activeFiscalTab, setActiveFiscalTab] = useState(() => requestedFiscalTab || fiscalTabs[0]?.key || 'general');
66
68
  const activeTabDef = useMemo(
67
69
  () => fiscalTabs.find(tab => tab.key === activeFiscalTab) || fiscalTabs[0] || null,
68
70
  [activeFiscalTab, fiscalTabs],
@@ -71,10 +73,13 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
71
73
  () => (activeTabDef?.fields?.length ? activeTabDef.fields : configFields),
72
74
  [activeTabDef, configFields],
73
75
  );
74
-
75
76
  useEffect(() => {
77
+ if (requestedFiscalTab && fiscalTabs.some(tab => tab.key === requestedFiscalTab)) {
78
+ if (activeFiscalTab !== requestedFiscalTab) setActiveFiscalTab(requestedFiscalTab);
79
+ return;
80
+ }
76
81
  if (fiscalTabs.length && !fiscalTabs.some(tab => tab.key === activeFiscalTab)) setActiveFiscalTab(fiscalTabs[0].key);
77
- }, [activeFiscalTab, fiscalTabs]);
82
+ }, [activeFiscalTab, fiscalTabs, requestedFiscalTab]);
78
83
 
79
84
  const returnPath = useMemo(() => {
80
85
  const routePath = normalizeTextValue(route?.params?.return_path || route?.params?.returnPath || '');
@@ -88,28 +93,30 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
88
93
  [themeColors, currentCompany?.id],
89
94
  );
90
95
  const [loading, setLoading] = useState(true);
96
+ const [tabLoading, setTabLoading] = useState(false);
91
97
  const [refreshing, setRefreshing] = useState(false);
92
98
  const [authLoading, setAuthLoading] = useState(false);
93
99
  const [configValues, setConfigValues] = useState({});
94
100
  const [integrationSummary, setIntegrationSummary] = useState(null);
95
101
 
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
102
  const providerId = useMemo(
99
- () => resolveProviderId({ route, currentCompany }),
100
- [route?.params?.companyId, currentCompany?.id],
103
+ () => resolveProviderId({ route, currentCompany, embedded }),
104
+ [embedded, route?.params?.companyId, route?.params?.clientId, currentCompany?.id],
101
105
  );
102
106
  const providerIri = useMemo(() => (providerId ? `/people/${providerId}` : ''), [providerId]);
103
- const fallbackConfigs = useMemo(
104
- () => resolveFallbackConfigs({ providerId, currentCompany }),
105
- [providerId, currentCompany?.id, currentCompany?.configs],
107
+ const allConfigFields = useMemo(() => getConfigFields(providerConfig), [providerConfig]);
108
+ const allFieldKeys = useMemo(
109
+ () => allConfigFields.map(field => field.key).filter(Boolean),
110
+ [allConfigFields],
106
111
  );
112
+ const allFieldKeysSig = useMemo(() => allFieldKeys.join('|'), [allFieldKeys]);
113
+ // Pure config providers (e.g. receita-federal) do not use marketplace/integrations.
114
+ const needsMarketplaceIntegration = Boolean(providerConfig?.oauthConnect);
107
115
 
108
- const syncConfigValues = useCallback(source => {
109
- setConfigValues(providerConfig ? buildFieldValues(providerConfig, source) : {});
110
- }, [providerConfig]);
116
+ useEffect(() => {
117
+ setConfigValues({});
118
+ }, [providerIri]);
111
119
 
112
- useEffect(() => syncConfigValues(fallbackConfigs), [fallbackConfigs, syncConfigValues]);
113
120
  useEffect(() => {
114
121
  const oauthStatus = normalizeTextValue(route?.params?.oauth_status).toLowerCase();
115
122
  const oauthError = normalizeTextValue(route?.params?.oauth_error);
@@ -128,26 +135,49 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
128
135
  }
129
136
  if (showLoading) setLoading(true);
130
137
  try {
131
- const integrationPromise = api.fetch('/marketplace/integrations', { params: { provider_id: providerId } });
132
- if (configFields.length) {
133
- const [configResponse, integrationResponse] = await Promise.all([
134
- api.fetch('/configs', { params: { people: providerIri } }),
135
- integrationPromise,
136
- ]);
137
- syncConfigValues(parseIntegrationCollection(configResponse));
138
- setIntegrationSummary(getIntegrationByKey(integrationResponse, providerConfig.key));
138
+ const keys = allFieldKeys.length
139
+ ? allFieldKeys
140
+ : visibleFields.map(field => field.key).filter(Boolean);
141
+
142
+ if (keys.length) {
143
+ const configItems = await fetchPeopleConfigs({ peopleIri: providerIri, configKeys: keys });
144
+ const mergeFields = allConfigFields.length ? allConfigFields : visibleFields;
145
+ setConfigValues(current => mergeTabValues(mergeFields, configItems, current));
146
+ }
147
+
148
+ if (needsMarketplaceIntegration) {
149
+ try {
150
+ const integrationResponse = await api.fetch('/marketplace/integrations', {
151
+ params: { provider_id: providerId },
152
+ });
153
+ setIntegrationSummary(getIntegrationByKey(integrationResponse, providerConfig.key));
154
+ } catch (integrationError) {
155
+ // OAuth / marketplace providers still surface the error; pure config never hits this branch.
156
+ showError(formatApiError(integrationError));
157
+ setIntegrationSummary(null);
158
+ }
139
159
  } else {
140
- setIntegrationSummary(getIntegrationByKey(await integrationPromise, providerConfig.key));
160
+ setIntegrationSummary(null);
141
161
  }
142
162
  } catch (error) {
143
163
  showError(formatApiError(error));
144
- syncConfigValues(fallbackConfigs);
145
164
  setIntegrationSummary(null);
146
165
  } finally {
147
- if (showLoading) setLoading(false);
166
+ setLoading(false);
167
+ setTabLoading(false);
148
168
  }
149
- }, [configFields.length, fallbackConfigs, providerConfig, providerIri, providerId, showError, syncConfigValues]);
169
+ }, [
170
+ allConfigFields,
171
+ allFieldKeys,
172
+ allFieldKeysSig,
173
+ needsMarketplaceIntegration,
174
+ providerConfig,
175
+ providerIri,
176
+ providerId,
177
+ showError,
178
+ ]);
150
179
 
180
+ // Reload only when the company/provider identity or full key set changes — not on fiscal sub-tab switch.
151
181
  useFocusEffect(useCallback(() => { loadPageData(); }, [loadPageData]));
152
182
  const onRefresh = useCallback(async () => {
153
183
  setRefreshing(true);
@@ -157,6 +187,29 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
157
187
  setConfigValues(current => ({ ...current, [fieldKey]: value }));
158
188
  }, []);
159
189
 
190
+ const persistConfigs = useCallback(async payload => {
191
+ if (typeof configActions.addManyConfigs === 'function') {
192
+ try {
193
+ return await configActions.addManyConfigs(payload);
194
+ } catch (error) {
195
+ if (!isMethodNotAllowed(error)) throw error;
196
+ }
197
+ }
198
+ if (typeof configActions.addConfigs !== 'function') {
199
+ throw new Error('Nao foi possivel salvar: acao de configs indisponivel.');
200
+ }
201
+ for (const item of payload.configs) {
202
+ await configActions.addConfigs({
203
+ configKey: item.configKey,
204
+ configValue: item.configValue,
205
+ people: payload.people,
206
+ module: payload.module,
207
+ visibility: payload.visibility,
208
+ });
209
+ }
210
+ return true;
211
+ }, [configActions]);
212
+
160
213
  const handleOAuthConnect = useCallback(async () => {
161
214
  if (!providerIri || !providerConfig?.oauthConnect) {
162
215
  showError('Nao foi possivel identificar a integracao selecionada.');
@@ -181,29 +234,32 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
181
234
  ? isConnectedValue(integrationSummary.connected)
182
235
  : requiredKeys.length > 0 && requiredKeys.every(key => normalizeTextValue(configValues[key]) !== '');
183
236
  const statusTone = connected ? '#16A34A' : '#e67e22';
184
- const editable = Boolean(providerIri && providerConfig && !isSaving && !loading && !providerConfig.oauthConnect);
185
- const actionLoading = providerConfig?.oauthConnect ? authLoading : isSaving;
186
- const actionDisabled = providerConfig?.oauthConnect ? authLoading || loading : !editable;
237
+ const editable = Boolean(providerIri && providerConfig && !providerConfig.oauthConnect);
238
+ const actionLoading = providerConfig?.oauthConnect ? authLoading : Boolean(isSaving || tabLoading);
239
+ const actionDisabled = providerConfig?.oauthConnect ? authLoading || loading : !editable || actionLoading;
240
+ const saveLabel = fiscalTabs.length
241
+ ? `Salvar ${activeTabDef?.label || 'aba'}`
242
+ : providerConfig?.saveLabel;
187
243
 
188
244
  const saveIntegration = useCallback(async () => {
189
245
  if (!providerIri || !providerConfig || providerConfig.oauthConnect) {
190
246
  showError('Nao foi possivel identificar a integracao selecionada.');
191
247
  return;
192
248
  }
193
- const configs = configFields.map(field => ({
249
+ const configs = visibleFields.map(field => ({
194
250
  configKey: field.key,
195
251
  configValue: toConfigRequestValue(normalizeTextValue(configValues[field.key])),
196
252
  }));
197
253
  try {
198
- await configActions.addManyConfigs({ configs, people: providerIri, module: 4, visibility: 'public' });
199
- showSuccess(`${providerConfig.label} salvo com sucesso.`);
254
+ await persistConfigs({ configs, people: providerIri, module: 4, visibility: 'public' });
255
+ showSuccess(`${activeTabDef?.label || providerConfig.label} salvo com sucesso.`);
200
256
  await loadPageData({ showLoading: false });
201
257
  } catch (error) { showError(error?.message || 'Nao foi possivel salvar a integracao.'); }
202
- }, [configActions, configFields, configValues, loadPageData, providerConfig, providerIri, showError, showSuccess]);
258
+ }, [activeTabDef, configValues, loadPageData, persistConfigs, providerConfig, providerIri, showError, showSuccess, visibleFields]);
203
259
 
204
260
  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." />;
261
+ if (!providerId) return <CenterState icon="building" iconColor="#94A3B8" title="Empresa nao identificada" text="Abra a ficha da empresa para gravar as configuracoes fiscais nela." />;
262
+ if (loading) return <CenterState backgroundColor={brandColors.background} spinnerColor={providerConfig.accent} title="Carregando integracao" text="Buscando as credenciais desta aba." />;
207
263
 
208
264
  return (
209
265
  <SafeAreaView style={[styles.container, { backgroundColor: brandColors.background }]} edges={['bottom']}>
@@ -227,7 +283,7 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
227
283
  <Text style={styles.sectionTitle}>Status</Text>
228
284
  <Text style={styles.sectionSubtitle}>{providerConfig.oauthConnect
229
285
  ? 'A integracao fica conectada quando o login do Uber termina e o store e salvo automaticamente.'
230
- : 'A integracao so aparece como conectada quando todos os campos obrigatorios foram salvos na empresa ativa.'}</Text>
286
+ : 'A integracao so aparece como conectada quando todos os campos obrigatorios foram salvos nesta empresa.'}</Text>
231
287
  </View>
232
288
  <View style={[styles.statusBadge, { backgroundColor: withOpacity(statusTone, 0.12) }]}>
233
289
  <Text style={[styles.statusBadgeText, { color: statusTone }]}>{connected ? 'Conectado' : 'Pendente'}</Text>
@@ -240,7 +296,7 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
240
296
  <Text style={styles.cardTitle}>{providerConfig.oauthConnect ? 'Conexao' : fiscalTabs.length ? activeTabDef?.label || 'Configuracoes' : 'Credenciais'}</Text>
241
297
  {!embedded ? <Text style={styles.cardSubtitle}>{providerConfig.oauthConnect
242
298
  ? '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}
299
+ : 'Troca de aba e local; as configuracoes sao carregadas uma vez e cada aba grava so as chaves dela.'}</Text> : null}
244
300
 
245
301
  {providerConfig.oauthConnect ? (
246
302
  <View style={styles.fieldList}><View style={styles.fieldGroup}>
@@ -249,7 +305,7 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
249
305
  </View></View>
250
306
  ) : (
251
307
  <View style={styles.fieldList}>
252
- {fiscalTabs.length ? <View style={styles.subTabRow}>{fiscalTabs.map(tab => {
308
+ {fiscalTabs.length && !onlyFiscalTab ? <View style={styles.subTabRow}>{fiscalTabs.map(tab => {
253
309
  const selected = tab.key === (activeTabDef?.key || activeFiscalTab);
254
310
  return <TouchableOpacity key={tab.key} style={[styles.subTabButton, selected && styles.subTabButtonActive]}
255
311
  activeOpacity={0.85} onPress={() => setActiveFiscalTab(tab.key)}>
@@ -257,6 +313,7 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
257
313
  </TouchableOpacity>;
258
314
  })}</View> : null}
259
315
  {activeTabDef?.description ? <Text style={styles.tabDescription}>{activeTabDef.description}</Text> : null}
316
+ {tabLoading ? <ActivityIndicator color={providerConfig.accent} /> : null}
260
317
  <IntegrationConfigFields fields={visibleFields} configValues={configValues} editable={editable}
261
318
  embedded={embedded} providerId={providerId} updateField={updateField} />
262
319
  </View>
@@ -265,11 +322,10 @@ export default function IntegrationConfigPage({ route, embedded = false }) {
265
322
  <TouchableOpacity style={[styles.saveButton, { backgroundColor: providerConfig.accent }, actionDisabled && styles.saveButtonDisabled]}
266
323
  disabled={actionDisabled} activeOpacity={0.9} onPress={providerConfig.oauthConnect ? handleOAuthConnect : saveIntegration}>
267
324
  {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>
325
+ <Text style={styles.saveButtonText}>{providerConfig.oauthConnect ? providerConfig.connectLabel || 'Conectar' : saveLabel}</Text>
269
326
  </TouchableOpacity>
270
327
  </View>
271
328
  </ScrollView>
272
329
  </SafeAreaView>
273
330
  );
274
331
  }
275
- // TODO(store-first): quando este arquivo for mexido, mover a leitura para stores, remover api.fetch e evitar repassar dados em objetos quando o store ja resolver isso.
@@ -35,7 +35,7 @@ export const getConfigFields = providerConfig => {
35
35
  return providerConfig.fields || [];
36
36
  };
37
37
 
38
- export const buildFieldValues = (providerConfig, source) => {
38
+ export const mergeTabValues = (fields, source, current = {}) => {
39
39
  const sourceMap = Array.isArray(source)
40
40
  ? source.reduce((acc, item) => {
41
41
  const key = String(item?.configKey || '').trim();
@@ -44,23 +44,25 @@ export const buildFieldValues = (providerConfig, source) => {
44
44
  }, {})
45
45
  : source && typeof source === 'object' ? source : {};
46
46
 
47
- return getConfigFields(providerConfig).reduce((acc, field) => {
48
- acc[field.key] = normalizeTextValue(sourceMap[field.key]);
49
- return acc;
50
- }, {});
47
+ const next = { ...current };
48
+ (fields || []).forEach(field => {
49
+ next[field.key] = Object.prototype.hasOwnProperty.call(sourceMap, field.key)
50
+ ? normalizeTextValue(sourceMap[field.key])
51
+ : normalizeTextValue(next[field.key]);
52
+ });
53
+ return next;
51
54
  };
52
55
 
53
- export const resolveProviderId = ({ route, currentCompany }) => {
54
- const routeCompanyId = String(route?.params?.companyId || '').replace(/\D/g, '');
55
- if (routeCompanyId) return Number(routeCompanyId);
56
+ export const buildFieldValues = (providerConfig, source) =>
57
+ mergeTabValues(getConfigFields(providerConfig), source, {});
56
58
 
57
- const currentCompanyId = String(currentCompany?.id || '').replace(/\D/g, '');
58
- return currentCompanyId ? Number(currentCompanyId) : null;
59
+ export const resolveProviderId = ({ route, currentCompany, embedded = false }) => {
60
+ const fromRoute = String(route?.params?.companyId || route?.params?.clientId || '').replace(/\D/g, '');
61
+ if (fromRoute) return fromRoute;
62
+ if (embedded) return '';
63
+ return String(currentCompany?.id || '').replace(/\D/g, '');
59
64
  };
60
65
 
61
- export const resolveFallbackConfigs = ({ providerId, currentCompany }) =>
62
- Number(providerId) === Number(currentCompany?.id) ? currentCompany?.configs || {} : {};
63
-
64
66
  export const isConnectedValue = value =>
65
67
  value === true || value === 1 || value === '1' || String(value).trim().toLowerCase() === 'true';
66
68
 
@@ -93,3 +95,9 @@ export const openAuthorizationUrl = async authUrl => {
93
95
  }
94
96
  await Linking.openURL(authUrl);
95
97
  };
98
+
99
+ export const isMethodNotAllowed = error => {
100
+ const status = Number(error?.status || error?.code || error?.body?.status || 0);
101
+ const message = String(error?.message || '').toLowerCase();
102
+ return status === 404 || status === 405 || message.includes('method not allowed');
103
+ };
@@ -0,0 +1,21 @@
1
+ import { api } from '@controleonline/ui-common/src/api';
2
+ import { parseIntegrationCollection } from './integrationsCatalog';
3
+
4
+ export async function fetchPeopleConfigs({ peopleIri, configKeys = [] }) {
5
+ const keys = [...new Set(configKeys.map(key => String(key || '').trim()).filter(Boolean))];
6
+ if (!peopleIri || !keys.length) {
7
+ return [];
8
+ }
9
+
10
+ const response = await api.fetch('/configs', {
11
+ params: {
12
+ people: peopleIri,
13
+ configKey: keys,
14
+ itemsPerPage: keys.length,
15
+ },
16
+ });
17
+
18
+ return parseIntegrationCollection(response).filter(item =>
19
+ keys.includes(String(item?.configKey || '').trim()),
20
+ );
21
+ }