@controleonline/ui-common 1.2.71 → 1.2.76

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 +164 -104
  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 +2865 -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,1530 @@
1
+ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Linking,
5
+ RefreshControl,
6
+ ScrollView,
7
+ Text,
8
+ View,
9
+ } from 'react-native';
10
+ import {SafeAreaView} from 'react-native-safe-area-context';
11
+ import {useFocusEffect} from '@react-navigation/native';
12
+ import Icon from 'react-native-vector-icons/Feather';
13
+
14
+ import {api} from '@controleonline/ui-common/src/api';
15
+ import useToastMessage from '@controleonline/ui-crm/src/react/hooks/useToastMessage';
16
+ import {useStore} from '@store';
17
+ import {colors} from '@controleonline/../../src/styles/colors';
18
+ import {resolveThemePalette} from '@controleonline/../../src/styles/branding';
19
+ import {getOrderChannelLogo} from '@assets/ppc/channels';
20
+
21
+ import IntegrationHero from '../../components/integrations/IntegrationHero';
22
+ import IntegrationTabs from '../../components/integrations/IntegrationTabs';
23
+ import {integrationCardShadowStyle} from '@controleonline/ui-common/src/react/utils/integrationPage';
24
+ import Food99CatalogTab from './components/Food99CatalogTab';
25
+ import Food99OverviewTab from './components/Food99OverviewTab';
26
+ import Food99PreviewModal from './components/Food99PreviewModal';
27
+ import Food99SettingsTab from './components/Food99SettingsTab';
28
+ import Food99QuickWalletModal from './components/Food99QuickWalletModal';
29
+ import Food99StoreTab from './components/Food99StoreTab';
30
+ import RemoteMarketplaceMenuTab from '../MarketplaceIntegrationPage/components/RemoteMarketplaceMenuTab';
31
+ import styles from './styles';
32
+ import {
33
+ createEmptyStoreSettingsDraft,
34
+ formatDeliveryMethodLabel,
35
+ formatFood99ApiError,
36
+ getPublishStateLabel,
37
+ isErrnoSuccess,
38
+ isValidTimeInput,
39
+ MINIMUM_REQUIRED_ITEMS,
40
+ normalizeDeliveryMethodCode,
41
+ normalizeTaskId,
42
+ publishStateToneMap,
43
+ resolveFood99HistoryFromTime,
44
+ sanitizeConfirmMethodInput,
45
+ sanitizeTimeInput,
46
+ statusLabelMap,
47
+ subStatusLabelMap,
48
+ wait,
49
+ } from './utils';
50
+
51
+ const openAuthorizationUrl = async authUrl => {
52
+ if (
53
+ typeof window !== 'undefined' &&
54
+ typeof window.location?.assign === 'function'
55
+ ) {
56
+ window.location.assign(authUrl);
57
+ return;
58
+ }
59
+
60
+ await Linking.openURL(authUrl);
61
+ };
62
+
63
+ // Página principal da integração 99Food com separação por contexto.
64
+ export default function Food99IntegrationPage() {
65
+ const peopleStore = useStore('people');
66
+ const walletStore = useStore('wallet');
67
+ const themeStore = useStore('theme');
68
+ const {currentCompany} = peopleStore.getters;
69
+ const {items: wallets = [], isLoading: isLoadingWallets} =
70
+ walletStore.getters;
71
+ const {colors: themeColors} = themeStore.getters;
72
+ const {showError, showInfo, showSuccess} = useToastMessage();
73
+
74
+ const brandColors = useMemo(
75
+ () =>
76
+ resolveThemePalette(
77
+ {
78
+ ...themeColors,
79
+ ...(currentCompany?.theme?.colors || {}),
80
+ },
81
+ colors,
82
+ ),
83
+ [themeColors, currentCompany?.id],
84
+ );
85
+
86
+ const heroPalette = useMemo(
87
+ () => ({
88
+ ...brandColors,
89
+ actionBackground:
90
+ brandColors.buttonBackground ||
91
+ brandColors.actionBackground ||
92
+ brandColors.primary,
93
+ actionText:
94
+ brandColors.buttonText ||
95
+ brandColors.actionText ||
96
+ brandColors.white,
97
+ cardIconBackground:
98
+ brandColors.cardIconBackground ||
99
+ brandColors.iconBackground ||
100
+ brandColors.white,
101
+ cardIconColor:
102
+ brandColors.cardIconColor ||
103
+ brandColors.iconColor ||
104
+ brandColors.buttonText ||
105
+ brandColors.textPrimary,
106
+ }),
107
+ [brandColors],
108
+ );
109
+
110
+ const providerId = currentCompany?.id;
111
+ const logo = getOrderChannelLogo({app: '99Food'});
112
+
113
+ const [activeTab, setActiveTab] = useState('overview');
114
+ const [loading, setLoading] = useState(true);
115
+ const [refreshing, setRefreshing] = useState(false);
116
+ const [search, setSearch] = useState('');
117
+ const [filterKey, setFilterKey] = useState('all');
118
+ const [integrationItem, setIntegrationItem] = useState(null);
119
+ const [productsResponse, setProductsResponse] = useState(null);
120
+ const [selectedProductIds, setSelectedProductIds] = useState([]);
121
+ const [previewData, setPreviewData] = useState(null);
122
+ const [previewVisible, setPreviewVisible] = useState(false);
123
+ const [previewLoading, setPreviewLoading] = useState(false);
124
+ const [uploading, setUploading] = useState(false);
125
+ const [actionLoading, setActionLoading] = useState('');
126
+ const [lastUploadResult, setLastUploadResult] = useState(null);
127
+ const [storeSettingsResponse, setStoreSettingsResponse] = useState(null);
128
+ const [storeSettingsDraft, setStoreSettingsDraft] = useState(
129
+ createEmptyStoreSettingsDraft,
130
+ );
131
+ const [manualShopId, setManualShopId] = useState('');
132
+ const [quickWalletModalVisible, setQuickWalletModalVisible] = useState(false);
133
+ const [quickWalletName, setQuickWalletName] = useState('');
134
+ const [remoteMenuSnapshot, setRemoteMenuSnapshot] = useState(null);
135
+ const [remoteMenuError, setRemoteMenuError] = useState('');
136
+ const hasHydratedSelection = useRef(false);
137
+ const ignoreNextProductCardPressRef = useRef(false);
138
+ const hasLoadedOnceRef = useRef(false);
139
+
140
+ useEffect(() => {
141
+ hasLoadedOnceRef.current = false;
142
+ }, [providerId]);
143
+
144
+ const products = useMemo(
145
+ () =>
146
+ Array.isArray(productsResponse?.products)
147
+ ? productsResponse.products
148
+ : [],
149
+ [productsResponse],
150
+ );
151
+
152
+ const selectedProductSet = useMemo(
153
+ () => new Set(selectedProductIds.map(id => String(id))),
154
+ [selectedProductIds],
155
+ );
156
+
157
+ const productMap = useMemo(() => {
158
+ const map = new Map();
159
+ products.forEach(product => {
160
+ map.set(String(product.id), product);
161
+ });
162
+ return map;
163
+ }, [products]);
164
+
165
+ const selectedProducts = useMemo(
166
+ () =>
167
+ selectedProductIds.map(id => productMap.get(String(id))).filter(Boolean),
168
+ [productMap, selectedProductIds],
169
+ );
170
+
171
+ const selectedEligibleProducts = useMemo(
172
+ () => selectedProducts.filter(product => product.eligible),
173
+ [selectedProducts],
174
+ );
175
+
176
+ const connected = Boolean(integrationItem?.connected);
177
+ const remoteConnected = Boolean(integrationItem?.remote_connected);
178
+ const needsReconnect = connected && !remoteConnected;
179
+ const storeBizStatus = Number(integrationItem?.biz_status ?? 0);
180
+ const storeSubStatus = Number(integrationItem?.sub_biz_status ?? 0);
181
+ const isOnline = Boolean(integrationItem?.online);
182
+ const menuCount = Number(integrationItem?.menu_count || 0);
183
+ const menuItemCount = Number(integrationItem?.menu_item_count || 0);
184
+ const deliveryAreaCount = Number(integrationItem?.delivery_area_count || 0);
185
+ const publishedProductCount = Number(
186
+ integrationItem?.published_product_count || 0,
187
+ );
188
+ const remoteOnlyItemCount = Number(
189
+ integrationItem?.remote_only_item_count || 0,
190
+ );
191
+ const lastSyncAt = integrationItem?.last_sync_at || null;
192
+ const lastErrorMessage = integrationItem?.last_error_message || null;
193
+ const lastMenuTaskId =
194
+ normalizeTaskId(integrationItem?.last_menu_task_id) ||
195
+ normalizeTaskId(lastUploadResult?.integration?.last_menu_task_id) ||
196
+ normalizeTaskId(lastUploadResult?.result?.data?.taskID) ||
197
+ normalizeTaskId(lastUploadResult?.result?.data?.taskId) ||
198
+ null;
199
+ const lastMenuTaskStatus = integrationItem?.last_menu_task_status || null;
200
+ const lastMenuTaskMessage = integrationItem?.last_menu_task_message || null;
201
+ const lastMenuTaskCheckedAt =
202
+ integrationItem?.last_menu_task_checked_at || null;
203
+ const lastMenuPublishState = integrationItem?.last_menu_publish_state || null;
204
+ const lastReconcileAt = integrationItem?.last_reconcile_at || null;
205
+ const lastWebhookReceivedAt =
206
+ integrationItem?.last_webhook_received_at || null;
207
+ const publicationTone =
208
+ publishStateToneMap[lastMenuPublishState] || brandColors.textSecondary;
209
+ const storeSettings = storeSettingsResponse?.settings || {};
210
+ const settingsSource = String(
211
+ storeSettingsResponse?.settings_source || 'unavailable',
212
+ );
213
+ const settingsSourceLabelMap = {
214
+ remote: 'Remoto',
215
+ mixed: 'Remoto + fallback local',
216
+ fallback: 'Fallback local',
217
+ unavailable: 'Indisponivel',
218
+ };
219
+ const settingsSourceLabel =
220
+ settingsSourceLabelMap[settingsSource] || settingsSource;
221
+
222
+ const resolveSettingDisplay = value =>
223
+ value === null || value === undefined || String(value).trim() === ''
224
+ ? '-'
225
+ : String(value);
226
+
227
+ const currentDeliveryRadius = resolveSettingDisplay(
228
+ storeSettings?.delivery_radius,
229
+ );
230
+ const currentOpenTime = resolveSettingDisplay(storeSettings?.open_time);
231
+ const currentCloseTime = resolveSettingDisplay(storeSettings?.close_time);
232
+ const currentDeliveryMethod = formatDeliveryMethodLabel(
233
+ storeSettings?.delivery_method,
234
+ );
235
+ const currentConfirmMethod = resolveSettingDisplay(
236
+ storeSettings?.confirm_method,
237
+ );
238
+ const currentSettlementWalletId = String(
239
+ storeSettings?.settlement_wallet_id || '',
240
+ ).trim();
241
+ const currentSettlementWalletLabel = useMemo(() => {
242
+ if (!currentSettlementWalletId) {
243
+ return '-';
244
+ }
245
+
246
+ const selectedWallet = wallets.find(
247
+ wallet => String(wallet.id) === currentSettlementWalletId,
248
+ );
249
+ if (selectedWallet) {
250
+ const walletLabel =
251
+ String(selectedWallet.wallet || '').trim() ||
252
+ `Carteira #${selectedWallet.id}`;
253
+
254
+ return `${walletLabel} (#${selectedWallet.id})`;
255
+ }
256
+
257
+ return `#${currentSettlementWalletId}`;
258
+ }, [currentSettlementWalletId, wallets]);
259
+
260
+ const filteredProducts = useMemo(() => {
261
+ const normalizedSearch = String(search || '')
262
+ .trim()
263
+ .toLowerCase();
264
+
265
+ return products.filter(product => {
266
+ if (filterKey === 'eligible' && !product.eligible) return false;
267
+ if (filterKey === 'blocked' && product.eligible) return false;
268
+ if (
269
+ filterKey === 'selected' &&
270
+ !selectedProductSet.has(String(product.id))
271
+ )
272
+ return false;
273
+
274
+ if (!normalizedSearch) return true;
275
+
276
+ const haystack = [
277
+ product.name,
278
+ product.description,
279
+ product.category?.name,
280
+ product.food99_code,
281
+ product.suggested_app_item_id,
282
+ ]
283
+ .filter(Boolean)
284
+ .join(' ')
285
+ .toLowerCase();
286
+
287
+ return haystack.includes(normalizedSearch);
288
+ });
289
+ }, [filterKey, products, search, selectedProductSet]);
290
+
291
+ const summaryCards = useMemo(
292
+ () => [
293
+ {
294
+ key: 'connection',
295
+ label: 'Loja 99',
296
+ value: needsReconnect
297
+ ? 'Reconexão necessária'
298
+ : connected
299
+ ? 'Conectada'
300
+ : 'Pendente',
301
+ icon: 'link',
302
+ color: needsReconnect ? '#e67e22' : connected ? '#10B981' : '#e67e22',
303
+ },
304
+ {
305
+ key: 'status',
306
+ label: 'Status',
307
+ value: remoteConnected
308
+ ? integrationItem?.biz_status_label ||
309
+ statusLabelMap[storeBizStatus] ||
310
+ 'Indefinido'
311
+ : needsReconnect
312
+ ? 'Reconexão necessária'
313
+ : connected
314
+ ? 'Aguardando sync'
315
+ : 'Pendente',
316
+ icon: isOnline ? 'wifi' : 'wifi-off',
317
+ color: isOnline ? brandColors.primary : '#64748B',
318
+ },
319
+ {
320
+ key: 'eligible',
321
+ label: 'Produtos locais aptos',
322
+ value: String(productsResponse?.eligible_product_count || 0),
323
+ icon: 'package',
324
+ color: '#3B82F6',
325
+ },
326
+ {
327
+ key: 'selected',
328
+ label: 'Selecionados',
329
+ value: String(selectedEligibleProducts.length),
330
+ icon: 'check-circle',
331
+ color:
332
+ selectedEligibleProducts.length >= MINIMUM_REQUIRED_ITEMS
333
+ ? '#10B981'
334
+ : '#e67e22',
335
+ },
336
+ ],
337
+ [
338
+ brandColors.primary,
339
+ connected,
340
+ needsReconnect,
341
+ remoteConnected,
342
+ integrationItem?.biz_status_label,
343
+ isOnline,
344
+ productsResponse?.eligible_product_count,
345
+ storeBizStatus,
346
+ selectedEligibleProducts.length,
347
+ ],
348
+ );
349
+
350
+ const storeStatusRows = [
351
+ {
352
+ label: 'Business status',
353
+ value: remoteConnected
354
+ ? integrationItem?.biz_status_label ||
355
+ statusLabelMap[storeBizStatus] ||
356
+ 'Indefinido'
357
+ : needsReconnect
358
+ ? 'Reconexão necessária'
359
+ : connected
360
+ ? 'Aguardando sync'
361
+ : 'Não conectado',
362
+ },
363
+ {
364
+ label: 'Substatus',
365
+ value: remoteConnected
366
+ ? integrationItem?.sub_biz_status_label ||
367
+ subStatusLabelMap[storeSubStatus] ||
368
+ 'Indefinido'
369
+ : '-',
370
+ },
371
+ {label: 'Menus remotos', value: String(menuCount)},
372
+ {label: 'Itens remotos', value: String(menuItemCount)},
373
+ {label: 'Produtos publicados', value: String(publishedProductCount)},
374
+ {label: 'Itens remotos sem local', value: String(remoteOnlyItemCount)},
375
+ {label: 'Areas de entrega', value: String(deliveryAreaCount)},
376
+ {label: 'Ultimo task ID', value: lastMenuTaskId || '-'},
377
+ {label: 'Status da task', value: lastMenuTaskStatus || '-'},
378
+ {
379
+ label: 'Ultima verificacao da task',
380
+ value: lastMenuTaskCheckedAt || 'Ainda nao verificada',
381
+ wide: true,
382
+ small: true,
383
+ },
384
+ {
385
+ label: 'Ultima sincronizacao',
386
+ value: lastSyncAt || 'Ainda nao sincronizado',
387
+ wide: true,
388
+ small: true,
389
+ },
390
+ {
391
+ label: 'Ultimo webhook recebido',
392
+ value: lastWebhookReceivedAt || 'Ainda nao recebido',
393
+ wide: true,
394
+ small: true,
395
+ },
396
+ {
397
+ label: 'Ultima reconciliacao',
398
+ value: lastReconcileAt || 'Ainda nao reconciliado',
399
+ wide: true,
400
+ small: true,
401
+ },
402
+ ];
403
+
404
+ const settingsSummaryRows = [
405
+ {label: 'Raio atual', value: currentDeliveryRadius},
406
+ {label: 'Metodo atual', value: currentDeliveryMethod},
407
+ {label: 'Abertura atual', value: currentOpenTime},
408
+ {label: 'Fechamento atual', value: currentCloseTime},
409
+ {
410
+ label: 'Carteira de repasse',
411
+ value: currentSettlementWalletLabel,
412
+ wide: true,
413
+ },
414
+ {
415
+ label: 'Metodo de confirmacao atual',
416
+ value: currentConfirmMethod,
417
+ wide: true,
418
+ },
419
+ {
420
+ label: 'Fonte dos valores atuais',
421
+ value: settingsSourceLabel,
422
+ wide: true,
423
+ small: true,
424
+ },
425
+ ];
426
+
427
+ const tabCounts = {
428
+ all: products.length,
429
+ eligible: products.filter(product => product.eligible).length,
430
+ selected: selectedEligibleProducts.length,
431
+ blocked: products.filter(product => !product.eligible).length,
432
+ };
433
+
434
+ const hydrateSelection = useCallback(productList => {
435
+ if (hasHydratedSelection.current) return;
436
+
437
+ const preselected = productList
438
+ .filter(
439
+ product => product?.eligible && Boolean(product.published_remotely),
440
+ )
441
+ .map(product => String(product.id));
442
+
443
+ if (preselected.length > 0) {
444
+ setSelectedProductIds(preselected);
445
+ }
446
+
447
+ hasHydratedSelection.current = true;
448
+ }, []);
449
+
450
+ const syncSelectionWithPublishedProducts = useCallback(productList => {
451
+ const publishedIds = (Array.isArray(productList) ? productList : [])
452
+ .filter(product => product?.eligible && product?.published_remotely)
453
+ .map(product => String(product.id));
454
+
455
+ setSelectedProductIds(publishedIds);
456
+ hasHydratedSelection.current = true;
457
+ }, []);
458
+
459
+ const applyDetailResponse = useCallback(
460
+ (detailResponse, {syncPublishedSelection = false} = {}) => {
461
+ setIntegrationItem(detailResponse?.integration || null);
462
+ setProductsResponse(detailResponse?.products || null);
463
+
464
+ if (syncPublishedSelection) {
465
+ syncSelectionWithPublishedProducts(
466
+ detailResponse?.products?.products || [],
467
+ );
468
+ } else {
469
+ hydrateSelection(detailResponse?.products?.products || []);
470
+ }
471
+ },
472
+ [hydrateSelection, syncSelectionWithPublishedProducts],
473
+ );
474
+
475
+ const applyStoreSettingsResponse = useCallback(response => {
476
+ const settings = response?.settings || {};
477
+
478
+ setStoreSettingsResponse(response || null);
479
+ setStoreSettingsDraft({
480
+ deliveryRadiusKm:
481
+ settings?.delivery_radius === null ||
482
+ settings?.delivery_radius === undefined
483
+ ? ''
484
+ : String(settings.delivery_radius),
485
+ openTime: settings?.open_time
486
+ ? sanitizeTimeInput(String(settings.open_time))
487
+ : '',
488
+ closeTime: settings?.close_time
489
+ ? sanitizeTimeInput(String(settings.close_time))
490
+ : '',
491
+ deliveryMethod: normalizeDeliveryMethodCode(settings?.delivery_method),
492
+ confirmMethod: settings?.confirm_method
493
+ ? sanitizeConfirmMethodInput(String(settings.confirm_method))
494
+ : '',
495
+ deliveryAreaId: settings?.delivery_area_id
496
+ ? String(settings.delivery_area_id)
497
+ : '',
498
+ settlementWalletId: settings?.settlement_wallet_id
499
+ ? String(settings.settlement_wallet_id)
500
+ : '',
501
+ });
502
+ }, []);
503
+
504
+ const loadData = useCallback(
505
+ async ({silent = false, refreshRemote = false} = {}) => {
506
+ if (!providerId) {
507
+ setLoading(false);
508
+ return;
509
+ }
510
+
511
+ if (!silent) {
512
+ setLoading(true);
513
+ }
514
+
515
+ try {
516
+ const detailParams = {provider_id: providerId};
517
+ if (refreshRemote) detailParams.refresh_remote = 1;
518
+
519
+ const walletLoadPromise = walletStore.actions?.getItems
520
+ ? walletStore.actions
521
+ .getItems({
522
+ people: providerId,
523
+ })
524
+ .catch(() => null)
525
+ : Promise.resolve(null);
526
+
527
+ const [detailResponse, settingsResponse] = await Promise.all([
528
+ api.fetch('/marketplace/integrations/99food/detail', {
529
+ params: detailParams,
530
+ }),
531
+ api.fetch('/marketplace/integrations/99food/store/settings', {
532
+ params: {provider_id: providerId},
533
+ }),
534
+ walletLoadPromise,
535
+ ]);
536
+
537
+ applyDetailResponse(detailResponse);
538
+ applyStoreSettingsResponse(settingsResponse);
539
+ } catch (error) {
540
+ showError(formatFood99ApiError(error));
541
+ } finally {
542
+ setLoading(false);
543
+ }
544
+ },
545
+ [
546
+ applyDetailResponse,
547
+ applyStoreSettingsResponse,
548
+ providerId,
549
+ showError,
550
+ walletStore.actions,
551
+ ],
552
+ );
553
+
554
+ const fetchMenuTaskStatus = useCallback(
555
+ async (taskId, {poll = false} = {}) => {
556
+ if (!providerId || !taskId) return null;
557
+
558
+ let lastResponse = null;
559
+ const maxAttempts = poll ? 6 : 1;
560
+
561
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
562
+ const response = await api.fetch(
563
+ `/marketplace/integrations/99food/menu/task/${taskId}`,
564
+ {
565
+ params: {provider_id: providerId},
566
+ },
567
+ );
568
+
569
+ lastResponse = response;
570
+
571
+ if (response?.integration || response?.products) {
572
+ applyDetailResponse(
573
+ {
574
+ integration: response?.integration,
575
+ products: response?.products,
576
+ },
577
+ {syncPublishedSelection: response?.publish_state === 'published'},
578
+ );
579
+ } else {
580
+ await loadData({silent: true});
581
+ }
582
+
583
+ if (
584
+ ['published', 'failed', 'sync_error'].includes(
585
+ response?.publish_state,
586
+ )
587
+ ) {
588
+ break;
589
+ }
590
+
591
+ if (!poll || attempt === maxAttempts - 1) {
592
+ break;
593
+ }
594
+
595
+ await wait(3000);
596
+ }
597
+
598
+ return lastResponse;
599
+ },
600
+ [applyDetailResponse, loadData, providerId],
601
+ );
602
+
603
+ useFocusEffect(
604
+ useCallback(() => {
605
+ if (!providerId || hasLoadedOnceRef.current) {
606
+ return undefined;
607
+ }
608
+
609
+ hasLoadedOnceRef.current = true;
610
+ loadData();
611
+
612
+ return undefined;
613
+ }, [loadData, providerId]),
614
+ );
615
+
616
+ const onRefresh = useCallback(async () => {
617
+ setRefreshing(true);
618
+ try {
619
+ if (lastMenuTaskId) {
620
+ await fetchMenuTaskStatus(lastMenuTaskId, {poll: false});
621
+ } else {
622
+ await loadData({silent: true, refreshRemote: true});
623
+ }
624
+ } catch (error) {
625
+ showError(formatFood99ApiError(error));
626
+ } finally {
627
+ setRefreshing(false);
628
+ }
629
+ }, [fetchMenuTaskStatus, lastMenuTaskId, loadData, showError]);
630
+
631
+ const withAction = useCallback(async (key, action) => {
632
+ setActionLoading(key);
633
+ try {
634
+ await action();
635
+ } finally {
636
+ setActionLoading('');
637
+ }
638
+ }, []);
639
+
640
+ const handleToggleProduct = useCallback(
641
+ product => {
642
+ if (!product?.eligible) {
643
+ showError(
644
+ product?.blockers?.[0] || 'Produto indisponivel para a integracao.',
645
+ );
646
+ return;
647
+ }
648
+
649
+ setSelectedProductIds(currentIds => {
650
+ const productId = String(product.id);
651
+ if (currentIds.includes(productId)) {
652
+ return currentIds.filter(id => id !== productId);
653
+ }
654
+ return [...currentIds, productId];
655
+ });
656
+ },
657
+ [showError],
658
+ );
659
+
660
+ const markNextProductCardPressAsHandled = useCallback(() => {
661
+ ignoreNextProductCardPressRef.current = true;
662
+
663
+ const release = () => {
664
+ ignoreNextProductCardPressRef.current = false;
665
+ };
666
+
667
+ if (typeof requestAnimationFrame === 'function') {
668
+ requestAnimationFrame(release);
669
+ return;
670
+ }
671
+
672
+ setTimeout(release, 0);
673
+ }, []);
674
+
675
+ const handleProductCardPress = useCallback(
676
+ product => {
677
+ if (ignoreNextProductCardPressRef.current) {
678
+ ignoreNextProductCardPressRef.current = false;
679
+ return;
680
+ }
681
+
682
+ handleToggleProduct(product);
683
+ },
684
+ [handleToggleProduct],
685
+ );
686
+
687
+ const handleConnectStore = useCallback(async () => {
688
+ if (!providerId) return;
689
+
690
+ await withAction('connect', async () => {
691
+ try {
692
+ const response = await api.fetch(
693
+ '/marketplace/integrations/99food/store/authorization-page',
694
+ {
695
+ method: 'POST',
696
+ body: {provider_id: providerId},
697
+ },
698
+ );
699
+
700
+ const authUrl =
701
+ response?.data?.url ||
702
+ response?.data?.auth_url ||
703
+ response?.data?.authorization_url ||
704
+ response?.data?.authorization_page ||
705
+ response?.data?.web_page_url ||
706
+ null;
707
+
708
+ if (authUrl) {
709
+ await openAuthorizationUrl(authUrl);
710
+ showInfo('Abrindo fluxo de integracao da loja.');
711
+ return;
712
+ }
713
+
714
+ if (isErrnoSuccess(response?.errno)) {
715
+ showSuccess('Solicitacao de integracao enviada.');
716
+ return;
717
+ }
718
+
719
+ showError(
720
+ response?.errmsg || 'Nao foi possivel iniciar a integracao da loja.',
721
+ );
722
+ } catch (error) {
723
+ showError(formatFood99ApiError(error));
724
+ }
725
+ });
726
+ }, [providerId, showError, showInfo, showSuccess, withAction]);
727
+
728
+ const handleStoreStatusChange = useCallback(
729
+ async nextStatus => {
730
+ if (!providerId) return;
731
+
732
+ await withAction(nextStatus === 1 ? 'online' : 'offline', async () => {
733
+ try {
734
+ const response = await api.fetch(
735
+ '/marketplace/integrations/99food/store/status',
736
+ {
737
+ method: 'POST',
738
+ body: {
739
+ provider_id: providerId,
740
+ biz_status: nextStatus,
741
+ auto_switch: 2,
742
+ },
743
+ },
744
+ );
745
+
746
+ if (!isErrnoSuccess(response?.errno)) {
747
+ showError(
748
+ response?.errmsg ||
749
+ 'Nao foi possivel atualizar o status da loja.',
750
+ );
751
+ return;
752
+ }
753
+
754
+ showSuccess(
755
+ nextStatus === 1
756
+ ? 'Loja enviada para online.'
757
+ : 'Loja enviada para offline.',
758
+ );
759
+ await loadData({silent: true});
760
+ } catch (error) {
761
+ showError(formatFood99ApiError(error));
762
+ }
763
+ });
764
+ },
765
+ [loadData, providerId, showError, showSuccess, withAction],
766
+ );
767
+
768
+ const getFirstOperationError = useCallback(response => {
769
+ if (!response || typeof response !== 'object') return null;
770
+
771
+ if (response?.result && !isErrnoSuccess(response.result?.errno)) {
772
+ return response.result?.errmsg || 'A operacao falhou na 99Food.';
773
+ }
774
+
775
+ const operations = response?.operations || {};
776
+ for (const operationKey of Object.keys(operations)) {
777
+ const operationResult = operations?.[operationKey];
778
+ if (!isErrnoSuccess(operationResult?.errno)) {
779
+ return operationResult?.errmsg || `Falha em ${operationKey}.`;
780
+ }
781
+ }
782
+
783
+ return null;
784
+ }, []);
785
+
786
+ const handleSaveStoreSettings = useCallback(async () => {
787
+ if (!providerId) return;
788
+
789
+ await withAction('save-settings', async () => {
790
+ try {
791
+ const deliveryRadius = String(
792
+ storeSettingsDraft?.deliveryRadiusKm || '',
793
+ ).trim();
794
+ const openTime = String(storeSettingsDraft?.openTime || '').trim();
795
+ const closeTime = String(storeSettingsDraft?.closeTime || '').trim();
796
+ const deliveryMethod = String(
797
+ storeSettingsDraft?.deliveryMethod || '',
798
+ ).trim();
799
+ const confirmMethod = String(
800
+ storeSettingsDraft?.confirmMethod || '',
801
+ ).trim();
802
+ const deliveryAreaId = String(
803
+ storeSettingsDraft?.deliveryAreaId || '',
804
+ ).trim();
805
+ const settlementWalletId = String(
806
+ storeSettingsDraft?.settlementWalletId || '',
807
+ ).trim();
808
+
809
+ if (deliveryRadius && !/^\d+(\.\d{1,2})?$/.test(deliveryRadius)) {
810
+ showError(
811
+ 'Raio de atendimento invalido. Use numero positivo (ex.: 5 ou 5.5).',
812
+ );
813
+ return;
814
+ }
815
+
816
+ if (
817
+ (openTime || closeTime) &&
818
+ (!isValidTimeInput(openTime) || !isValidTimeInput(closeTime))
819
+ ) {
820
+ showError('Horario invalido. Use o formato HH:mm (ex.: 08:00).');
821
+ return;
822
+ }
823
+
824
+ if (deliveryMethod && !['1', '2'].includes(deliveryMethod)) {
825
+ showError(
826
+ 'Metodo de entrega invalido. Selecione Loja (2) ou Entrega 99 (1).',
827
+ );
828
+ return;
829
+ }
830
+
831
+ if (confirmMethod && !/^\d{1,3}$/.test(confirmMethod)) {
832
+ showError('Metodo de confirmacao invalido. Informe apenas numeros.');
833
+ return;
834
+ }
835
+
836
+ if (!settlementWalletId) {
837
+ showError('Selecione a carteira de repasse da loja.');
838
+ return;
839
+ }
840
+
841
+ const walletMatch = wallets.some(
842
+ wallet => String(wallet.id) === settlementWalletId,
843
+ );
844
+ if (!walletMatch) {
845
+ showError('Selecione uma carteira valida da empresa ativa.');
846
+ return;
847
+ }
848
+
849
+ const payload = {provider_id: providerId};
850
+ if (deliveryRadius) payload.delivery_radius_km = deliveryRadius;
851
+ if (openTime) payload.open_time = openTime;
852
+ if (closeTime) payload.close_time = closeTime;
853
+ if (deliveryMethod) payload.delivery_method = deliveryMethod;
854
+ if (confirmMethod) payload.confirm_method = confirmMethod;
855
+ if (deliveryAreaId) payload.delivery_area_id = deliveryAreaId;
856
+ payload.settlement_wallet_id = settlementWalletId;
857
+
858
+ const response = await api.fetch(
859
+ '/marketplace/integrations/99food/store/settings',
860
+ {
861
+ method: 'POST',
862
+ body: payload,
863
+ },
864
+ );
865
+
866
+ const operationError = getFirstOperationError(response);
867
+ if (response?.error || operationError) {
868
+ showError(
869
+ response?.error ||
870
+ operationError ||
871
+ 'Nao foi possivel salvar as configuracoes.',
872
+ );
873
+ return;
874
+ }
875
+
876
+ applyStoreSettingsResponse(response);
877
+ await loadData({silent: true});
878
+ showSuccess('Configuracoes da loja atualizadas.');
879
+ } catch (error) {
880
+ showError(formatFood99ApiError(error));
881
+ }
882
+ });
883
+ }, [
884
+ applyStoreSettingsResponse,
885
+ getFirstOperationError,
886
+ loadData,
887
+ providerId,
888
+ showError,
889
+ showSuccess,
890
+ storeSettingsDraft,
891
+ wallets,
892
+ withAction,
893
+ ]);
894
+
895
+ const openQuickWalletModal = useCallback(() => {
896
+ setQuickWalletName('');
897
+ setQuickWalletModalVisible(true);
898
+ }, []);
899
+
900
+ const closeQuickWalletModal = useCallback(() => {
901
+ setQuickWalletModalVisible(false);
902
+ setQuickWalletName('');
903
+ }, []);
904
+
905
+ const handleCreateQuickWallet = useCallback(async () => {
906
+ if (!providerId) {
907
+ return;
908
+ }
909
+
910
+ const walletName = String(quickWalletName || '').trim();
911
+ if (!walletName) {
912
+ showError('Informe o nome da carteira.');
913
+ return;
914
+ }
915
+
916
+ await withAction('create-wallet', async () => {
917
+ try {
918
+ const savedWallet = await walletStore.actions.save({
919
+ people: `/people/${providerId}`,
920
+ wallet: walletName,
921
+ });
922
+
923
+ const savedWalletId = String(savedWallet?.id || '').trim();
924
+ const resolvedWallet =
925
+ (Array.isArray(walletStore.getters.items)
926
+ ? walletStore.getters.items.find(wallet => {
927
+ return (
928
+ String(wallet?.wallet || '')
929
+ .trim()
930
+ .toLowerCase() === walletName.toLowerCase()
931
+ );
932
+ })
933
+ : null) || savedWallet;
934
+ const resolvedWalletId = String(
935
+ savedWalletId || resolvedWallet?.id || '',
936
+ ).trim();
937
+
938
+ if (resolvedWalletId) {
939
+ setStoreSettingsDraft(current => ({
940
+ ...current,
941
+ settlementWalletId: resolvedWalletId,
942
+ }));
943
+ showSuccess('Carteira criada e selecionada no repasse.');
944
+ } else {
945
+ showSuccess('Carteira criada. Selecione-a no repasse.');
946
+ }
947
+ closeQuickWalletModal();
948
+ } catch (error) {
949
+ showError(formatFood99ApiError(error));
950
+ }
951
+ });
952
+ }, [
953
+ closeQuickWalletModal,
954
+ formatFood99ApiError,
955
+ providerId,
956
+ quickWalletName,
957
+ setStoreSettingsDraft,
958
+ showError,
959
+ showSuccess,
960
+ walletStore.actions,
961
+ withAction,
962
+ ]);
963
+
964
+ const handleManualBindStore = useCallback(async () => {
965
+ if (!providerId) return;
966
+
967
+ const shopId = String(manualShopId || '').trim();
968
+ if (!shopId) {
969
+ showError('Informe o shop_id para vincular a loja.');
970
+ return;
971
+ }
972
+
973
+ await withAction('bind-manual', async () => {
974
+ try {
975
+ const response = await api.fetch(
976
+ '/marketplace/integrations/99food/store/connect',
977
+ {
978
+ method: 'POST',
979
+ body: {
980
+ provider_id: providerId,
981
+ shop_id: shopId,
982
+ },
983
+ },
984
+ );
985
+
986
+ const operationError = getFirstOperationError(response);
987
+ if (response?.error || operationError) {
988
+ showError(
989
+ response?.error ||
990
+ operationError ||
991
+ 'Nao foi possivel vincular a loja.',
992
+ );
993
+ return;
994
+ }
995
+
996
+ applyStoreSettingsResponse(response);
997
+ await loadData({silent: true});
998
+ showSuccess('Loja vinculada manualmente com sucesso.');
999
+ } catch (error) {
1000
+ showError(formatFood99ApiError(error));
1001
+ }
1002
+ });
1003
+ }, [
1004
+ applyStoreSettingsResponse,
1005
+ getFirstOperationError,
1006
+ loadData,
1007
+ manualShopId,
1008
+ providerId,
1009
+ showError,
1010
+ showSuccess,
1011
+ withAction,
1012
+ ]);
1013
+
1014
+ const handleDisconnectStore = useCallback(async () => {
1015
+ if (!providerId) return;
1016
+
1017
+ await withAction('disconnect', async () => {
1018
+ try {
1019
+ const response = await api.fetch(
1020
+ '/marketplace/integrations/99food/store/disconnect',
1021
+ {
1022
+ method: 'POST',
1023
+ body: {provider_id: providerId},
1024
+ },
1025
+ );
1026
+
1027
+ const operationError = getFirstOperationError(response);
1028
+ if (response?.error || operationError) {
1029
+ showError(
1030
+ response?.error ||
1031
+ operationError ||
1032
+ 'Nao foi possivel desconectar a loja.',
1033
+ );
1034
+ return;
1035
+ }
1036
+
1037
+ setManualShopId('');
1038
+ applyStoreSettingsResponse(response);
1039
+ await loadData({silent: true});
1040
+ showSuccess('Loja desconectada da 99Food.');
1041
+ } catch (error) {
1042
+ showError(formatFood99ApiError(error));
1043
+ }
1044
+ });
1045
+ }, [
1046
+ applyStoreSettingsResponse,
1047
+ getFirstOperationError,
1048
+ loadData,
1049
+ providerId,
1050
+ showError,
1051
+ showSuccess,
1052
+ withAction,
1053
+ ]);
1054
+
1055
+ const handlePreview = useCallback(async () => {
1056
+ if (!providerId) return;
1057
+
1058
+ if (selectedEligibleProducts.length < MINIMUM_REQUIRED_ITEMS) {
1059
+ showError(
1060
+ `Selecione pelo menos ${MINIMUM_REQUIRED_ITEMS} produtos elegiveis para publicar.`,
1061
+ );
1062
+ return;
1063
+ }
1064
+
1065
+ setPreviewLoading(true);
1066
+ try {
1067
+ const response = await api.fetch(
1068
+ '/marketplace/integrations/99food/menu/preview',
1069
+ {
1070
+ method: 'POST',
1071
+ body: {
1072
+ provider_id: providerId,
1073
+ product_ids: selectedEligibleProducts.map(product => product.id),
1074
+ },
1075
+ },
1076
+ );
1077
+
1078
+ if (Array.isArray(response?.errors) && response.errors.length > 0) {
1079
+ showError(response.errors[0]);
1080
+ return;
1081
+ }
1082
+
1083
+ setPreviewData(response);
1084
+ setPreviewVisible(true);
1085
+ } catch (error) {
1086
+ showError(formatFood99ApiError(error));
1087
+ } finally {
1088
+ setPreviewLoading(false);
1089
+ }
1090
+ }, [providerId, selectedEligibleProducts, showError]);
1091
+
1092
+ const handleUpload = useCallback(async () => {
1093
+ if (!providerId) return;
1094
+
1095
+ setUploading(true);
1096
+ try {
1097
+ const response = await api.fetch(
1098
+ '/marketplace/integrations/99food/menu/upload',
1099
+ {
1100
+ method: 'POST',
1101
+ body: {
1102
+ provider_id: providerId,
1103
+ product_ids: selectedEligibleProducts.map(product => product.id),
1104
+ },
1105
+ },
1106
+ );
1107
+
1108
+ if (!isErrnoSuccess(response?.result?.errno)) {
1109
+ showError(
1110
+ response?.result?.errmsg ||
1111
+ 'Nao foi possivel publicar o menu no 99Food.',
1112
+ );
1113
+ return;
1114
+ }
1115
+
1116
+ setLastUploadResult(response);
1117
+ if (response?.integration || response?.products) {
1118
+ applyDetailResponse({
1119
+ integration: response?.integration,
1120
+ products: response?.products,
1121
+ });
1122
+ } else {
1123
+ await loadData({silent: true});
1124
+ }
1125
+
1126
+ showInfo('Cardapio enviado para processamento na 99Food.');
1127
+ setPreviewVisible(false);
1128
+
1129
+ const taskId =
1130
+ normalizeTaskId(response?.integration?.last_menu_task_id) ||
1131
+ normalizeTaskId(response?.result?.data?.taskID) ||
1132
+ normalizeTaskId(response?.result?.data?.taskId);
1133
+
1134
+ if (taskId) {
1135
+ const taskResponse = await fetchMenuTaskStatus(taskId, {poll: true});
1136
+
1137
+ if (taskResponse?.publish_state === 'published') {
1138
+ showSuccess('Cardapio publicado com sucesso na 99Food.');
1139
+ } else if (taskResponse?.publish_state === 'failed') {
1140
+ showError(
1141
+ taskResponse?.task_message ||
1142
+ 'A publicacao do cardapio falhou na 99Food.',
1143
+ );
1144
+ } else if (taskResponse?.publish_state === 'sync_error') {
1145
+ showError(
1146
+ taskResponse?.task_message ||
1147
+ 'A task foi concluida, mas o catalogo remoto ainda nao foi confirmado.',
1148
+ );
1149
+ } else {
1150
+ showInfo(
1151
+ 'A publicacao do cardapio segue em processamento na 99Food.',
1152
+ );
1153
+ }
1154
+ }
1155
+ } catch (error) {
1156
+ showError(formatFood99ApiError(error));
1157
+ } finally {
1158
+ setUploading(false);
1159
+ }
1160
+ }, [
1161
+ applyDetailResponse,
1162
+ fetchMenuTaskStatus,
1163
+ loadData,
1164
+ providerId,
1165
+ selectedEligibleProducts,
1166
+ showError,
1167
+ showInfo,
1168
+ showSuccess,
1169
+ ]);
1170
+
1171
+ const handleRefreshStoreStatus = useCallback(async () => {
1172
+ await withAction('refresh', async () => {
1173
+ if (lastMenuTaskId) {
1174
+ await fetchMenuTaskStatus(lastMenuTaskId, {poll: false});
1175
+ return;
1176
+ }
1177
+ await loadData({silent: true});
1178
+ });
1179
+ }, [fetchMenuTaskStatus, lastMenuTaskId, loadData, withAction]);
1180
+
1181
+ const handleSyncOrders = useCallback(async () => {
1182
+ if (!providerId) return;
1183
+
1184
+ await withAction('sync-orders', async () => {
1185
+ try {
1186
+ const response = await api.fetch(
1187
+ '/marketplace/integrations/99food/orders/sync',
1188
+ {
1189
+ method: 'POST',
1190
+ body: {
1191
+ provider_id: providerId,
1192
+ },
1193
+ },
1194
+ );
1195
+
1196
+ if (!isErrnoSuccess(response?.errno)) {
1197
+ showError(
1198
+ response?.errmsg ||
1199
+ 'Nao foi possivel sincronizar os pedidos da 99Food.',
1200
+ );
1201
+ return;
1202
+ }
1203
+
1204
+ const processedCount = Number(
1205
+ response?.data?.processed_order_count || 0,
1206
+ );
1207
+ const failedCount = Number(response?.data?.failed_order_count || 0);
1208
+ const acknowledgedCount = Number(
1209
+ response?.data?.acknowledged_event_count || 0,
1210
+ );
1211
+ const failedAckCount = Number(
1212
+ response?.data?.failed_acknowledged_count || 0,
1213
+ );
1214
+ const ackSummary =
1215
+ acknowledgedCount > 0
1216
+ ? `, ${acknowledgedCount} evento(s) reconhecido(s)`
1217
+ : '';
1218
+
1219
+ if (processedCount > 0) {
1220
+ if (failedCount > 0 || failedAckCount > 0) {
1221
+ showInfo(
1222
+ `Sincronizacao executada com pendencias: ${processedCount} pedido(s) importado(s), ${failedCount} falha(s)${ackSummary}.`,
1223
+ );
1224
+ } else {
1225
+ showSuccess(
1226
+ `Sincronizacao concluida: ${processedCount} pedido(s) importado(s)${ackSummary}.`,
1227
+ );
1228
+ }
1229
+ } else {
1230
+ showInfo(
1231
+ `Sincronizacao executada, mas nenhum pedido novo foi importado${ackSummary}.`,
1232
+ );
1233
+ }
1234
+
1235
+ await loadData({silent: true});
1236
+ } catch (error) {
1237
+ showError(formatFood99ApiError(error));
1238
+ }
1239
+ });
1240
+ }, [loadData, providerId, showError, showInfo, showSuccess, withAction]);
1241
+
1242
+ const handleSyncTodayHistory = useCallback(async () => {
1243
+ if (!providerId) return;
1244
+
1245
+ await withAction('sync-history', async () => {
1246
+ try {
1247
+ const fromTime = resolveFood99HistoryFromTime();
1248
+ const response = await api.fetch(
1249
+ '/marketplace/integrations/99food/orders/sync',
1250
+ {
1251
+ method: 'POST',
1252
+ body: {
1253
+ provider_id: providerId,
1254
+ ...(fromTime ? {from_time: fromTime} : {}),
1255
+ },
1256
+ },
1257
+ );
1258
+
1259
+ if (!isErrnoSuccess(response?.errno)) {
1260
+ showError(
1261
+ response?.errmsg ||
1262
+ 'Nao foi possivel sincronizar o historico de pedidos da 99Food.',
1263
+ );
1264
+ return;
1265
+ }
1266
+
1267
+ const processedCount = Number(
1268
+ response?.data?.processed_order_count || 0,
1269
+ );
1270
+ const failedCount = Number(response?.data?.failed_order_count || 0);
1271
+ const acknowledgedCount = Number(
1272
+ response?.data?.acknowledged_event_count || 0,
1273
+ );
1274
+ const failedAckCount = Number(
1275
+ response?.data?.failed_acknowledged_count || 0,
1276
+ );
1277
+ const ackSummary =
1278
+ acknowledgedCount > 0
1279
+ ? `, ${acknowledgedCount} evento(s) reconhecido(s)`
1280
+ : '';
1281
+
1282
+ if (processedCount > 0) {
1283
+ if (failedCount > 0 || failedAckCount > 0) {
1284
+ showInfo(
1285
+ `Historico de hoje executado com pendencias: ${processedCount} pedido(s) importado(s), ${failedCount} falha(s)${ackSummary}.`,
1286
+ );
1287
+ } else {
1288
+ showSuccess(
1289
+ `Historico de hoje concluido: ${processedCount} pedido(s) importado(s)${ackSummary}.`,
1290
+ );
1291
+ }
1292
+ } else {
1293
+ showInfo(
1294
+ `Historico de hoje executado, mas nenhum pedido novo foi importado${ackSummary}.`,
1295
+ );
1296
+ }
1297
+
1298
+ await loadData({silent: true});
1299
+ } catch (error) {
1300
+ showError(formatFood99ApiError(error));
1301
+ }
1302
+ });
1303
+ }, [loadData, providerId, showError, showInfo, showSuccess, withAction]);
1304
+
1305
+ const handleLoadRemoteMenu = useCallback(async () => {
1306
+ if (!providerId) return;
1307
+
1308
+ setRemoteMenuError('');
1309
+ await withAction('remote-menu', async () => {
1310
+ try {
1311
+ const response = await api.fetch(
1312
+ '/marketplace/integrations/99food/menu/import',
1313
+ {
1314
+ method: 'POST',
1315
+ body: JSON.stringify({provider_id: providerId, mode: 'snapshot'}),
1316
+ },
1317
+ );
1318
+ const result = response?.result || {};
1319
+ if (String(result?.errno ?? '0') !== '0') {
1320
+ setRemoteMenuError(
1321
+ result?.errmsg ||
1322
+ 'Nao foi possivel ler o cardapio remoto da 99Food.',
1323
+ );
1324
+ }
1325
+ setRemoteMenuSnapshot(result?.data || null);
1326
+ } catch (error) {
1327
+ setRemoteMenuError(formatFood99ApiError(error));
1328
+ }
1329
+ });
1330
+ }, [providerId, withAction]);
1331
+
1332
+ const selectionSummaryTone =
1333
+ selectedEligibleProducts.length >= MINIMUM_REQUIRED_ITEMS
1334
+ ? brandColors.success
1335
+ : brandColors.warning;
1336
+
1337
+ const sectionTabs = [
1338
+ {key: 'overview', label: 'Resumo'},
1339
+ {key: 'store', label: 'Loja'},
1340
+ {key: 'settings', label: 'Operação'},
1341
+ {key: 'catalog', label: 'Cardápio', badge: selectedEligibleProducts.length},
1342
+ {key: 'remoteCatalog', label: 'Cardápio no 99'},
1343
+ ];
1344
+
1345
+ if (!providerId) {
1346
+ return (
1347
+ <SafeAreaView style={styles.container} edges={['bottom']}>
1348
+ <View style={styles.centerState}>
1349
+ <Icon name="building" size={32} color={brandColors.textSecondary} />
1350
+ <Text style={styles.centerStateTitle}>
1351
+ Selecione uma empresa para continuar
1352
+ </Text>
1353
+ <Text style={styles.centerStateText}>
1354
+ A integração 99Food sempre trabalha com a empresa ativa no filtro.
1355
+ </Text>
1356
+ </View>
1357
+ </SafeAreaView>
1358
+ );
1359
+ }
1360
+
1361
+ if (loading) {
1362
+ return (
1363
+ <SafeAreaView style={styles.container} edges={['bottom']}>
1364
+ <View style={styles.centerState}>
1365
+ <ActivityIndicator size="large" color={brandColors.primary} />
1366
+ <Text style={styles.centerStateTitle}>Carregando integrações</Text>
1367
+ <Text style={styles.centerStateText}>
1368
+ Estamos buscando o status da loja e os produtos disponíveis.
1369
+ </Text>
1370
+ </View>
1371
+ </SafeAreaView>
1372
+ );
1373
+ }
1374
+
1375
+ return (
1376
+ <SafeAreaView
1377
+ style={[styles.container, {backgroundColor: brandColors.background}]}
1378
+ edges={['bottom']}>
1379
+ <ScrollView
1380
+ contentContainerStyle={styles.scroll}
1381
+ showsVerticalScrollIndicator={false}
1382
+ refreshControl={
1383
+ <RefreshControl
1384
+ refreshing={refreshing}
1385
+ onRefresh={onRefresh}
1386
+ tintColor={brandColors.primary}
1387
+ />
1388
+ }>
1389
+ <IntegrationHero
1390
+ shadowStyle={integrationCardShadowStyle}
1391
+ accentColor={brandColors.primary}
1392
+ palette={heroPalette}
1393
+ eyebrow="Marketplace"
1394
+ title="Integração 99Food"
1395
+ description="Separei leitura geral, vínculo da loja, configuração operacional e catálogo para reduzir a complexidade da tela."
1396
+ logo={logo}
1397
+ iconName="cpu"
1398
+ />
1399
+
1400
+ <IntegrationTabs
1401
+ tabs={sectionTabs}
1402
+ activeKey={activeTab}
1403
+ onChange={setActiveTab}
1404
+ accentColor={brandColors.primary}
1405
+ palette={brandColors}
1406
+ />
1407
+
1408
+ {activeTab === 'overview' && (
1409
+ <Food99OverviewTab
1410
+ shadowStyle={integrationCardShadowStyle}
1411
+ palette={brandColors}
1412
+ currentCompany={currentCompany}
1413
+ providerId={providerId}
1414
+ connected={connected}
1415
+ needsReconnect={needsReconnect}
1416
+ summaryCards={summaryCards}
1417
+ lastMenuPublishState={lastMenuPublishState}
1418
+ publicationTone={publicationTone}
1419
+ publishStateLabel={getPublishStateLabel(lastMenuPublishState)}
1420
+ lastMenuTaskMessage={lastMenuTaskMessage}
1421
+ lastErrorMessage={lastErrorMessage}
1422
+ />
1423
+ )}
1424
+
1425
+ {activeTab === 'store' && (
1426
+ <Food99StoreTab
1427
+ shadowStyle={integrationCardShadowStyle}
1428
+ accentColor={brandColors.primary}
1429
+ palette={brandColors}
1430
+ statusRows={storeStatusRows}
1431
+ lastMenuPublishState={lastMenuPublishState}
1432
+ publicationTone={publicationTone}
1433
+ publishStateLabel={getPublishStateLabel(lastMenuPublishState)}
1434
+ lastMenuTaskMessage={lastMenuTaskMessage}
1435
+ lastErrorMessage={lastErrorMessage}
1436
+ actionLoading={actionLoading}
1437
+ connected={connected}
1438
+ needsReconnect={needsReconnect}
1439
+ isOnline={isOnline}
1440
+ onRefresh={handleRefreshStoreStatus}
1441
+ onConnect={handleConnectStore}
1442
+ onToggleStatus={() => handleStoreStatusChange(isOnline ? 2 : 1)}
1443
+ onSyncOrders={handleSyncOrders}
1444
+ onSyncTodayHistory={handleSyncTodayHistory}
1445
+ manualShopId={manualShopId}
1446
+ setManualShopId={setManualShopId}
1447
+ onManualBind={handleManualBindStore}
1448
+ onDisconnect={handleDisconnectStore}
1449
+ />
1450
+ )}
1451
+
1452
+ {activeTab === 'settings' && (
1453
+ <Food99SettingsTab
1454
+ shadowStyle={integrationCardShadowStyle}
1455
+ accentColor={brandColors.primary}
1456
+ palette={brandColors}
1457
+ settingsSummaryRows={settingsSummaryRows}
1458
+ storeSettingsDraft={storeSettingsDraft}
1459
+ setStoreSettingsDraft={setStoreSettingsDraft}
1460
+ wallets={wallets}
1461
+ walletLoading={isLoadingWallets}
1462
+ actionLoading={actionLoading}
1463
+ onQuickCreateWallet={openQuickWalletModal}
1464
+ onSave={handleSaveStoreSettings}
1465
+ />
1466
+ )}
1467
+
1468
+ {activeTab === 'catalog' && (
1469
+ <Food99CatalogTab
1470
+ shadowStyle={integrationCardShadowStyle}
1471
+ accentColor={brandColors.primary}
1472
+ palette={brandColors}
1473
+ selectionSummaryTone={selectionSummaryTone}
1474
+ search={search}
1475
+ setSearch={setSearch}
1476
+ filterKey={filterKey}
1477
+ setFilterKey={setFilterKey}
1478
+ tabCounts={tabCounts}
1479
+ productsResponse={productsResponse}
1480
+ selectedEligibleProducts={selectedEligibleProducts}
1481
+ previewLoading={previewLoading}
1482
+ onPreview={handlePreview}
1483
+ filteredProducts={filteredProducts}
1484
+ selectedProductSet={selectedProductSet}
1485
+ onToggleProduct={handleProductCardPress}
1486
+ onMarkCardPressHandled={markNextProductCardPressAsHandled}
1487
+ />
1488
+ )}
1489
+
1490
+ {activeTab === 'remoteCatalog' && (
1491
+ <RemoteMarketplaceMenuTab
1492
+ shadowStyle={integrationCardShadowStyle}
1493
+ accentColor={brandColors.primary}
1494
+ palette={brandColors}
1495
+ title="Cardapio no 99"
1496
+ subtitle="Leitura direta do cardapio remoto da loja selecionada na 99Food."
1497
+ buttonLabel="Ler cardapio do 99"
1498
+ loading={actionLoading === 'remote-menu'}
1499
+ snapshot={remoteMenuSnapshot}
1500
+ errorMessage={remoteMenuError}
1501
+ onLoad={handleLoadRemoteMenu}
1502
+ />
1503
+ )}
1504
+ </ScrollView>
1505
+
1506
+ <Food99PreviewModal
1507
+ visible={previewVisible}
1508
+ previewData={previewData}
1509
+ selectedEligibleProducts={selectedEligibleProducts}
1510
+ uploading={uploading}
1511
+ accentColor={brandColors.primary}
1512
+ palette={brandColors}
1513
+ onClose={() => setPreviewVisible(false)}
1514
+ onUpload={handleUpload}
1515
+ />
1516
+
1517
+ <Food99QuickWalletModal
1518
+ visible={quickWalletModalVisible}
1519
+ walletName={quickWalletName}
1520
+ setWalletName={setQuickWalletName}
1521
+ actionLoading={actionLoading}
1522
+ accentColor={brandColors.primary}
1523
+ palette={brandColors}
1524
+ onClose={closeQuickWalletModal}
1525
+ onCreate={handleCreateQuickWallet}
1526
+ />
1527
+ </SafeAreaView>
1528
+ );
1529
+ }
1530
+ // 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.