@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,91 @@
1
+ // Filtros de catálogo exibidos na aba de produtos.
2
+ export const filterTabs = [
3
+ { key: 'all', label: 'Todos' },
4
+ { key: 'eligible', label: 'Elegiveis' },
5
+ { key: 'selected', label: 'Selecionados' },
6
+ { key: 'blocked', label: 'Com bloqueio' },
7
+ ];
8
+
9
+ // Ordem e rótulos fixos usados na edição dos horários de funcionamento.
10
+ export const DAY_LABELS = {
11
+ MONDAY: 'Segunda',
12
+ TUESDAY: 'Terça',
13
+ WEDNESDAY: 'Quarta',
14
+ THURSDAY: 'Quinta',
15
+ FRIDAY: 'Sexta',
16
+ SATURDAY: 'Sábado',
17
+ SUNDAY: 'Domingo',
18
+ };
19
+
20
+ export const DAY_ORDER = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'];
21
+
22
+ export const formatIFoodApiError = error => {
23
+ if (!error) return 'Nao foi possivel carregar os dados da integracao iFood.';
24
+ if (typeof error === 'string') return error;
25
+ return error?.message || error?.description || error?.errmsg || 'Nao foi possivel carregar os dados da integracao iFood.';
26
+ };
27
+
28
+ export const calcEndTime = (start, durationMin) => {
29
+ if (!start || durationMin == null || durationMin === '') return '--:--';
30
+ const [hours, minutes] = start.split(':').map(Number);
31
+ const totalMinutes = hours * 60 + minutes + Number(durationMin);
32
+ const endHours = Math.floor(totalMinutes / 60) % 24;
33
+ const endMinutes = totalMinutes % 60;
34
+ return `${String(endHours).padStart(2, '0')}:${String(endMinutes).padStart(2, '0')}`;
35
+ };
36
+
37
+ export const calcDuration = (start, end) => {
38
+ const [startHours, startMinutes] = (start || '00:00').split(':').map(Number);
39
+ const [endHours, endMinutes] = (end || '23:59').split(':').map(Number);
40
+ let duration = endHours * 60 + endMinutes - (startHours * 60 + startMinutes);
41
+ if (duration <= 0) duration += 1440;
42
+ return duration;
43
+ };
44
+
45
+ export const formatDateTimeLabel = value => {
46
+ if (!value) return '--';
47
+ const parsed = new Date(value);
48
+ if (Number.isNaN(parsed.getTime())) return String(value);
49
+
50
+ return parsed.toLocaleString('pt-BR', {
51
+ day: '2-digit',
52
+ month: '2-digit',
53
+ year: 'numeric',
54
+ hour: '2-digit',
55
+ minute: '2-digit',
56
+ });
57
+ };
58
+
59
+ const pad = value => String(value).padStart(2, '0');
60
+
61
+ export const toDateTimeLocalValue = value => {
62
+ const parsed = value ? new Date(value) : new Date();
63
+ const date = Number.isNaN(parsed.getTime()) ? new Date() : parsed;
64
+
65
+ return [
66
+ date.getFullYear(),
67
+ pad(date.getMonth() + 1),
68
+ pad(date.getDate()),
69
+ ].join('-') + `T${pad(date.getHours())}:${pad(date.getMinutes())}`;
70
+ };
71
+
72
+ export const buildDefaultInterruptionDraft = () => {
73
+ const start = new Date();
74
+ start.setMinutes(start.getMinutes() + 5);
75
+ const end = new Date(start.getTime() + (60 * 60 * 1000));
76
+
77
+ return {
78
+ description: 'Pausa operacional',
79
+ start: toDateTimeLocalValue(start),
80
+ end: toDateTimeLocalValue(end),
81
+ };
82
+ };
83
+
84
+ export const toIFoodInterruptionDateTime = value => {
85
+ const normalized = String(value || '').trim();
86
+ if (!normalized) return '';
87
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(normalized)) {
88
+ return `${normalized}:00`;
89
+ }
90
+ return normalized;
91
+ };
@@ -0,0 +1 @@
1
+ export { default } from './IFoodIntegrationPage/index';
@@ -0,0 +1 @@
1
+ export { default } from './IFoodIntegrationPage/styles';
@@ -28,12 +28,31 @@ const formatDate = (dateString) => {
28
28
  const d = new Date(dateString);
29
29
  return d.toLocaleString(); // Formato local legível
30
30
  };
