@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,1298 @@
1
+ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ RefreshControl,
5
+ ScrollView,
6
+ Text,
7
+ View,
8
+ } from 'react-native';
9
+ import {SafeAreaView} from 'react-native-safe-area-context';
10
+ import {useFocusEffect} from '@react-navigation/native';
11
+ import Icon from 'react-native-vector-icons/Feather';
12
+
13
+ import {api} from '@controleonline/ui-common/src/api';
14
+ import useToastMessage from '@controleonline/ui-crm/src/react/hooks/useToastMessage';
15
+ import {useStore} from '@store';
16
+ import {colors} from '@controleonline/../../src/styles/colors';
17
+ import {resolveThemePalette} from '@controleonline/../../src/styles/branding';
18
+ import {getOrderChannelLogo} from '@assets/ppc/channels';
19
+
20
+ import IntegrationHero from '../../components/integrations/IntegrationHero';
21
+ import IntegrationTabs from '../../components/integrations/IntegrationTabs';
22
+ import {integrationCardShadowStyle} from '@controleonline/ui-common/src/react/utils/integrationPage';
23
+ import IFoodCatalogTab from './components/IFoodCatalogTab';
24
+ import IFoodOperationsTab from './components/IFoodOperationsTab';
25
+ import IFoodOverviewTab from './components/IFoodOverviewTab';
26
+ import IFoodPreviewModal from './components/IFoodPreviewModal';
27
+ import IFoodStoreTab from './components/IFoodStoreTab';
28
+ import RemoteMarketplaceMenuTab from '../MarketplaceIntegrationPage/components/RemoteMarketplaceMenuTab';
29
+ import styles from './styles';
30
+ import {
31
+ calcDuration,
32
+ DAY_ORDER,
33
+ buildDefaultInterruptionDraft,
34
+ formatIFoodApiError,
35
+ toIFoodInterruptionDateTime,
36
+ } from './utils';
37
+
38
+ // Página principal da integração iFood, agora reduzida a orquestração.
39
+ export default function IFoodIntegrationPage() {
40
+ const peopleStore = useStore('people');
41
+ const themeStore = useStore('theme');
42
+ const {currentCompany} = peopleStore.getters;
43
+ const {colors: themeColors} = themeStore.getters;
44
+ const {showError, showInfo} = useToastMessage();
45
+
46
+ const brandColors = useMemo(
47
+ () =>
48
+ resolveThemePalette(
49
+ {...themeColors, ...(currentCompany?.theme?.colors || {})},
50
+ colors,
51
+ ),
52
+ [themeColors, currentCompany?.id],
53
+ );
54
+
55
+ const providerId = currentCompany?.id;
56
+
57
+ const [activeTab, setActiveTab] = useState('overview');
58
+ const [loading, setLoading] = useState(true);
59
+ const [refreshing, setRefreshing] = useState(false);
60
+ const [actionLoading, setActionLoading] = useState(null);
61
+ const [detail, setDetail] = useState(null);
62
+ const [productsResponse, setProductsResponse] = useState(null);
63
+ const [merchantIdInput, setMerchantIdInput] = useState('');
64
+ const [selectedIds, setSelectedIds] = useState(new Set());
65
+ const [filterKey, setFilterKey] = useState('all');
66
+ const [search, setSearch] = useState('');
67
+ const [storeStatus, setStoreStatus] = useState(null);
68
+ const [previewVisible, setPreviewVisible] = useState(false);
69
+ const [previewData, setPreviewData] = useState(null);
70
+ const [itemStatusLoading, setItemStatusLoading] = useState(new Set());
71
+ const [itemPriceEditing, setItemPriceEditing] = useState({});
72
+ const [itemPriceLoading, setItemPriceLoading] = useState(new Set());
73
+ const [hours, setHours] = useState(null);
74
+ const [hoursLoading, setHoursLoading] = useState(false);
75
+ const [hoursSaving, setHoursSaving] = useState(false);
76
+ const [hoursEditing, setHoursEditing] = useState(false);
77
+ const [hoursDraft, setHoursDraft] = useState(null);
78
+ const [interruptionDraft, setInterruptionDraft] = useState(
79
+ buildDefaultInterruptionDraft,
80
+ );
81
+ const [interruptionRemoving, setInterruptionRemoving] = useState(new Set());
82
+ const [optStatusLoading, setOptStatusLoading] = useState(new Set());
83
+ const [optPriceEditing, setOptPriceEditing] = useState({});
84
+ const [optPriceLoading, setOptPriceLoading] = useState(new Set());
85
+ const [remoteMenuSnapshot, setRemoteMenuSnapshot] = useState(null);
86
+ const [remoteMenuError, setRemoteMenuError] = useState('');
87
+ const ignoreNextProductCardPressRef = useRef(false);
88
+ const hasLoadedOnceRef = useRef(false);
89
+
90
+ useEffect(() => {
91
+ hasLoadedOnceRef.current = false;
92
+ }, [providerId]);
93
+
94
+ // Lista local de produtos retornados pela API do iFood.
95
+ const products = useMemo(
96
+ () =>
97
+ Array.isArray(productsResponse?.products)
98
+ ? productsResponse.products
99
+ : [],
100
+ [productsResponse],
101
+ );
102
+
103
+ const syncPublishedSelection = useCallback(productList => {
104
+ const ids = (Array.isArray(productList) ? productList : [])
105
+ .filter(product => product?.eligible && product?.published_remotely)
106
+ .map(product => String(product.id));
107
+ setSelectedIds(new Set(ids));
108
+ }, []);
109
+
110
+ const loadHours = useCallback(async () => {
111
+ if (!providerId) return;
112
+
113
+ setHoursLoading(true);
114
+ try {
115
+ const response = await api.fetch(
116
+ '/marketplace/integrations/ifood/store/hours',
117
+ {
118
+ params: {provider_id: providerId},
119
+ },
120
+ );
121
+ const raw =
122
+ response?.result?.data?.shifts ??
123
+ response?.result?.data ??
124
+ response?.data ??
125
+ [];
126
+
127
+ if (Array.isArray(raw) && raw.length > 0) {
128
+ const groupByDay = list =>
129
+ list.reduce((accumulator, {dayOfWeek, start, duration}) => {
130
+ let entry = accumulator.find(item => item.dayOfWeek === dayOfWeek);
131
+ if (!entry) {
132
+ entry = {dayOfWeek, shifts: []};
133
+ accumulator.push(entry);
134
+ }
135
+ entry.shifts.push({start: String(start).substring(0, 5), duration});
136
+ return accumulator;
137
+ }, []);
138
+
139
+ let grouped;
140
+ if (raw[0]?.dayOfWeek && Array.isArray(raw[0]?.shifts)) {
141
+ grouped = raw;
142
+ } else if (!raw[0]?.dayOfWeek && raw[0]?.shifts) {
143
+ grouped = groupByDay(raw.flatMap(item => item.shifts || []));
144
+ } else {
145
+ grouped = groupByDay(raw);
146
+ }
147
+
148
+ setHours(grouped);
149
+ }
150
+ } catch {
151
+ // Horário é complementar. A tela continua funcionando sem esse dado.
152
+ } finally {
153
+ setHoursLoading(false);
154
+ }
155
+ }, [providerId]);
156
+
157
+ const applyDetailResponse = useCallback(
158
+ (response, {syncSelection = false} = {}) => {
159
+ setDetail(response || null);
160
+
161
+ const integrationMerchantId = String(
162
+ response?.integration?.ifood_code ||
163
+ response?.integration?.merchant_id ||
164
+ response?.selected_store?.merchant_id ||
165
+ '',
166
+ );
167
+
168
+ if (integrationMerchantId !== '') {
169
+ setMerchantIdInput(integrationMerchantId);
170
+ }
171
+
172
+ if (response?.products) {
173
+ setProductsResponse(response.products);
174
+ if (syncSelection) {
175
+ syncPublishedSelection(response.products?.products || []);
176
+ }
177
+ }
178
+
179
+ if (response?.integration?.connected) {
180
+ loadHours();
181
+ }
182
+ },
183
+ [loadHours, syncPublishedSelection],
184
+ );
185
+
186
+ const loadDetail = useCallback(
187
+ async ({refreshRemote = false} = {}) => {
188
+ if (!providerId) {
189
+ setLoading(false);
190
+ return;
191
+ }
192
+
193
+ try {
194
+ const params = {provider_id: providerId};
195
+ if (refreshRemote) params.refresh_remote = 1;
196
+ const response = await api.fetch(
197
+ '/marketplace/integrations/ifood/detail',
198
+ {params},
199
+ );
200
+ applyDetailResponse(response);
201
+ } catch (error) {
202
+ showError(formatIFoodApiError(error));
203
+ } finally {
204
+ setLoading(false);
205
+ }
206
+ },
207
+ [applyDetailResponse, providerId, showError],
208
+ );
209
+
210
+ const loadProducts = useCallback(async () => {
211
+ if (!providerId) return;
212
+
213
+ try {
214
+ const response = await api.fetch(
215
+ '/marketplace/integrations/ifood/menu/products',
216
+ {
217
+ params: {provider_id: providerId},
218
+ },
219
+ );
220
+
221
+ if (response?.products) {
222
+ setProductsResponse(response.products);
223
+ syncPublishedSelection(response.products?.products || []);
224
+ }
225
+ } catch (error) {
226
+ showError(formatIFoodApiError(error));
227
+ }
228
+ }, [providerId, showError, syncPublishedSelection]);
229
+
230
+ const loadStoreStatus = useCallback(async () => {
231
+ if (!providerId) return;
232
+
233
+ try {
234
+ const response = await api.fetch(
235
+ '/marketplace/integrations/ifood/store/status',
236
+ {
237
+ params: {provider_id: providerId},
238
+ },
239
+ );
240
+ if (response?.result) setStoreStatus(response.result);
241
+ } catch {
242
+ // Status remoto não deve bloquear a navegação da tela.
243
+ }
244
+ }, [providerId]);
245
+
246
+ useFocusEffect(
247
+ useCallback(() => {
248
+ if (!providerId || hasLoadedOnceRef.current) {
249
+ return undefined;
250
+ }
251
+
252
+ hasLoadedOnceRef.current = true;
253
+ loadDetail();
254
+ loadProducts();
255
+ loadStoreStatus();
256
+
257
+ return undefined;
258
+ }, [loadDetail, loadProducts, loadStoreStatus, providerId]),
259
+ );
260
+
261
+ const onRefresh = useCallback(async () => {
262
+ setRefreshing(true);
263
+ try {
264
+ await Promise.all([
265
+ loadDetail({refreshRemote: true}),
266
+ loadProducts(),
267
+ loadStoreStatus(),
268
+ ]);
269
+ } finally {
270
+ setRefreshing(false);
271
+ }
272
+ }, [loadDetail, loadProducts, loadStoreStatus]);
273
+
274
+ const withAction = useCallback(async (actionName, callback) => {
275
+ setActionLoading(actionName);
276
+ try {
277
+ await callback();
278
+ } finally {
279
+ setActionLoading(null);
280
+ }
281
+ }, []);
282
+
283
+ const filteredProducts = useMemo(() => {
284
+ const query = search.trim().toLowerCase();
285
+
286
+ return products.filter(product => {
287
+ if (filterKey === 'eligible' && !product.eligible) return false;
288
+ if (filterKey === 'blocked' && product.eligible) return false;
289
+ if (filterKey === 'selected' && !selectedIds.has(String(product.id)))
290
+ return false;
291
+
292
+ if (query) {
293
+ const haystack = [
294
+ product?.name,
295
+ product?.description,
296
+ product?.category?.name,
297
+ product?.ifood_item_id,
298
+ product?.id,
299
+ ]
300
+ .filter(Boolean)
301
+ .join(' ')
302
+ .toLowerCase();
303
+
304
+ if (!haystack.includes(query)) return false;
305
+ }
306
+
307
+ return true;
308
+ });
309
+ }, [filterKey, products, search, selectedIds]);
310
+
311
+ const toggleProduct = useCallback(id => {
312
+ const key = String(id);
313
+ setSelectedIds(current => {
314
+ const next = new Set(current);
315
+ next.has(key) ? next.delete(key) : next.add(key);
316
+ return next;
317
+ });
318
+ }, []);
319
+
320
+ const markNextProductCardPressAsHandled = useCallback(() => {
321
+ ignoreNextProductCardPressRef.current = true;
322
+
323
+ const release = () => {
324
+ ignoreNextProductCardPressRef.current = false;
325
+ };
326
+
327
+ if (typeof requestAnimationFrame === 'function') {
328
+ requestAnimationFrame(release);
329
+ return;
330
+ }
331
+
332
+ setTimeout(release, 0);
333
+ }, []);
334
+
335
+ const blockNextProductCardPress = useCallback(
336
+ event => {
337
+ event?.preventDefault?.();
338
+ event?.stopPropagation?.();
339
+ markNextProductCardPressAsHandled();
340
+ },
341
+ [markNextProductCardPressAsHandled],
342
+ );
343
+
344
+ const handleProductCardPress = useCallback(
345
+ id => {
346
+ if (ignoreNextProductCardPressRef.current) {
347
+ ignoreNextProductCardPressRef.current = false;
348
+ return;
349
+ }
350
+
351
+ toggleProduct(id);
352
+ },
353
+ [toggleProduct],
354
+ );
355
+
356
+ const selectedEligible = useMemo(
357
+ () =>
358
+ products.filter(
359
+ product => product.eligible && selectedIds.has(String(product.id)),
360
+ ),
361
+ [products, selectedIds],
362
+ );
363
+
364
+ const handleSync = useCallback(async () => {
365
+ if (!providerId) return;
366
+
367
+ await withAction('sync', async () => {
368
+ try {
369
+ const response = await api.fetch(
370
+ '/marketplace/integrations/ifood/sync',
371
+ {
372
+ method: 'POST',
373
+ body: JSON.stringify({provider_id: providerId}),
374
+ },
375
+ );
376
+ applyDetailResponse(response);
377
+ showInfo('Estado da integracao iFood sincronizado.');
378
+ } catch (error) {
379
+ showError(formatIFoodApiError(error));
380
+ }
381
+ });
382
+ }, [applyDetailResponse, providerId, showError, showInfo, withAction]);
383
+
384
+ const handleCatalogSync = useCallback(async () => {
385
+ if (!providerId) return;
386
+
387
+ await withAction('catalog_sync', async () => {
388
+ try {
389
+ const response = await api.fetch(
390
+ '/marketplace/integrations/ifood/menu/sync',
391
+ {
392
+ method: 'POST',
393
+ body: JSON.stringify({provider_id: providerId}),
394
+ },
395
+ );
396
+ applyDetailResponse(response, {syncSelection: true});
397
+ const synced = response?.result?.data?.synced ?? 0;
398
+ showInfo(
399
+ `Catalogo iFood sincronizado. ${synced} produto(s) vinculado(s).`,
400
+ );
401
+ } catch (error) {
402
+ showError(formatIFoodApiError(error));
403
+ }
404
+ });
405
+ }, [applyDetailResponse, providerId, showError, showInfo, withAction]);
406
+
407
+ const handleLoadRemoteMenu = useCallback(async () => {
408
+ if (!providerId) return;
409
+
410
+ setRemoteMenuError('');
411
+ await withAction('remote-menu', async () => {
412
+ try {
413
+ const response = await api.fetch(
414
+ '/marketplace/integrations/ifood/menu/import',
415
+ {
416
+ method: 'POST',
417
+ body: JSON.stringify({provider_id: providerId, mode: 'snapshot'}),
418
+ },
419
+ );
420
+ const result = response?.result || {};
421
+ if (String(result?.errno ?? '0') !== '0') {
422
+ setRemoteMenuError(
423
+ result?.errmsg ||
424
+ 'Nao foi possivel ler o cardapio remoto do iFood.',
425
+ );
426
+ }
427
+ setRemoteMenuSnapshot(result?.data || null);
428
+ } catch (error) {
429
+ setRemoteMenuError(formatIFoodApiError(error));
430
+ }
431
+ });
432
+ }, [providerId, withAction]);
433
+
434
+ const handleMenuUpload = useCallback(
435
+ async (selectedProducts = selectedEligible) => {
436
+ if (!providerId) return;
437
+
438
+ if (!Array.isArray(selectedProducts) || selectedProducts.length === 0) {
439
+ showInfo('Selecione ao menos um produto elegivel para publicar.');
440
+ return;
441
+ }
442
+
443
+ await withAction('menu_upload', async () => {
444
+ try {
445
+ const response = await api.fetch(
446
+ '/marketplace/integrations/ifood/menu/upload',
447
+ {
448
+ method: 'POST',
449
+ body: JSON.stringify({
450
+ provider_id: providerId,
451
+ product_ids: selectedProducts.map(product => product.id),
452
+ }),
453
+ },
454
+ );
455
+
456
+ applyDetailResponse(response, {syncSelection: true});
457
+ if (String(response?.result?.errno ?? '') === '0') {
458
+ const pushed = response?.result?.data?.pushed_count ?? 0;
459
+ showInfo(
460
+ `Cardapio enviado ao iFood. ${pushed} produto(s) publicado(s).`,
461
+ );
462
+ setPreviewVisible(false);
463
+ return;
464
+ }
465
+
466
+ showError(formatIFoodApiError(response?.result || response));
467
+ } catch (error) {
468
+ showError(formatIFoodApiError(error));
469
+ }
470
+ });
471
+ },
472
+ [
473
+ applyDetailResponse,
474
+ providerId,
475
+ selectedEligible,
476
+ showError,
477
+ showInfo,
478
+ withAction,
479
+ ],
480
+ );
481
+
482
+ const minimumRequiredItems = Number(
483
+ productsResponse?.minimum_required_items || 1,
484
+ );
485
+
486
+ const buildPreviewData = useCallback(() => {
487
+ const categoriesMap = new Map();
488
+
489
+ selectedEligible.forEach(product => {
490
+ const categoryName = String(
491
+ product?.category?.name || 'Sem categoria',
492
+ ).trim();
493
+ const currentCount = categoriesMap.get(categoryName) || 0;
494
+ categoriesMap.set(categoryName, currentCount + 1);
495
+ });
496
+
497
+ const categories = Array.from(categoriesMap.entries()).map(
498
+ ([categoryName, count], index) => ({
499
+ app_category_id: `${index + 1}`,
500
+ category_name: categoryName,
501
+ app_item_ids: Array.from({length: count}).map(
502
+ (_, itemIndex) => `${categoryName}-${itemIndex + 1}`,
503
+ ),
504
+ }),
505
+ );
506
+
507
+ return {
508
+ eligible_product_count: selectedEligible.length,
509
+ payload: {
510
+ menus:
511
+ selectedEligible.length > 0
512
+ ? [{app_menu_id: 'ifood_default_menu'}]
513
+ : [],
514
+ categories,
515
+ items: selectedEligible.map(product => ({
516
+ app_item_id: String(product.id),
517
+ item_name: product.name,
518
+ })),
519
+ },
520
+ };
521
+ }, [selectedEligible]);
522
+
523
+ const handleOpenPreview = useCallback(() => {
524
+ if (selectedEligible.length < minimumRequiredItems) {
525
+ showInfo(
526
+ `Selecione pelo menos ${minimumRequiredItems} produto(s) elegivel(is) para pre-visualizar.`,
527
+ );
528
+ return;
529
+ }
530
+
531
+ setPreviewData(buildPreviewData());
532
+ setPreviewVisible(true);
533
+ }, [
534
+ buildPreviewData,
535
+ minimumRequiredItems,
536
+ selectedEligible.length,
537
+ showInfo,
538
+ ]);
539
+
540
+ const handleStoreOpen = useCallback(async () => {
541
+ if (!providerId) return;
542
+
543
+ await withAction('store_open', async () => {
544
+ try {
545
+ const response = await api.fetch(
546
+ '/marketplace/integrations/ifood/store/open',
547
+ {
548
+ method: 'POST',
549
+ body: JSON.stringify({provider_id: providerId}),
550
+ },
551
+ );
552
+ if (response?.result) setStoreStatus(response.result);
553
+
554
+ if (String(response?.result?.errno ?? '') === '0') {
555
+ showInfo('Loja aberta para pedidos no iFood.');
556
+ applyDetailResponse(response);
557
+ } else {
558
+ showError(formatIFoodApiError(response?.result || response));
559
+ }
560
+ } catch (error) {
561
+ showError(formatIFoodApiError(error));
562
+ }
563
+ });
564
+ }, [applyDetailResponse, providerId, showError, showInfo, withAction]);
565
+
566
+ const handleStoreClose = useCallback(async () => {
567
+ if (!providerId) return;
568
+
569
+ await withAction('store_close', async () => {
570
+ try {
571
+ const response = await api.fetch(
572
+ '/marketplace/integrations/ifood/store/close',
573
+ {
574
+ method: 'POST',
575
+ body: JSON.stringify({
576
+ provider_id: providerId,
577
+ description: interruptionDraft.description,
578
+ start: toIFoodInterruptionDateTime(interruptionDraft.start),
579
+ end: toIFoodInterruptionDateTime(interruptionDraft.end),
580
+ }),
581
+ },
582
+ );
583
+ if (response?.result) setStoreStatus(response.result);
584
+
585
+ if (String(response?.result?.errno ?? '') === '0') {
586
+ showInfo('Loja fechada para pedidos no iFood.');
587
+ applyDetailResponse(response);
588
+ setInterruptionDraft(buildDefaultInterruptionDraft());
589
+ } else {
590
+ showError(formatIFoodApiError(response?.result || response));
591
+ }
592
+ } catch (error) {
593
+ showError(formatIFoodApiError(error));
594
+ }
595
+ });
596
+ }, [
597
+ applyDetailResponse,
598
+ interruptionDraft,
599
+ providerId,
600
+ showError,
601
+ showInfo,
602
+ withAction,
603
+ ]);
604
+
605
+ const handleInterruptionDraftChange = useCallback((field, value) => {
606
+ setInterruptionDraft(current => ({...current, [field]: value}));
607
+ }, []);
608
+
609
+ const handleRemoveInterruption = useCallback(
610
+ async interruption => {
611
+ if (!providerId) return;
612
+
613
+ const interruptionId = String(interruption?.id || '').trim();
614
+ if (!interruptionId) {
615
+ showError('Pausa iFood sem identificador para remocao.');
616
+ return;
617
+ }
618
+
619
+ setInterruptionRemoving(current => new Set([...current, interruptionId]));
620
+ try {
621
+ const response = await api.fetch(
622
+ `/marketplace/integrations/ifood/store/interruptions/${encodeURIComponent(interruptionId)}`,
623
+ {
624
+ method: 'DELETE',
625
+ body: JSON.stringify({provider_id: providerId}),
626
+ },
627
+ );
628
+
629
+ if (response?.result) setStoreStatus(response.result);
630
+
631
+ if (String(response?.result?.errno ?? '') === '0') {
632
+ showInfo('Pausa removida no iFood.');
633
+ applyDetailResponse(response);
634
+ } else {
635
+ showError(formatIFoodApiError(response?.result || response));
636
+ }
637
+ } catch (error) {
638
+ showError(formatIFoodApiError(error));
639
+ } finally {
640
+ setInterruptionRemoving(current => {
641
+ const next = new Set(current);
642
+ next.delete(interruptionId);
643
+ return next;
644
+ });
645
+ }
646
+ },
647
+ [applyDetailResponse, providerId, showError, showInfo],
648
+ );
649
+
650
+ const handleConnect = useCallback(async () => {
651
+ if (!providerId) return;
652
+
653
+ const merchantId = String(merchantIdInput || '').trim();
654
+ if (!merchantId) {
655
+ showInfo('Informe o código da loja iFood para conectar.');
656
+ return;
657
+ }
658
+
659
+ await withAction('connect', async () => {
660
+ try {
661
+ const response = await api.fetch(
662
+ '/marketplace/integrations/ifood/store/connect',
663
+ {
664
+ method: 'POST',
665
+ body: JSON.stringify({
666
+ provider_id: providerId,
667
+ merchant_id: merchantId,
668
+ }),
669
+ },
670
+ );
671
+ applyDetailResponse(response);
672
+ showInfo('Loja iFood conectada com sucesso.');
673
+ loadProducts();
674
+ } catch (error) {
675
+ showError(formatIFoodApiError(error));
676
+ }
677
+ });
678
+ }, [
679
+ applyDetailResponse,
680
+ loadProducts,
681
+ merchantIdInput,
682
+ providerId,
683
+ showError,
684
+ showInfo,
685
+ withAction,
686
+ ]);
687
+
688
+ const handleDisconnect = useCallback(async () => {
689
+ if (!providerId) return;
690
+
691
+ await withAction('disconnect', async () => {
692
+ try {
693
+ const response = await api.fetch(
694
+ '/marketplace/integrations/ifood/store/disconnect',
695
+ {
696
+ method: 'POST',
697
+ body: JSON.stringify({provider_id: providerId}),
698
+ },
699
+ );
700
+ applyDetailResponse(response);
701
+ setMerchantIdInput('');
702
+ } catch (error) {
703
+ showError(formatIFoodApiError(error));
704
+ }
705
+ });
706
+ }, [applyDetailResponse, providerId, showError, withAction]);
707
+
708
+ const handleItemStatusToggle = useCallback(
709
+ async product => {
710
+ if (!providerId || !product?.ifood_item_id) return;
711
+
712
+ const itemId = product.ifood_item_id;
713
+ const nextStatus =
714
+ product.ifood_status === 'UNAVAILABLE' ? 'AVAILABLE' : 'UNAVAILABLE';
715
+ setItemStatusLoading(current => new Set([...current, itemId]));
716
+
717
+ try {
718
+ const response = await api.fetch(
719
+ '/marketplace/integrations/ifood/menu/item/status',
720
+ {
721
+ method: 'PATCH',
722
+ body: JSON.stringify({
723
+ provider_id: providerId,
724
+ item_id: itemId,
725
+ status: nextStatus,
726
+ }),
727
+ },
728
+ );
729
+
730
+ if (String(response?.result?.errno ?? '') === '0') {
731
+ setProductsResponse(current => {
732
+ if (!current?.products) return current;
733
+ return {
734
+ ...current,
735
+ products: current.products.map(item =>
736
+ item.ifood_item_id === itemId
737
+ ? {...item, ifood_status: nextStatus}
738
+ : item,
739
+ ),
740
+ };
741
+ });
742
+ showInfo(
743
+ `Item ${nextStatus === 'AVAILABLE' ? 'disponivel' : 'indisponivel'} no iFood.`,
744
+ );
745
+ } else {
746
+ showError(formatIFoodApiError(response?.result || response));
747
+ }
748
+ } catch (error) {
749
+ showError(formatIFoodApiError(error));
750
+ } finally {
751
+ setItemStatusLoading(current => {
752
+ const next = new Set(current);
753
+ next.delete(itemId);
754
+ return next;
755
+ });
756
+ }
757
+ },
758
+ [providerId, showError, showInfo],
759
+ );
760
+
761
+ const handleItemPriceSave = useCallback(
762
+ async product => {
763
+ if (!providerId || !product?.ifood_item_id) return;
764
+
765
+ const itemId = product.ifood_item_id;
766
+ const raw = itemPriceEditing[itemId];
767
+ const price = parseFloat(String(raw ?? '').replace(',', '.'));
768
+
769
+ if (!Number.isFinite(price) || price <= 0) {
770
+ showError('Informe um preco valido maior que zero.');
771
+ return;
772
+ }
773
+
774
+ setItemPriceLoading(current => new Set([...current, itemId]));
775
+
776
+ try {
777
+ const response = await api.fetch(
778
+ '/marketplace/integrations/ifood/menu/item/price',
779
+ {
780
+ method: 'PATCH',
781
+ body: JSON.stringify({
782
+ provider_id: providerId,
783
+ item_id: itemId,
784
+ price,
785
+ }),
786
+ },
787
+ );
788
+
789
+ if (String(response?.result?.errno ?? '') === '0') {
790
+ setProductsResponse(current => {
791
+ if (!current?.products) return current;
792
+ return {
793
+ ...current,
794
+ products: current.products.map(item =>
795
+ item.ifood_item_id === itemId ? {...item, price} : item,
796
+ ),
797
+ };
798
+ });
799
+ setItemPriceEditing(current => {
800
+ const next = {...current};
801
+ delete next[itemId];
802
+ return next;
803
+ });
804
+ showInfo('Preco atualizado no iFood.');
805
+ } else {
806
+ showError(formatIFoodApiError(response?.result || response));
807
+ }
808
+ } catch (error) {
809
+ showError(formatIFoodApiError(error));
810
+ } finally {
811
+ setItemPriceLoading(current => {
812
+ const next = new Set(current);
813
+ next.delete(itemId);
814
+ return next;
815
+ });
816
+ }
817
+ },
818
+ [itemPriceEditing, providerId, showError, showInfo],
819
+ );
820
+
821
+ const startEditHours = useCallback(() => {
822
+ const draft = DAY_ORDER.map(day => {
823
+ const existing = (hours || []).find(item => item.dayOfWeek === day);
824
+ const isOpen = !!existing && (existing.shifts || []).length > 0;
825
+ const shifts = isOpen
826
+ ? existing.shifts
827
+ : [{start: '09:00', duration: 840}];
828
+ return {dayOfWeek: day, open: isOpen, shifts};
829
+ });
830
+
831
+ setHoursDraft(draft);
832
+ setHoursEditing(true);
833
+ }, [hours]);
834
+
835
+ const cancelEditHours = useCallback(() => {
836
+ setHoursEditing(false);
837
+ setHoursDraft(null);
838
+ }, []);
839
+
840
+ const updateHoursDraft = useCallback(
841
+ (dayOfWeek, shiftIndex, field, value) => {
842
+ setHoursDraft(current => {
843
+ if (!Array.isArray(current)) return current;
844
+
845
+ return current.map(day => {
846
+ if (day.dayOfWeek !== dayOfWeek) return day;
847
+ if (field === 'open') return {...day, open: value};
848
+
849
+ const shifts = (day.shifts || []).map((shift, index) => {
850
+ if (index !== shiftIndex) return shift;
851
+ if (field === 'end')
852
+ return {...shift, duration: calcDuration(shift.start, value)};
853
+ return {...shift, [field]: value};
854
+ });
855
+
856
+ return {...day, shifts};
857
+ });
858
+ });
859
+ },
860
+ [],
861
+ );
862
+
863
+ const addHoursShift = useCallback(dayOfWeek => {
864
+ setHoursDraft(current => {
865
+ if (!Array.isArray(current)) return current;
866
+
867
+ return current.map(day => {
868
+ if (day.dayOfWeek !== dayOfWeek) return day;
869
+
870
+ return {
871
+ ...day,
872
+ open: true,
873
+ shifts: [...(day.shifts || []), {start: '09:00', duration: 180}],
874
+ };
875
+ });
876
+ });
877
+ }, []);
878
+
879
+ const removeHoursShift = useCallback((dayOfWeek, shiftIndex) => {
880
+ setHoursDraft(current => {
881
+ if (!Array.isArray(current)) return current;
882
+
883
+ return current.map(day => {
884
+ if (day.dayOfWeek !== dayOfWeek) return day;
885
+
886
+ const shifts = (day.shifts || []).filter(
887
+ (_, index) => index !== shiftIndex,
888
+ );
889
+ return {...day, shifts};
890
+ });
891
+ });
892
+ }, []);
893
+
894
+ const saveHours = useCallback(async () => {
895
+ if (!providerId || !hoursDraft) return;
896
+
897
+ setHoursSaving(true);
898
+ try {
899
+ const flatShifts = (hoursDraft || [])
900
+ .filter(day => day.open)
901
+ .flatMap(day =>
902
+ (day.shifts || []).map(shift => ({
903
+ dayOfWeek: day.dayOfWeek,
904
+ start: /^\d{2}:\d{2}$/.test(shift.start)
905
+ ? `${shift.start}:00`
906
+ : shift.start,
907
+ duration: Number(shift.duration) || 840,
908
+ })),
909
+ );
910
+
911
+ const response = await api.fetch(
912
+ '/marketplace/integrations/ifood/store/hours',
913
+ {
914
+ method: 'PUT',
915
+ body: JSON.stringify({provider_id: providerId, shifts: flatShifts}),
916
+ },
917
+ );
918
+
919
+ if (String(response?.result?.errno ?? response?.errno ?? '') === '0') {
920
+ const grouped = flatShifts.reduce(
921
+ (accumulator, {dayOfWeek, start, duration}) => {
922
+ let entry = accumulator.find(item => item.dayOfWeek === dayOfWeek);
923
+ if (!entry) {
924
+ entry = {dayOfWeek, shifts: []};
925
+ accumulator.push(entry);
926
+ }
927
+ entry.shifts.push({start: String(start).substring(0, 5), duration});
928
+ return accumulator;
929
+ },
930
+ [],
931
+ );
932
+
933
+ setHours(grouped);
934
+ setHoursEditing(false);
935
+ setHoursDraft(null);
936
+ showInfo('Horarios de funcionamento atualizados no iFood.');
937
+ } else {
938
+ showError(formatIFoodApiError(response?.result || response));
939
+ }
940
+ } catch (error) {
941
+ showError(formatIFoodApiError(error));
942
+ } finally {
943
+ setHoursSaving(false);
944
+ }
945
+ }, [hoursDraft, providerId, showError, showInfo]);
946
+
947
+ const handleOptionStatusToggle = useCallback(
948
+ async (product, option) => {
949
+ if (!providerId || !option?.ifood_option_id) return;
950
+
951
+ const optionId = option.ifood_option_id;
952
+ const nextStatus =
953
+ option.ifood_status === 'UNAVAILABLE' ? 'AVAILABLE' : 'UNAVAILABLE';
954
+ setOptStatusLoading(current => new Set([...current, optionId]));
955
+
956
+ try {
957
+ const response = await api.fetch(
958
+ '/marketplace/integrations/ifood/menu/option/status',
959
+ {
960
+ method: 'PATCH',
961
+ body: JSON.stringify({
962
+ provider_id: providerId,
963
+ option_id: optionId,
964
+ status: nextStatus,
965
+ }),
966
+ },
967
+ );
968
+
969
+ if (String(response?.result?.errno ?? '') === '0') {
970
+ setProductsResponse(current => {
971
+ if (!current?.products) return current;
972
+ return {
973
+ ...current,
974
+ products: current.products.map(item =>
975
+ item.id === product.id
976
+ ? {
977
+ ...item,
978
+ options: (item.options || []).map(currentOption =>
979
+ currentOption.ifood_option_id === optionId
980
+ ? {...currentOption, ifood_status: nextStatus}
981
+ : currentOption,
982
+ ),
983
+ }
984
+ : item,
985
+ ),
986
+ };
987
+ });
988
+ showInfo(
989
+ `Complemento ${nextStatus === 'AVAILABLE' ? 'disponivel' : 'indisponivel'} no iFood.`,
990
+ );
991
+ } else {
992
+ showError(formatIFoodApiError(response?.result || response));
993
+ }
994
+ } catch (error) {
995
+ showError(formatIFoodApiError(error));
996
+ } finally {
997
+ setOptStatusLoading(current => {
998
+ const next = new Set(current);
999
+ next.delete(optionId);
1000
+ return next;
1001
+ });
1002
+ }
1003
+ },
1004
+ [providerId, showError, showInfo],
1005
+ );
1006
+
1007
+ const handleOptionPriceSave = useCallback(
1008
+ async (product, option) => {
1009
+ if (!providerId || !option?.ifood_option_id) return;
1010
+
1011
+ const optionId = option.ifood_option_id;
1012
+ const raw = optPriceEditing[optionId];
1013
+ const price = parseFloat(String(raw ?? '').replace(',', '.'));
1014
+
1015
+ if (!Number.isFinite(price) || price <= 0) {
1016
+ showError('Informe um preco valido maior que zero.');
1017
+ return;
1018
+ }
1019
+
1020
+ setOptPriceLoading(current => new Set([...current, optionId]));
1021
+
1022
+ try {
1023
+ const response = await api.fetch(
1024
+ '/marketplace/integrations/ifood/menu/option/price',
1025
+ {
1026
+ method: 'PATCH',
1027
+ body: JSON.stringify({
1028
+ provider_id: providerId,
1029
+ option_id: optionId,
1030
+ price,
1031
+ }),
1032
+ },
1033
+ );
1034
+
1035
+ if (String(response?.result?.errno ?? '') === '0') {
1036
+ setProductsResponse(current => {
1037
+ if (!current?.products) return current;
1038
+ return {
1039
+ ...current,
1040
+ products: current.products.map(item =>
1041
+ item.id === product.id
1042
+ ? {
1043
+ ...item,
1044
+ options: (item.options || []).map(currentOption =>
1045
+ currentOption.ifood_option_id === optionId
1046
+ ? {...currentOption, price}
1047
+ : currentOption,
1048
+ ),
1049
+ }
1050
+ : item,
1051
+ ),
1052
+ };
1053
+ });
1054
+ setOptPriceEditing(current => {
1055
+ const next = {...current};
1056
+ delete next[optionId];
1057
+ return next;
1058
+ });
1059
+ showInfo('Preco do complemento atualizado no iFood.');
1060
+ } else {
1061
+ showError(formatIFoodApiError(response?.result || response));
1062
+ }
1063
+ } catch (error) {
1064
+ showError(formatIFoodApiError(error));
1065
+ } finally {
1066
+ setOptPriceLoading(current => {
1067
+ const next = new Set(current);
1068
+ next.delete(optionId);
1069
+ return next;
1070
+ });
1071
+ }
1072
+ },
1073
+ [optPriceEditing, providerId, showError, showInfo],
1074
+ );
1075
+
1076
+ if (!providerId) {
1077
+ return (
1078
+ <SafeAreaView style={styles.container} edges={['bottom']}>
1079
+ <View style={styles.centerState}>
1080
+ <Icon name="building" size={32} color="#94A3B8" />
1081
+ <Text style={styles.centerStateTitle}>
1082
+ Selecione uma empresa ativa
1083
+ </Text>
1084
+ </View>
1085
+ </SafeAreaView>
1086
+ );
1087
+ }
1088
+
1089
+ if (loading) {
1090
+ return (
1091
+ <SafeAreaView style={styles.container} edges={['bottom']}>
1092
+ <View style={styles.centerState}>
1093
+ <ActivityIndicator size="large" color={brandColors.primary} />
1094
+ <Text style={styles.centerStateTitle}>
1095
+ Carregando integracao iFood
1096
+ </Text>
1097
+ </View>
1098
+ </SafeAreaView>
1099
+ );
1100
+ }
1101
+
1102
+ const integration = detail?.integration || {};
1103
+ const stores = Array.isArray(detail?.stores?.items)
1104
+ ? detail.stores.items
1105
+ : [];
1106
+ const ifoodCode = String(
1107
+ integration?.ifood_code || integration?.merchant_id || '',
1108
+ );
1109
+ const selectedStore =
1110
+ detail?.selected_store ||
1111
+ stores.find(store => String(store?.merchant_id || '') === ifoodCode);
1112
+ const selectedStoreDetail = detail?.selected_store_detail || null;
1113
+ const connected = Boolean(integration?.connected);
1114
+ const authAvailable = Boolean(integration?.auth_available);
1115
+ const remoteConnected = Boolean(integration?.remote_connected);
1116
+ const statusTone = connected ? brandColors.success : brandColors.warning;
1117
+ const statusText = connected ? 'Conectada' : 'Pendente';
1118
+ const logo = getOrderChannelLogo({app: 'iFood'});
1119
+ const eligibleCount = productsResponse?.eligible_product_count || 0;
1120
+ const activeInterruptions = Array.isArray(storeStatus?.data?.interruptions)
1121
+ ? storeStatus.data.interruptions
1122
+ : [];
1123
+
1124
+ const tabCounts = {
1125
+ all: products.length,
1126
+ eligible: products.filter(product => product.eligible).length,
1127
+ selected: selectedIds.size,
1128
+ blocked: products.filter(product => !product.eligible).length,
1129
+ };
1130
+
1131
+ const sectionTabs = [
1132
+ {key: 'overview', label: 'Resumo'},
1133
+ {key: 'store', label: 'Loja', badge: stores.length},
1134
+ {key: 'operations', label: 'Operação'},
1135
+ {key: 'catalog', label: 'Cardápio', badge: selectedEligible.length},
1136
+ {key: 'remoteCatalog', label: 'Cardápio no iFood'},
1137
+ ];
1138
+
1139
+ return (
1140
+ <SafeAreaView
1141
+ style={[styles.container, {backgroundColor: brandColors.background}]}
1142
+ edges={['bottom']}>
1143
+ <ScrollView
1144
+ contentContainerStyle={styles.scroll}
1145
+ showsVerticalScrollIndicator={false}
1146
+ refreshControl={
1147
+ <RefreshControl
1148
+ refreshing={refreshing}
1149
+ onRefresh={onRefresh}
1150
+ tintColor={brandColors.primary}
1151
+ />
1152
+ }>
1153
+ <IntegrationHero
1154
+ shadowStyle={integrationCardShadowStyle}
1155
+ accentColor={brandColors.primary}
1156
+ eyebrow="Marketplace"
1157
+ title="Integração iFood"
1158
+ description="Gerencie vinculação da loja, operação diária e publicação do cardápio em blocos menores."
1159
+ logo={logo}
1160
+ iconName="shopping-bag"
1161
+ />
1162
+
1163
+ <IntegrationTabs
1164
+ tabs={sectionTabs}
1165
+ activeKey={activeTab}
1166
+ onChange={setActiveTab}
1167
+ accentColor={brandColors.primary}
1168
+ palette={brandColors}
1169
+ />
1170
+
1171
+ {activeTab === 'overview' && (
1172
+ <IFoodOverviewTab
1173
+ shadowStyle={integrationCardShadowStyle}
1174
+ integration={integration}
1175
+ connected={connected}
1176
+ remoteConnected={remoteConnected}
1177
+ authAvailable={authAvailable}
1178
+ statusTone={statusTone}
1179
+ statusText={statusText}
1180
+ eligibleCount={eligibleCount}
1181
+ selectedStoreDetail={selectedStoreDetail}
1182
+ />
1183
+ )}
1184
+
1185
+ {activeTab === 'store' && (
1186
+ <IFoodStoreTab
1187
+ shadowStyle={integrationCardShadowStyle}
1188
+ accentColor={brandColors.primary}
1189
+ palette={brandColors}
1190
+ stores={stores}
1191
+ merchantIdInput={merchantIdInput}
1192
+ setMerchantIdInput={setMerchantIdInput}
1193
+ ifoodCode={ifoodCode}
1194
+ selectedStore={selectedStore}
1195
+ connected={connected}
1196
+ actionLoading={actionLoading}
1197
+ onConnect={handleConnect}
1198
+ onDisconnect={handleDisconnect}
1199
+ onSync={handleSync}
1200
+ />
1201
+ )}
1202
+
1203
+ {activeTab === 'operations' && (
1204
+ <IFoodOperationsTab
1205
+ shadowStyle={integrationCardShadowStyle}
1206
+ palette={brandColors}
1207
+ connected={connected}
1208
+ storeStatus={storeStatus}
1209
+ activeInterruptions={activeInterruptions}
1210
+ actionLoading={actionLoading}
1211
+ interruptionDraft={interruptionDraft}
1212
+ interruptionRemoving={interruptionRemoving}
1213
+ onInterruptionDraftChange={handleInterruptionDraftChange}
1214
+ onRemoveInterruption={handleRemoveInterruption}
1215
+ onStoreOpen={handleStoreOpen}
1216
+ onStoreClose={handleStoreClose}
1217
+ onRefreshStatus={loadStoreStatus}
1218
+ hours={hours}
1219
+ hoursLoading={hoursLoading}
1220
+ hoursEditing={hoursEditing}
1221
+ onStartEditHours={startEditHours}
1222
+ hoursDraft={hoursDraft}
1223
+ onUpdateHoursDraft={updateHoursDraft}
1224
+ onAddHoursShift={addHoursShift}
1225
+ onRemoveHoursShift={removeHoursShift}
1226
+ hoursSaving={hoursSaving}
1227
+ onSaveHours={saveHours}
1228
+ onCancelEditHours={cancelEditHours}
1229
+ />
1230
+ )}
1231
+
1232
+ {activeTab === 'catalog' && (
1233
+ <IFoodCatalogTab
1234
+ shadowStyle={integrationCardShadowStyle}
1235
+ accentColor={brandColors.primary}
1236
+ palette={brandColors}
1237
+ connected={connected}
1238
+ minimumRequiredItems={minimumRequiredItems}
1239
+ eligibleCount={eligibleCount}
1240
+ search={search}
1241
+ setSearch={setSearch}
1242
+ filterKey={filterKey}
1243
+ setFilterKey={setFilterKey}
1244
+ tabCounts={tabCounts}
1245
+ selectedEligible={selectedEligible}
1246
+ onOpenPreview={handleOpenPreview}
1247
+ actionLoading={actionLoading}
1248
+ filteredProducts={filteredProducts}
1249
+ selectedIds={selectedIds}
1250
+ onProductCardPress={handleProductCardPress}
1251
+ onMarkCardPressHandled={markNextProductCardPressAsHandled}
1252
+ onBlockCardPress={blockNextProductCardPress}
1253
+ onToggleItemStatus={handleItemStatusToggle}
1254
+ onSaveItemPrice={handleItemPriceSave}
1255
+ itemStatusLoading={itemStatusLoading}
1256
+ itemPriceLoading={itemPriceLoading}
1257
+ itemPriceEditing={itemPriceEditing}
1258
+ setItemPriceEditing={setItemPriceEditing}
1259
+ optStatusLoading={optStatusLoading}
1260
+ optPriceLoading={optPriceLoading}
1261
+ optPriceEditing={optPriceEditing}
1262
+ setOptPriceEditing={setOptPriceEditing}
1263
+ onSaveOptionPrice={handleOptionPriceSave}
1264
+ onToggleOptionStatus={handleOptionStatusToggle}
1265
+ onSyncCatalog={handleCatalogSync}
1266
+ />
1267
+ )}
1268
+
1269
+ {activeTab === 'remoteCatalog' && (
1270
+ <RemoteMarketplaceMenuTab
1271
+ shadowStyle={integrationCardShadowStyle}
1272
+ accentColor={brandColors.primary}
1273
+ palette={brandColors}
1274
+ title="Cardapio no iFood"
1275
+ subtitle="Leitura direta do cardapio remoto da loja selecionada no iFood."
1276
+ buttonLabel="Ler cardapio do iFood"
1277
+ loading={actionLoading === 'remote-menu'}
1278
+ snapshot={remoteMenuSnapshot}
1279
+ errorMessage={remoteMenuError}
1280
+ onLoad={handleLoadRemoteMenu}
1281
+ />
1282
+ )}
1283
+ </ScrollView>
1284
+
1285
+ <IFoodPreviewModal
1286
+ visible={previewVisible}
1287
+ previewData={previewData}
1288
+ selectedEligible={selectedEligible}
1289
+ accentColor={brandColors.primary}
1290
+ palette={brandColors}
1291
+ uploading={actionLoading === 'menu_upload'}
1292
+ onClose={() => setPreviewVisible(false)}
1293
+ onUpload={() => handleMenuUpload(selectedEligible)}
1294
+ />
1295
+ </SafeAreaView>
1296
+ );
1297
+ }
1298
+ // 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.