@controleonline/ui-common 1.2.71 → 1.2.75

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 (150) hide show
  1. package/.github/actions/workers/manager/action.yml +147 -0
  2. package/.github/actions/workers/qa/action.yml +44 -0
  3. package/.github/actions/workers/security/action.yml +44 -0
  4. package/.github/actions/workers/technical-documenter/action.yml +51 -0
  5. package/.github/workflows/manager-worker.yml +168 -0
  6. package/.github/workflows/publish-npm.yml +45 -0
  7. package/.gitmodules +3 -0
  8. package/AGENTS.md +6 -0
  9. package/README.md +14 -0
  10. package/package.json +3 -2
  11. package/src/api/fetch.js +1 -1
  12. package/src/api/index.js +92 -22
  13. package/src/react/components/AddImportModal.js +94 -110
  14. package/src/react/components/BottomNavigationBar.config.js +211 -231
  15. package/src/react/components/CategoryForm.js +52 -35
  16. package/src/react/components/DefaultProvider.native.js +131 -95
  17. package/src/react/components/DefaultProvider.web.js +148 -74
  18. package/src/react/components/NetworkPrinterPreviewModal.js +249 -0
  19. package/src/react/components/NetworkPrinterPreviewModal.styles.js +159 -0
  20. package/src/react/components/RuntimeInfoFooter.js +100 -36
  21. package/src/react/components/RuntimeInfoFooter.styles.js +3 -0
  22. package/src/react/components/UnifiedPaymentBar.js +7 -4
  23. package/src/react/components/UserAvatar.js +160 -0
  24. package/src/react/components/integrations/IntegrationHero/index.js +48 -0
  25. package/src/react/components/integrations/IntegrationHero/styles.js +57 -0
  26. package/src/react/components/integrations/IntegrationTabs/index.js +89 -0
  27. package/src/react/components/integrations/IntegrationTabs/styles.js +44 -0
  28. package/src/react/config/deviceConfigBootstrap.js +5 -3
  29. package/src/react/pages/Connections.js +229 -0
  30. package/src/react/pages/Connections.styles.js +222 -0
  31. package/src/react/pages/DeviceDetailPage.js +2814 -0
  32. package/src/react/pages/DeviceDetailPage.styles.js +308 -0
  33. package/src/react/pages/Devices/currentDevice.js +194 -0
  34. package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +257 -0
  35. package/src/react/pages/Devices/deviceTypes/all.js +23 -0
  36. package/src/react/pages/Devices/deviceTypes/device.js +25 -0
  37. package/src/react/pages/Devices/deviceTypes/deviceListHelpers.js +178 -0
  38. package/src/react/pages/Devices/deviceTypes/display.js +26 -0
  39. package/src/react/pages/Devices/deviceTypes/index.js +145 -0
  40. package/src/react/pages/Devices/deviceTypes/ipCamera.js +35 -0
  41. package/src/react/pages/Devices/deviceTypes/pdv.js +88 -0
  42. package/src/react/pages/Devices/deviceTypes/printer.js +37 -0
  43. package/src/react/pages/Devices/deviceTypes/shared.js +453 -0
  44. package/src/react/pages/Devices/deviceTypes/useDeviceNetworkConnectivity.js +145 -0
  45. package/src/react/pages/Devices.js +183 -0
  46. package/src/react/pages/Devices.styles.js +353 -0
  47. package/src/react/pages/Food99IntegrationPage/components/Food99CatalogTab.js +219 -0
  48. package/src/react/pages/Food99IntegrationPage/components/Food99OverviewTab.js +134 -0
  49. package/src/react/pages/Food99IntegrationPage/components/Food99PreviewModal.js +173 -0
  50. package/src/react/pages/Food99IntegrationPage/components/Food99ProductCard.js +1 -0
  51. package/src/react/pages/Food99IntegrationPage/components/Food99QuickWalletModal.js +181 -0
  52. package/src/react/pages/Food99IntegrationPage/components/Food99SettingsTab.js +306 -0
  53. package/src/react/pages/Food99IntegrationPage/components/Food99StoreTab.js +417 -0
  54. package/src/react/pages/Food99IntegrationPage/index.js +1530 -0
  55. package/src/react/pages/Food99IntegrationPage/styles.js +792 -0
  56. package/src/react/pages/Food99IntegrationPage/utils.js +155 -0
  57. package/src/react/pages/Food99IntegrationPage.js +1 -0
  58. package/src/react/pages/Food99IntegrationPage.styles.js +1 -0
  59. package/src/react/pages/GenericLogPage.js +42 -167
  60. package/src/react/pages/IFoodIntegrationPage/components/IFoodCatalogTab.js +8 -0
  61. package/src/react/pages/IFoodIntegrationPage/components/IFoodOperationsTab.js +420 -0
  62. package/src/react/pages/IFoodIntegrationPage/components/IFoodOverviewTab.js +118 -0
  63. package/src/react/pages/IFoodIntegrationPage/components/IFoodPreviewModal.js +103 -0
  64. package/src/react/pages/IFoodIntegrationPage/components/IFoodProductCard.js +1 -0
  65. package/src/react/pages/IFoodIntegrationPage/components/IFoodStoreTab.js +150 -0
  66. package/src/react/pages/IFoodIntegrationPage/index.js +1298 -0
  67. package/src/react/pages/IFoodIntegrationPage/styles.js +764 -0
  68. package/src/react/pages/IFoodIntegrationPage/utils.js +91 -0
  69. package/src/react/pages/IFoodIntegrationPage.js +1 -0
  70. package/src/react/pages/IFoodIntegrationPage.styles.js +1 -0
  71. package/src/react/pages/Imports.js +214 -166
  72. package/src/react/pages/IntegrationConfigPage.js +744 -0
  73. package/src/react/pages/IntegrationConfigPage.styles.js +269 -0
  74. package/src/react/pages/Integrations.js +299 -0
  75. package/src/react/pages/Integrations.styles.js +130 -0
  76. package/src/react/pages/ManagerOrderNotificationsPage.js +642 -0
  77. package/src/react/pages/MarketplaceIntegrationPage/components/MarketplaceProductCard.js +205 -0
  78. package/src/react/pages/MarketplaceIntegrationPage/components/RemoteMarketplaceMenuTab.js +335 -0
  79. package/src/react/pages/MarketplaceIntegrationPage/index.js +60 -0
  80. package/src/react/pages/MarketplaceIntegrationPage/utils.js +137 -0
  81. package/src/react/pages/MercadoLivreIntegrationPage.js +538 -0
  82. package/src/react/pages/PrinterDeviceDetailPage.js +1916 -0
  83. package/src/react/pages/PrinterDeviceDetailPage.styles.js +223 -0
  84. package/src/react/pages/PrinterDeviceFormPage.js +521 -0
  85. package/src/react/pages/PrinterDeviceFormPage.styles.js +159 -0
  86. package/src/react/pages/WhatsAppConnectionPage.js +459 -0
  87. package/src/react/pages/WhatsAppConnectionPage.styles.js +284 -0
  88. package/src/react/pages/integrationsCatalog.js +399 -0
  89. package/src/react/services/Cielo/Cielo.js +193 -193
  90. package/src/react/utils/categoryContexts.js +46 -0
  91. package/src/react/utils/entityDisplay.js +222 -217
  92. package/src/react/utils/fileUrl.js +39 -20
  93. package/src/react/utils/integrationPage.js +16 -0
  94. package/src/react/utils/runtimeFooter.js +40 -22
  95. package/src/react/utils/runtimeLanguage.js +50 -9
  96. package/src/react/utils/runtimeMenu.js +191 -131
  97. package/src/react/utils/runtimeZoom.js +43 -0
  98. package/src/react/utils/shopConfig.js +27 -15
  99. package/src/react/utils/shopFranchises.js +126 -1
  100. package/src/react/utils/storeColumns.js +23 -4
  101. package/src/react/utils/userAvatar.js +104 -0
  102. package/src/store/categories/index.js +48 -20
  103. package/src/store/device_config/index.js +62 -21
  104. package/src/store/entity_log/index.js +22 -1
  105. package/src/store/imports/customActions.js +51 -0
  106. package/src/store/imports/index.js +89 -88
  107. package/src/store/status/index.js +11 -11
  108. package/src/store/themes/index.js +22 -0
  109. package/src/store/timezones/index.js +3 -1
  110. package/src/tests/browser/manager/connections.spec.js +143 -0
  111. package/src/tests/browser/manager/devices-current.spec.js +343 -0
  112. package/src/tests/browser/manager/mercado-livre-integration.spec.js +268 -0
  113. package/src/tests/react/api/deviceHeaders.test.js +114 -0
  114. package/src/tests/react/components/AddImportModal.test.js +166 -0
  115. package/src/tests/react/components/BottomNavigationBar.config.test.js +20 -0
  116. package/src/tests/react/components/UserAvatar.test.js +52 -0
  117. package/src/tests/react/pages/DevicePickerStyles.test.js +47 -0
  118. package/src/tests/react/pages/Devices/deviceListHelpers.test.js +39 -0
  119. package/src/tests/react/pages/DevicesCurrentDevice.test.js +198 -0
  120. package/src/tests/react/pages/connectionsHelpers.test.js +34 -0
  121. package/src/tests/react/pages/mercadoLivreIntegrationPageContract.test.mjs +16 -0
  122. package/src/tests/react/services/Cielo.test.js +190 -190
  123. package/src/tests/react/store/categoriesOrdering.test.js +26 -0
  124. package/src/tests/react/utils/appDomain.test.js +62 -0
  125. package/src/tests/react/utils/entityDisplay.test.js +2 -2
  126. package/src/tests/react/utils/fileUrl.test.js +3 -3
  127. package/src/tests/react/utils/formatter.test.mjs +33 -0
  128. package/src/tests/react/utils/runtimeFooter.test.js +29 -4
  129. package/src/tests/react/utils/runtimeLanguage.test.js +79 -9
  130. package/src/tests/react/utils/runtimeMenu.test.js +198 -134
  131. package/src/tests/react/utils/runtimeZoom.test.js +38 -0
  132. package/src/tests/react/utils/shopConfig.test.js +10 -9
  133. package/src/tests/react/utils/shopFranchises.test.js +43 -0
  134. package/src/tests/react/utils/translate.discovery.test.mjs +170 -0
  135. package/src/tests/react/utils/translate.test.helpers.mjs +152 -0
  136. package/src/tests/react/utils/translate.test.mjs +141 -349
  137. package/src/tests/react/utils/userAvatar.test.js +63 -0
  138. package/src/utils/appDomain.js +15 -4
  139. package/src/utils/formatter.js +104 -16
  140. package/src/utils/integrationConfigs.js +80 -80
  141. package/src/utils/translate.js +222 -438
  142. package/src/utils/translateDiscovery.js +96 -0
  143. package/src/utils/translateNormalize.js +33 -0
  144. package/src/utils/translatePending.js +92 -0
  145. package/src/utils/translateStorage.js +142 -0
  146. package/.github/agents/developer.agent.md +0 -30
  147. package/.github/agents/devops.agent.md +0 -30
  148. package/.github/agents/qa.agent.md +0 -30
  149. package/.github/agents/security.agent.md +0 -30
  150. package/.github/workflows/npmjs.yml +0 -35