31
-
32
- const Imports = ({ context = {}, onClose }) => {
33
- const peopleStore = useStore('people');
34
- const { getters: peopleGetters } = peopleStore.getters;
35
- const { currentCompany } = peopleGetters;
36
-
31
+
32
+ const Imports = ({ context = {}, onClose }) => {
33
+ const peopleStore = useStore('people');
34
+ const themeStore = useStore('theme');
35
+ const { getters: peopleGetters } = peopleStore.getters;
36
+ const { currentCompany } = peopleGetters;
37
+ const themeColors = themeStore?.getters?.colors || {};
38
+ const buttonPalette = useMemo(
39
+ () => ({
40
+ buttonBackground: themeColors.buttonBackground,
41
+ buttonBorder: themeColors.buttonBorder,
42
+ buttonText: themeColors.buttonText,
43
+ buttonIcon: themeColors.buttonIcon || themeColors.buttonText,
44
+ modalCloseIcon: themeColors.modalCloseIcon || themeColors.iconColor,
45
+ }),
46
+ [
47
+ themeColors.buttonBackground,
48
+ themeColors.buttonBorder,
49
+ themeColors.buttonIcon,
50
+ themeColors.buttonText,
51
+ themeColors.iconColor,
52
+ themeColors.modalCloseIcon,
53
+ ],
54
+ );
55
+
37
56
  const importType = context.context;
38
57
  const title = context.title;
39
58
  const searchPlaceholder = context.searchPlaceholder;
@@ -45,25 +64,25 @@ const Imports = ({ context = {}, onClose }) => {
45
64
  noStatusLabel,
46
65
  processingHelpLabel,
47
66
  } = resolveImportLabels(global.t?.t);
48
-
49
- const navigation = useNavigation();
50
-
51
- const importsStore = useStore('imports');
52
- const getters = importsStore.getters;
53
- const actions = importsStore.actions;
54
-
67
+
68
+ const navigation = useNavigation();
69
+
70
+ const importsStore = useStore('imports');
71
+ const getters = importsStore.getters;
72
+ const actions = importsStore.actions;
73
+
55
74
  const { items, isLoading } = getters;
56
75
 
57
76
  const [currentPage, setCurrentPage] = useState(1);
58
77
 
59
78
  const [searchText, setSearchText] = useState('');
60
79
  const [searchQuery, setSearchQuery] = useState('');
61
-
62
- const [allImports, setAllImports] = useState([]);
63
-
64
- const [refreshing, setRefreshing] = useState(false);
65
- const [showAddImportModal, setShowAddImportModal] = useState(false);
66
-
80
+
81
+ const [allImports, setAllImports] = useState([]);
82
+
83
+ const [refreshing, setRefreshing] = useState(false);
84
+ const [showAddImportModal, setShowAddImportModal] = useState(false);
85
+
67
86
  const fetchImports = useCallback(
68
87
  (query, page) => {
69
88
  const params = {
@@ -71,43 +90,43 @@ const Imports = ({ context = {}, onClose }) => {
71
90
  importType: importType,
72
91
  page: page ?? currentPage,
73
92
  };
74
-
75
- if (String(query ?? searchQuery).trim()) {
76
- params.name = String(query ?? searchQuery).trim();
77
- }
78
-
93
+
94
+ if (String(query ?? searchQuery).trim()) {
95
+ params.name = String(query ?? searchQuery).trim();
96
+ }
97
+
79
98
  return actions.getItems(params);
80
99
  },
81
100
  [actions, currentCompany, currentPage, searchQuery, importType],
82
101
  );
83
-
84
- useLayoutEffect(() => {
85
- navigation.setOptions({
86
- headerTitle: title,
87
- });
88
- }, [navigation, title]);
89
-
102
+
103
+ useLayoutEffect(() => {
104
+ navigation.setOptions({
105
+ headerTitle: title,
106
+ });
107
+ }, [navigation, title]);
108
+
90
109
  useFocusEffect(
91
110
  useCallback(() => {
92
111
  fetchImports(searchQuery, currentPage);
93
112
  }, [currentPage, fetchImports, searchQuery]),
94
113
  );
95
-
96
- useEffect(() => {
97
- if (isLoading) return;
98
- if (items && Array.isArray(items)) {
99
- if (currentPage === 1) {
100
- setAllImports(items);
101
- } else {
102
- setAllImports((prev) => {
103
- const newIds = new Set(items.map((i) => i.id));
104
- const filteredPrev = prev.filter((p) => !newIds.has(p.id));
105
- return [...filteredPrev, ...items];
106
- });
107
- }
108
- }
109
- }, [items, currentPage, isLoading]);
110
-
114
+
115
+ useEffect(() => {
116
+ if (isLoading) return;
117
+ if (items && Array.isArray(items)) {
118
+ if (currentPage === 1) {
119
+ setAllImports(items);
120
+ } else {
121
+ setAllImports((prev) => {
122
+ const newIds = new Set(items.map((i) => i.id));
123
+ const filteredPrev = prev.filter((p) => !newIds.has(p.id));
124
+ return [...filteredPrev, ...items];
125
+ });
126
+ }
127
+ }
128
+ }, [items, currentPage, isLoading]);
129
+
111
130
  useEffect(() => {
112
131
  const t = setTimeout(() => {
113
132
  setSearchQuery(searchText.trim());
@@ -167,21 +186,21 @@ const Imports = ({ context = {}, onClose }) => {
167
186
  <Text style={styles.avatarText}>
168
187
  {item.name?.charAt(0)?.toUpperCase() || 'I'}
169
188
  </Text>
170
- </View>
171
-
172
- <Text style={styles.title} numberOfLines={1}>
173
- {item.name} (ID: {item.id})
174
- </Text>
175
-
176
- <Icon name="chevron-right" size={14} color="#CBD5E1" />
177
- </View>
178
-
179
- <View style={styles.cardBody}>
180
- <View style={styles.infoRow}>
181
- <Icon name="file-text-o" size={16} color={colors.primary} />
182
- <Text style={styles.infoText}>{item.fileFormat?.toUpperCase()}</Text>
183
- </View>
184
-
189
+ </View>
190
+
191
+ <Text style={styles.title} numberOfLines={1}>
192
+ {item.name} (ID: {item.id})
193
+ </Text>
194
+
195
+ <Icon name="chevron-right" size={14} color="#CBD5E1" />
196
+ </View>
197
+
198
+ <View style={styles.cardBody}>
199
+ <View style={styles.infoRow}>
200
+ <Icon name="file-text-o" size={16} color={colors.primary} />
201
+ <Text style={styles.infoText}>{item.fileFormat?.toUpperCase()}</Text>
202
+ </View>
203
+
185
204
  <View style={styles.infoRow}>
186
205
  <Icon name="calendar" size={16} color={colors.primary} />
187
206
  <Text style={styles.infoText}>{formatDate(item.uploadDate)}</Text>
@@ -226,74 +245,96 @@ const Imports = ({ context = {}, onClose }) => {
226
245
  </TouchableOpacity>
227
246
  );
228
247
  };
229
-
230
- const downloadTemplate = async () => {
231
- try {
232
- const response = await api.fetch(`/imports/example/${importType}`, {
233
- method: 'GET',
234
- responseType: 'text',
235
- });
236
-
237
- const csvText = typeof response === 'string' ? response : await response.text?.();
238
- console.log('CSV recebido:', csvText.slice(0, 200));
239
-
240
- if (!csvText) throw new Error('CSV vazio');
241
-
242
- // mobile: verifica se o diretório está disponível
243
- const dirUri = Directory?.cacheDocumentDirectory || Directory?.documentDirectory;
244
-
245
- if (dirUri) {
246
- const fileUri = `${dirUri}modelo_${importType}.csv`;
247
-
248
- const file = new File(fileUri, { contents: csvText });
249
- await file.create();
250
-
251
- if (await Sharing.isAvailableAsync()) {
252
- await Sharing.shareAsync(fileUri);
253
- }
254
-
255
- console.log(`Arquivo CSV criado em: ${fileUri}`);
256
- return fileUri;
257
- } else if (typeof window !== 'undefined') {
258
- // fallback web
259
- const blob = new Blob([csvText], { type: 'text/csv' });
260
- const link = document.createElement('a');
261
- link.href = URL.createObjectURL(blob);
262
- link.download = `modelo_${importType}.csv`;
263
- link.click();
264
- URL.revokeObjectURL(link.href);
265
- console.log('Download CSV disparado no web');
266
- return 'web-download';
267
- } else {
268
- throw new Error('FileSystem não disponível');
269
- }
270
- } catch (err) {
271
- console.error('Erro detalhado ao baixar CSV:', err);
272
- throw new Error('Falha ao baixar o modelo CSV');
273
- }
274
- };
275
-
276
- return (
277
- <View style={styles.container}>
278
-
248
+
249
+ const downloadTemplate = async () => {
250
+ try {
251
+ const response = await api.fetch(`/imports/example/${importType}`, {
252
+ method: 'GET',
253
+ responseType: 'text',
254
+ });
255
+
256
+ const csvText = typeof response === 'string' ? response : await response.text?.();
257
+ console.log('CSV recebido:', csvText.slice(0, 200));
258
+
259
+ if (!csvText) throw new Error('CSV vazio');
260
+
261
+ // mobile: verifica se o diretório está disponível
262
+ const dirUri = Directory?.cacheDocumentDirectory || Directory?.documentDirectory;
263
+
264
+ if (dirUri) {
265
+ const fileUri = `${dirUri}modelo_${importType}.csv`;
266
+
267
+ const file = new File(fileUri, { contents: csvText });
268
+ await file.create();
269
+
270
+ if (await Sharing.isAvailableAsync()) {
271
+ await Sharing.shareAsync(fileUri);
272
+ }
273
+
274
+ console.log(`Arquivo CSV criado em: ${fileUri}`);
275
+ return fileUri;
276
+ } else if (typeof window !== 'undefined') {
277
+ // fallback web
278
+ const blob = new Blob([csvText], { type: 'text/csv' });
279
+ const link = document.createElement('a');
280
+ link.href = URL.createObjectURL(blob);
281
+ link.download = `modelo_${importType}.csv`;
282
+ link.click();
283
+ URL.revokeObjectURL(link.href);
284
+ console.log('Download CSV disparado no web');
285
+ return 'web-download';
286
+ } else {
287
+ throw new Error('FileSystem não disponível');
288
+ }
289
+ } catch (err) {
290
+ console.error('Erro detalhado ao baixar CSV:', err);
291
+ throw new Error('Falha ao baixar o modelo CSV');
292
+ }
293
+ };
294
+
295
+ return (
296
+ <View style={styles.container}>
297
+
279
298
  <View style={styles.headerBar}>
280
299
  <Text style={styles.headerTitle}>{title}</Text>
281
300
  <TouchableOpacity onPress={onClose}>
282
- <Icon name="close" size={24} color="#000" />
301
+ <Icon name="close" size={24} color={buttonPalette.modalCloseIcon} />
283
302
  </TouchableOpacity>
284
- </View>
285
-
286
- <View style={styles.subHeader}>
303
+ </View>
304
+
305
+ <View style={styles.subHeader}>
287
306
  <View style={styles.topRow}>
288
307
  <View style={styles.topRowActions}>
289
308
  <TouchableOpacity style={styles.downloadButton} onPress={downloadTemplate}>
290
- <IconAdd name="download" size={20} color="#fff" />
291
- <Text style={styles.downloadText}>Baixar Modelo</Text>
309
+ <View
310
+ style={{
311
+ flexDirection: 'row',
312
+ alignItems: 'center',
313
+ backgroundColor: buttonPalette.buttonBackground,
314
+ borderColor: buttonPalette.buttonBorder,
315
+ borderWidth: 1,
316
+ borderRadius: 8,
317
+ paddingHorizontal: 12,
318
+ paddingVertical: 6,
319
+ }}
320
+ >
321
+ <IconAdd name="download" size={20} color={buttonPalette.buttonIcon} />
322
+ <Text style={[styles.downloadText, { color: buttonPalette.buttonText }]}>Baixar Modelo</Text>
323
+ </View>
292
324
  </TouchableOpacity>
293
325
 
294
- <TouchableOpacity style={styles.refreshButton} onPress={onRefresh}>
295
- <Icon name="refresh" size={16} color={colors.primary} />
296
- <Text style={styles.refreshButtonText}>{refreshLabel}</Text>
326
+ <TouchableOpacity
327
+ style={[
328
+ styles.refreshButton,
329
+ {
330
+ backgroundColor: buttonPalette.buttonBackground,
331
+ borderColor: buttonPalette.buttonBorder,
332
+ },
333
+ ]}
334
+ onPress={onRefresh}
335
+ >
336
+ <Icon name="refresh" size={16} color={buttonPalette.buttonIcon} />
337
+ <Text style={[styles.refreshButtonText, { color: buttonPalette.buttonText }]}>{refreshLabel}</Text>
297
338
  </TouchableOpacity>
298
339
  </View>
299
340
 
@@ -304,50 +345,57 @@ const Imports = ({ context = {}, onClose }) => {
304
345
  </View>
305
346
  ) : null}
306
347
  </View>
307
-
308
- <View style={styles.searchRow}>
309
- <View style={styles.searchInputContainer}>
310
- <Icon name="search" size={16} color="#94A3B8" />
311
-
312
- <TextInput
313
- style={styles.searchInput}
314
- placeholder={searchPlaceholder}
315
- placeholderTextColor="#94A3B8"
316
- value={searchText}
317
- onChangeText={setSearchText}
318
- />
319
-
320
- {searchText.length > 0 && (
321
- <TouchableOpacity
322
- onPress={() => setSearchText('')}
323
- style={styles.clearSearchButton}
324
- >
325
- <Icon name="times-circle" size={16} color="#94A3B8" />
326
- </TouchableOpacity>
327
- )}
328
- </View>
329
-
330
- <TouchableOpacity
331
- style={styles.addButton}
332
- onPress={() => setShowAddImportModal(true)}
333
- >
334
- <IconAdd name="upload-file" size={24} color="#FFFFFF" />
335
- </TouchableOpacity>
336
- </View>
337
- </View>
338
-
339
- <FlatList
340
- data={allImports}
341
- keyExtractor={(item) => String(item.id)}
342
- renderItem={renderImportCard}
343
- contentContainerStyle={styles.scrollContent}
344
- refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
345
- />
346
-
347
- <AddImportModal
348
- visible={showAddImportModal}
349
- onClose={() => setShowAddImportModal(false)}
350
- context={context}
348
+
349
+ <View style={styles.searchRow}>
350
+ <View style={styles.searchInputContainer}>
351
+ <Icon name="search" size={16} color="#94A3B8" />
352
+
353
+ <TextInput
354
+ style={styles.searchInput}
355
+ placeholder={searchPlaceholder}
356
+ placeholderTextColor="#94A3B8"
357
+ value={searchText}
358
+ onChangeText={setSearchText}
359
+ />
360
+
361
+ {searchText.length > 0 && (
362
+ <TouchableOpacity
363
+ onPress={() => setSearchText('')}
364
+ style={styles.clearSearchButton}
365
+ >
366
+ <Icon name="times-circle" size={16} color="#94A3B8" />
367
+ </TouchableOpacity>
368
+ )}
369
+ </View>
370
+
371
+ <TouchableOpacity
372
+ style={[
373
+ styles.addButton,
374
+ {
375
+ backgroundColor: buttonPalette.buttonBackground,
376
+ borderColor: buttonPalette.buttonBorder,
377
+ borderWidth: 1,
378
+ },
379
+ ]}
380
+ onPress={() => setShowAddImportModal(true)}
381
+ >
382
+ <IconAdd name="upload-file" size={24} color={buttonPalette.buttonIcon} />
383
+ </TouchableOpacity>
384
+ </View>
385
+ </View>
386
+
387
+ <FlatList
388
+ data={allImports}
389
+ keyExtractor={(item) => String(item.id)}
390
+ renderItem={renderImportCard}
391
+ contentContainerStyle={styles.scrollContent}
392
+ refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
393
+ />
394
+
395
+ <AddImportModal
396
+ visible={showAddImportModal}
397
+ onClose={() => setShowAddImportModal(false)}
398
+ context={context}
351
399
  onSuccess={() => {
352
400
  setCurrentPage(1);
353
401
  fetchImports(searchQuery, 1);
@@ -356,6 +404,6 @@ const Imports = ({ context = {}, onClose }) => {
356
404
  </View>
357
405
  );
358
406
  };
359
-
407
+
360
408
  export default Imports;
361
409
  // 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.