@@ -0,0 +1,205 @@
1
+ import React, {useState} from 'react';
2
+ import {Text, TouchableOpacity, View} from 'react-native';
3
+ import Icon from 'react-native-vector-icons/Feather';
4
+
5
+ import {withOpacity} from '@controleonline/../../src/styles/branding';
6
+
7
+ import styles from '../../Food99IntegrationPage/styles';
8
+
9
+ // Card compartilhado para o catálogo de marketplace.
10
+ export default function MarketplaceProductCard({
11
+ product,
12
+ selected,
13
+ accentColor,
14
+ onPress,
15
+ onMarkCardPressHandled,
16
+ }) {
17
+ const eligible = Boolean(product?.eligible);
18
+ const published = Boolean(product?.published);
19
+ const childGroups = Array.isArray(product?.children) ? product.children : [];
20
+ const childCount = childGroups.reduce(
21
+ (total, group) =>
22
+ total + (Array.isArray(group.items) ? group.items.length : 0),
23
+ 0,
24
+ );
25
+ const [childrenExpanded, setChildrenExpanded] = useState(false);
26
+
27
+ const handleToggleChildren = event => {
28
+ event?.preventDefault?.();
29
+ event?.stopPropagation?.();
30
+ onMarkCardPressHandled?.();
31
+ setChildrenExpanded(current => !current);
32
+ };
33
+
34
+ return (
35
+ <TouchableOpacity
36
+ style={[
37
+ styles.productCard,
38
+ selected && {
39
+ borderColor: accentColor,
40
+ backgroundColor: withOpacity(accentColor, 0.04),
41
+ },
42
+ ]}
43
+ activeOpacity={0.88}
44
+ onPress={() => onPress?.(product?.sourceProduct || product)}>
45
+ <View style={styles.productMain}>
46
+ <View
47
+ style={[
48
+ styles.productStatusIcon,
49
+ {backgroundColor: eligible ? '#DCFCE7' : '#FEE2E2'},
50
+ ]}>
51
+ <Icon
52
+ name={selected ? 'check-circle' : eligible ? 'circle' : 'x-circle'}
53
+ size={16}
54
+ color={selected ? accentColor : eligible ? '#16A34A' : '#DC2626'}
55
+ />
56
+ </View>
57
+
58
+ <View style={styles.productContent}>
59
+ <View style={styles.productTitleRow}>
60
+ <Text style={styles.productName} numberOfLines={1}>
61
+ {product?.name}
62
+ </Text>
63
+ <Text style={styles.productPrice}>
64
+ R$ {Number(product?.price || 0).toFixed(2)}
65
+ </Text>
66
+ </View>
67
+
68
+ <Text style={styles.productMeta} numberOfLines={1}>
69
+ {product?.categoryName || 'Sem categoria'} •{' '}
70
+ {product?.productType || 'produto'}
71
+ </Text>
72
+
73
+ {!!product?.description && (
74
+ <Text style={styles.productDescription} numberOfLines={1}>
75
+ {product.description}
76
+ </Text>
77
+ )}
78
+
79
+ {product?.code ? (
80
+ <Text style={styles.productCode}>
81
+ {product?.codeLabel || 'Codigo'}: {product.code}
82
+ </Text>
83
+ ) : (
84
+ <Text style={styles.productCode}>
85
+ {product?.noCodeLabel || 'Sem codigo'}
86
+ </Text>
87
+ )}
88
+
89
+ {published && (
90
+ <Text style={styles.productRemoteState}>
91
+ {product?.publishedLabel || 'Ja publicado no catalogo'}
92
+ </Text>
93
+ )}
94
+
95
+ {!eligible &&
96
+ Array.isArray(product?.blockers) &&
97
+ product.blockers.length > 0 && (
98
+ <Text style={styles.productBlocker}>
99
+ {product.blockers.join(' • ')}
100
+ </Text>
101
+ )}
102
+
103
+ {childCount > 0 && (
104
+ <TouchableOpacity
105
+ activeOpacity={0.8}
106
+ onPressIn={onMarkCardPressHandled}
107
+ onPress={handleToggleChildren}
108
+ style={[
109
+ styles.productDisclosureButton,
110
+ {
111
+ borderColor: accentColor,
112
+ backgroundColor: withOpacity(accentColor, 0.08),
113
+ },
114
+ ]}>
115
+ <Text
116
+ style={[
117
+ styles.productDisclosureButtonText,
118
+ {color: accentColor},
119
+ ]}>
120
+ {childrenExpanded
121
+ ? 'Ocultar filhos'
122
+ : `Mostrar filhos (${childCount})`}
123
+ </Text>
124
+ <Icon
125
+ name={childrenExpanded ? 'chevron-up' : 'chevron-down'}
126
+ size={14}
127
+ color={accentColor}
128
+ />
129
+ </TouchableOpacity>
130
+ )}
131
+
132
+ {childrenExpanded && childCount > 0 && (
133
+ <View style={styles.childrenSection}>
134
+ <Text style={styles.childrenSectionLabel}>
135
+ {product?.childrenSectionLabel || 'Filhos consultados'}
136
+ </Text>
137
+
138
+ {childGroups.map(group => {
139
+ const groupItems = Array.isArray(group.items)
140
+ ? group.items
141
+ : [];
142
+ const requiredLabel = group.required
143
+ ? 'Obrigatorio'
144
+ : 'Opcional';
145
+ const rangeParts = [];
146
+
147
+ if (Number(group.minimum ?? 0) > 0) {
148
+ rangeParts.push(`min ${group.minimum}`);
149
+ }
150
+
151
+ if (Number(group.maximum ?? 0) > 0) {
152
+ rangeParts.push(`max ${group.maximum}`);
153
+ }
154
+
155
+ const groupMeta = [requiredLabel, ...rangeParts].join(' • ');
156
+
157
+ return (
158
+ <View
159
+ key={`${group.id ?? group.name}`}
160
+ style={styles.childGroupCard}>
161
+ <View style={styles.childGroupHeader}>
162
+ <Text style={styles.childGroupTitle} numberOfLines={1}>
163
+ {group.name}
164
+ </Text>
165
+ <Text style={styles.childGroupMeta} numberOfLines={1}>
166
+ {groupMeta}
167
+ </Text>
168
+ </View>
169
+
170
+ <View style={styles.childItemsList}>
171
+ {groupItems.map(item => (
172
+ <View
173
+ key={`${item.id ?? item.code ?? item.name}`}
174
+ style={styles.childItemRow}>
175
+ <View style={styles.childItemContent}>
176
+ <Text
177
+ style={styles.childItemName}
178
+ numberOfLines={1}>
179
+ {item.name}
180
+ </Text>
181
+ <Text
182
+ style={styles.childItemMeta}
183
+ numberOfLines={1}>
184
+ {item.code
185
+ ? `${product?.childItemCodeLabel || 'Codigo'}: ${item.code}`
186
+ : product?.childItemNoCodeLabel || 'Sem codigo'}
187
+ {item.description ? ` • ${item.description}` : ''}
188
+ </Text>
189
+ </View>
190
+ <Text style={styles.childItemPrice}>
191
+ R$ {Number(item.price || 0).toFixed(2)}
192
+ </Text>
193
+ </View>
194
+ ))}
195
+ </View>
196
+ </View>
197
+ );
198
+ })}
199
+ </View>
200
+ )}
201
+ </View>
202
+ </View>
203
+ </TouchableOpacity>
204
+ );
205
+ }
@@ -0,0 +1,335 @@
1
+ import React, {useMemo, useState} from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Text,
5
+ TouchableOpacity,
6
+ View,
7
+ } from 'react-native';
8
+ import Icon from 'react-native-vector-icons/Feather';
9
+
10
+ import styles from '../../Food99IntegrationPage/styles';
11
+
12
+ const normalizeList = value => (Array.isArray(value) ? value : []);
13
+
14
+ const resolveButtonPalette = (palette = {}, accentColor) => {
15
+ const primaryBackground =
16
+ palette.buttonBackground || accentColor || palette.primary || '#2563EB';
17
+ const primaryBorder = palette.buttonBorder || primaryBackground;
18
+ const primaryText = palette.buttonText || palette.white || '#FFFFFF';
19
+ const primaryIcon = palette.buttonIcon || primaryText;
20
+ const disabledBackground =
21
+ palette.buttonDisabledBackground || palette.border || '#CBD5E1';
22
+ const disabledBorder =
23
+ palette.buttonBorderSecondary || palette.border || '#CBD5E1';
24
+ const disabledText =
25
+ palette.buttonDisabledText || palette.textSecondary || '#64748B';
26
+
27
+ return {
28
+ primaryBackground,
29
+ primaryBorder,
30
+ primaryText,
31
+ primaryIcon,
32
+ disabledBackground,
33
+ disabledBorder,
34
+ disabledText,
35
+ };
36
+ };
37
+
38
+ const formatValue = value => {
39
+ if (value === null || value === undefined || value === '') return '-';
40
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : '-';
41
+ if (typeof value === 'object') {
42
+ if (value.value !== null && value.value !== undefined) return formatValue(value.value);
43
+ if (value.originalValue !== null && value.originalValue !== undefined) {
44
+ return formatValue(value.originalValue);
45
+ }
46
+
47
+ return '-';
48
+ }
49
+ return String(value);
50
+ };
51
+
52
+ const formatMissingName = (kind, id) => {
53
+ const formattedId = formatValue(id);
54
+ return formattedId !== '-' ? `${kind} ${formattedId}` : `${kind} sem nome`;
55
+ };
56
+
57
+ const RemoteOptionRow = ({option}) => {
58
+ const displayId = option?.pdv_code ?? option?.id;
59
+ const name = option?.name || formatMissingName('Opcao', displayId);
60
+ const metaLabel = option?.pdv_code
61
+ ? `ID / Codigo PDV: ${formatValue(option.pdv_code)}`
62
+ : `ID: ${formatValue(option?.id)} | Codigo: ${formatValue(option?.external_code)}`;
63
+
64
+ return (
65
+ <View style={styles.remoteOptionRow}>
66
+ <View style={styles.remoteTreeContent}>
67
+ <Text style={styles.remoteTreeTitle}>{name}</Text>
68
+ <Text style={styles.remoteTreeMeta}>{metaLabel}</Text>
69
+ {option?.name_missing ? (
70
+ <Text style={styles.remoteTreeHint}>
71
+ Nome nao veio na resposta remota.
72
+ </Text>
73
+ ) : null}
74
+ </View>
75
+ <Text style={styles.remoteTreePrice}>{formatValue(option?.price)}</Text>
76
+ </View>
77
+ );
78
+ };
79
+
80
+ const RemoteGroupNode = ({group}) => {
81
+ const [expanded, setExpanded] = useState(false);
82
+ const options = normalizeList(group?.options);
83
+
84
+ return (
85
+ <View style={styles.remoteGroupCard}>
86
+ <TouchableOpacity
87
+ style={styles.remoteTreeHeader}
88
+ onPress={() => setExpanded(current => !current)}>
89
+ <Icon
90
+ name={expanded ? 'chevron-down' : 'chevron-right'}
91
+ size={16}
92
+ color="#64748B"
93
+ />
94
+ <View style={styles.remoteTreeContent}>
95
+ <Text style={styles.remoteTreeTitle}>
96
+ {group?.name || 'Grupo sem nome'}
97
+ </Text>
98
+ <Text style={styles.remoteTreeMeta}>
99
+ ID: {formatValue(group?.id)} | Min: {formatValue(group?.minimum)} |
100
+ Max: {formatValue(group?.maximum)} | Opcoes: {options.length}
101
+ </Text>
102
+ </View>
103
+ </TouchableOpacity>
104
+
105
+ {expanded && (
106
+ <View style={styles.remoteTreeChildren}>
107
+ {options.length > 0 ? (
108
+ options.map((option, index) => (
109
+ <RemoteOptionRow
110
+ key={`${group?.id || 'group'}-${option?.id || index}`}
111
+ option={option}
112
+ />
113
+ ))
114
+ ) : (
115
+ <Text style={styles.remoteEmptyText}>Grupo sem opcoes.</Text>
116
+ )}
117
+ </View>
118
+ )}
119
+ </View>
120
+ );
121
+ };
122
+
123
+ const RemoteProductNode = ({product}) => {
124
+ const [expanded, setExpanded] = useState(false);
125
+ const groups = normalizeList(product?.groups);
126
+
127
+ return (
128
+ <View style={styles.remoteProductCard}>
129
+ <TouchableOpacity
130
+ style={styles.remoteTreeHeader}
131
+ onPress={() => setExpanded(current => !current)}>
132
+ <Icon
133
+ name={expanded ? 'chevron-down' : 'chevron-right'}
134
+ size={16}
135
+ color="#64748B"
136
+ />
137
+ <View style={styles.remoteTreeContent}>
138
+ <Text style={styles.remoteTreeTitle}>
139
+ {product?.name || 'Produto sem nome'}
140
+ </Text>
141
+ <Text style={styles.remoteTreeMeta}>
142
+ ID: {formatValue(product?.id)} | Codigo:{' '}
143
+ {formatValue(product?.external_code)} | Grupos: {groups.length}
144
+ </Text>
145
+ </View>
146
+ <Text style={styles.remoteTreePrice}>{formatValue(product?.price)}</Text>
147
+ </TouchableOpacity>
148
+
149
+ {expanded && (
150
+ <View style={styles.remoteTreeChildren}>
151
+ {product?.description ? (
152
+ <Text style={styles.remoteDescription}>{product.description}</Text>
153
+ ) : null}
154
+
155
+ {groups.length > 0 ? (
156
+ groups.map((group, index) => (
157
+ <RemoteGroupNode
158
+ key={`${product?.id || 'product'}-${group?.id || index}`}
159
+ group={group}
160
+ />
161
+ ))
162
+ ) : (
163
+ <Text style={styles.remoteEmptyText}>
164
+ Produto sem grupos/adicionais remotos.
165
+ </Text>
166
+ )}
167
+ </View>
168
+ )}
169
+ </View>
170
+ );
171
+ };
172
+
173
+ const RemoteCategoryNode = ({category}) => {
174
+ const [expanded, setExpanded] = useState(false);
175
+ const products = normalizeList(category?.products);
176
+
177
+ return (
178
+ <View style={styles.remoteCategoryCard}>
179
+ <TouchableOpacity
180
+ style={styles.remoteTreeHeader}
181
+ onPress={() => setExpanded(current => !current)}>
182
+ <Icon
183
+ name={expanded ? 'chevron-down' : 'chevron-right'}
184
+ size={18}
185
+ color="#0F172A"
186
+ />
187
+ <View style={styles.remoteTreeContent}>
188
+ <Text style={styles.remoteCategoryTitle}>
189
+ {category?.name || 'Categoria sem nome'}
190
+ </Text>
191
+ <Text style={styles.remoteTreeMeta}>
192
+ ID: {formatValue(category?.id)} | Produtos: {products.length}
193
+ </Text>
194
+ </View>
195
+ </TouchableOpacity>
196
+
197
+ {expanded && (
198
+ <View style={styles.remoteTreeChildren}>
199
+ {products.length > 0 ? (
200
+ products.map((product, index) => (
201
+ <RemoteProductNode
202
+ key={`${category?.id || 'category'}-${product?.id || index}`}
203
+ product={product}
204
+ />
205
+ ))
206
+ ) : (
207
+ <Text style={styles.remoteEmptyText}>
208
+ Categoria sem produtos remotos.
209
+ </Text>
210
+ )}
211
+ </View>
212
+ )}
213
+ </View>
214
+ );
215
+ };
216
+
217
+ export default function RemoteMarketplaceMenuTab({
218
+ shadowStyle,
219
+ accentColor,
220
+ palette,
221
+ title,
222
+ subtitle,
223
+ buttonLabel,
224
+ loading,
225
+ snapshot,
226
+ errorMessage,
227
+ onLoad,
228
+ }) {
229
+ const categories = normalizeList(snapshot?.categories);
230
+ const buttonPalette = resolveButtonPalette(palette, accentColor);
231
+ const summaryItems = useMemo(() => {
232
+ const summary = snapshot?.summary || {};
233
+
234
+ return [
235
+ ['Catalogos/Menus', summary.catalogs ?? summary.menus ?? 0],
236
+ ['Categorias', summary.categories ?? 0],
237
+ ['Produtos', summary.products ?? 0],
238
+ ['Grupos', summary.groups ?? 0],
239
+ ['Opcoes', summary.options ?? 0],
240
+ ['Opcoes sem nome', summary.missing_option_names ?? 0],
241
+ ];
242
+ }, [snapshot]);
243
+
244
+ return (
245
+ <View style={[styles.panel, shadowStyle]}>
246
+ <View style={styles.panelHeader}>
247
+ <View style={styles.remoteHeaderCopy}>
248
+ <Text style={styles.panelTitle}>{title}</Text>
249
+ <Text style={styles.panelSubtitle}>{subtitle}</Text>
250
+ </View>
251
+
252
+ <TouchableOpacity
253
+ style={[
254
+ styles.previewButton,
255
+ {
256
+ borderWidth: 1,
257
+ borderColor: loading
258
+ ? buttonPalette.disabledBorder
259
+ : buttonPalette.primaryBorder,
260
+ backgroundColor: loading
261
+ ? buttonPalette.disabledBackground
262
+ : buttonPalette.primaryBackground,
263
+ },
264
+ ]}
265
+ onPress={onLoad}
266
+ disabled={loading}>
267
+ {loading ? (
268
+ <ActivityIndicator
269
+ size="small"
270
+ color={buttonPalette.disabledText}
271
+ />
272
+ ) : (
273
+ <>
274
+ <Icon
275
+ name="download-cloud"
276
+ size={15}
277
+ color={buttonPalette.primaryIcon}
278
+ />
279
+ <Text
280
+ style={[
281
+ styles.previewButtonText,
282
+ { color: buttonPalette.primaryText },
283
+ ]}>
284
+ {buttonLabel}
285
+ </Text>
286
+ </>
287
+ )}
288
+ </TouchableOpacity>
289
+ </View>
290
+
291
+ {errorMessage ? (
292
+ <View style={styles.errorBanner}>
293
+ <Icon name="alert-triangle" size={16} color="#B91C1C" />
294
+ <Text style={styles.errorBannerText}>{errorMessage}</Text>
295
+ </View>
296
+ ) : null}
297
+
298
+ {snapshot ? (
299
+ <>
300
+ <View style={styles.remoteSummaryGrid}>
301
+ {summaryItems.map(([label, value]) => (
302
+ <View key={label} style={styles.remoteSummaryCard}>
303
+ <Text style={styles.remoteSummaryValue}>{formatValue(value)}</Text>
304
+ <Text style={styles.remoteSummaryLabel}>{label}</Text>
305
+ </View>
306
+ ))}
307
+ </View>
308
+
309
+ <View style={styles.remoteTreeList}>
310
+ {categories.length > 0 ? (
311
+ categories.map((category, index) => (
312
+ <RemoteCategoryNode
313
+ key={`${category?.id || 'category'}-${index}`}
314
+ category={category}
315
+ />
316
+ ))
317
+ ) : (
318
+ <View style={styles.emptyProducts}>
319
+ <Text style={styles.emptyProductsText}>
320
+ Nenhuma categoria remota encontrada.
321
+ </Text>
322
+ </View>
323
+ )}
324
+ </View>
325
+ </>
326
+ ) : (
327
+ <View style={styles.emptyProducts}>
328
+ <Text style={styles.emptyProductsText}>
329
+ Clique no botao para ler o cardapio atual da plataforma.
330
+ </Text>
331
+ </View>
332
+ )}
333
+ </View>
334
+ );
335
+ }
@@ -0,0 +1,60 @@
1
+ import {useLayoutEffect, useMemo} from 'react';
2
+
3
+ import Food99IntegrationPage from '@controleonline/ui-common/src/react/pages/Food99IntegrationPage';
4
+ import IFoodIntegrationPage from '@controleonline/ui-common/src/react/pages/IFoodIntegrationPage';
5
+ import MercadoLivreIntegrationPage from '@controleonline/ui-common/src/react/pages/MercadoLivreIntegrationPage';
6
+
7
+ const ROUTE_PROVIDER_MAP = {
8
+ Food99IntegrationPage: '99food',
9
+ IFoodIntegrationPage: 'ifood',
10
+ MercadoLivreIntegrationPage: 'mercadolivre',
11
+ };
12
+
13
+ const PROVIDER_LABEL_MAP = {
14
+ '99food': '99Food',
15
+ ifood: 'iFood',
16
+ mercadolivre: 'Mercado Livre',
17
+ };
18
+
19
+ const normalizeProviderKey = value =>
20
+ String(value || '')
21
+ .trim()
22
+ .toLowerCase();
23
+
24
+ const resolveProviderKey = route =>
25
+ normalizeProviderKey(
26
+ route?.params?.providerKey ||
27
+ route?.params?.integrationKey ||
28
+ ROUTE_PROVIDER_MAP[route?.name] ||
29
+ '',
30
+ );
31
+
32
+ export default function MarketplaceIntegrationPage(props) {
33
+ const {navigation, route} = props;
34
+
35
+ const providerKey = useMemo(
36
+ () => resolveProviderKey(route),
37
+ [route?.name, route?.params?.providerKey, route?.params?.integrationKey],
38
+ );
39
+ const providerLabel = PROVIDER_LABEL_MAP[providerKey] || 'Marketplace';
40
+
41
+ useLayoutEffect(() => {
42
+ if (!navigation?.setOptions) {
43
+ return;
44
+ }
45
+
46
+ navigation.setOptions({
47
+ title: providerLabel,
48
+ });
49
+ }, [navigation, providerLabel]);
50
+
51
+ if (providerKey === 'ifood') {
52
+ return <IFoodIntegrationPage {...props} />;
53
+ }
54
+
55
+ if (providerKey === 'mercadolivre') {
56
+ return <MercadoLivreIntegrationPage {...props} />;
57
+ }
58
+
59
+ return <Food99IntegrationPage {...props} />;
60
+ }