@controleonline/ui-common 1.2.70 → 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 (188) 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 +32 -28
  11. package/src/api/fetch.js +1 -1
  12. package/src/api/index.js +328 -58
  13. package/src/react/components/AddImportModal.js +95 -111
  14. package/src/react/components/AnimatedModal.js +171 -0
  15. package/src/react/components/AnimatedModal.styles.js +21 -0
  16. package/src/react/components/AppTypeSwitcher.js +164 -0
  17. package/src/react/components/AppTypeSwitcher.styles.js +109 -0
  18. package/src/react/components/BackgroundRuntimeBridge.js +5 -4
  19. package/src/react/components/BottomNavigationBar.config.js +211 -231
  20. package/src/react/components/BottomNavigationBar.js +86 -31
  21. package/src/react/components/BottomNavigationBar.styles.js +118 -110
  22. package/src/react/components/CategoryForm.js +52 -35
  23. package/src/react/components/DefaultProvider.native.js +194 -156
  24. package/src/react/components/DefaultProvider.web.js +188 -112
  25. package/src/react/components/DeliveryPushBridge.native.js +2 -2
  26. package/src/react/components/DeviceAlertSoundService.js +3 -3
  27. package/src/react/components/KioskModeBridge.js +2 -2
  28. package/src/react/components/LauncherModeBridge.js +2 -2
  29. package/src/react/components/ManagerPushBridge.native.js +2 -2
  30. package/src/react/components/NetworkPrinterPreviewModal.js +249 -0
  31. package/src/react/components/NetworkPrinterPreviewModal.styles.js +159 -0
  32. package/src/react/components/RemoteCheckoutService.js +28 -20
  33. package/src/react/components/RuntimeBottomNavigationBar.js +146 -119
  34. package/src/react/components/RuntimeInfoFooter.js +137 -45
  35. package/src/react/components/RuntimeInfoFooter.styles.js +3 -0
  36. package/src/react/components/StateStore.js +258 -0
  37. package/src/react/components/UnifiedPaymentBar.js +7 -4
  38. package/src/react/components/UserAvatar.js +160 -0
  39. package/src/react/components/WebsocketListener.native.js +11 -11
  40. package/src/react/components/integrations/IntegrationHero/index.js +48 -0
  41. package/src/react/components/integrations/IntegrationHero/styles.js +57 -0
  42. package/src/react/components/integrations/IntegrationTabs/index.js +89 -0
  43. package/src/react/components/integrations/IntegrationTabs/styles.js +44 -0
  44. package/src/react/config/deviceConfigBootstrap.js +71 -30
  45. package/src/react/css/orders.js +72 -0
  46. package/src/react/pages/Connections.js +229 -0
  47. package/src/react/pages/Connections.styles.js +222 -0
  48. package/src/react/pages/DeviceDetailPage.js +2814 -0
  49. package/src/react/pages/DeviceDetailPage.styles.js +308 -0
  50. package/src/react/pages/Devices/currentDevice.js +194 -0
  51. package/src/react/pages/Devices/deviceTypes/DeviceGroupCard.js +257 -0
  52. package/src/react/pages/Devices/deviceTypes/all.js +23 -0
  53. package/src/react/pages/Devices/deviceTypes/device.js +25 -0
  54. package/src/react/pages/Devices/deviceTypes/deviceListHelpers.js +178 -0
  55. package/src/react/pages/Devices/deviceTypes/display.js +26 -0
  56. package/src/react/pages/Devices/deviceTypes/index.js +145 -0
  57. package/src/react/pages/Devices/deviceTypes/ipCamera.js +35 -0
  58. package/src/react/pages/Devices/deviceTypes/pdv.js +88 -0
  59. package/src/react/pages/Devices/deviceTypes/printer.js +37 -0
  60. package/src/react/pages/Devices/deviceTypes/shared.js +453 -0
  61. package/src/react/pages/Devices/deviceTypes/useDeviceNetworkConnectivity.js +145 -0
  62. package/src/react/pages/Devices.js +183 -0
  63. package/src/react/pages/Devices.styles.js +353 -0
  64. package/src/react/pages/Food99IntegrationPage/components/Food99CatalogTab.js +219 -0
  65. package/src/react/pages/Food99IntegrationPage/components/Food99OverviewTab.js +134 -0
  66. package/src/react/pages/Food99IntegrationPage/components/Food99PreviewModal.js +173 -0
  67. package/src/react/pages/Food99IntegrationPage/components/Food99ProductCard.js +1 -0
  68. package/src/react/pages/Food99IntegrationPage/components/Food99QuickWalletModal.js +181 -0
  69. package/src/react/pages/Food99IntegrationPage/components/Food99SettingsTab.js +306 -0
  70. package/src/react/pages/Food99IntegrationPage/components/Food99StoreTab.js +417 -0
  71. package/src/react/pages/Food99IntegrationPage/index.js +1530 -0
  72. package/src/react/pages/Food99IntegrationPage/styles.js +792 -0
  73. package/src/react/pages/Food99IntegrationPage/utils.js +155 -0
  74. package/src/react/pages/Food99IntegrationPage.js +1 -0
  75. package/src/react/pages/Food99IntegrationPage.styles.js +1 -0
  76. package/src/react/pages/GenericLogPage.js +42 -167
  77. package/src/react/pages/IFoodIntegrationPage/components/IFoodCatalogTab.js +8 -0
  78. package/src/react/pages/IFoodIntegrationPage/components/IFoodOperationsTab.js +420 -0
  79. package/src/react/pages/IFoodIntegrationPage/components/IFoodOverviewTab.js +118 -0
  80. package/src/react/pages/IFoodIntegrationPage/components/IFoodPreviewModal.js +103 -0
  81. package/src/react/pages/IFoodIntegrationPage/components/IFoodProductCard.js +1 -0
  82. package/src/react/pages/IFoodIntegrationPage/components/IFoodStoreTab.js +150 -0
  83. package/src/react/pages/IFoodIntegrationPage/index.js +1298 -0
  84. package/src/react/pages/IFoodIntegrationPage/styles.js +764 -0
  85. package/src/react/pages/IFoodIntegrationPage/utils.js +91 -0
  86. package/src/react/pages/IFoodIntegrationPage.js +1 -0
  87. package/src/react/pages/IFoodIntegrationPage.styles.js +1 -0
  88. package/src/react/pages/Imports.js +214 -166
  89. package/src/react/pages/IntegrationConfigPage.js +744 -0
  90. package/src/react/pages/IntegrationConfigPage.styles.js +269 -0
  91. package/src/react/pages/Integrations.js +299 -0
  92. package/src/react/pages/Integrations.styles.js +130 -0
  93. package/src/react/pages/ManagerOrderNotificationsPage.js +642 -0
  94. package/src/react/pages/MarketplaceIntegrationPage/components/MarketplaceProductCard.js +205 -0
  95. package/src/react/pages/MarketplaceIntegrationPage/components/RemoteMarketplaceMenuTab.js +335 -0
  96. package/src/react/pages/MarketplaceIntegrationPage/index.js +60 -0
  97. package/src/react/pages/MarketplaceIntegrationPage/utils.js +137 -0
  98. package/src/react/pages/MercadoLivreIntegrationPage.js +538 -0
  99. package/src/react/pages/PrinterDeviceDetailPage.js +1916 -0
  100. package/src/react/pages/PrinterDeviceDetailPage.styles.js +223 -0
  101. package/src/react/pages/PrinterDeviceFormPage.js +521 -0
  102. package/src/react/pages/PrinterDeviceFormPage.styles.js +159 -0
  103. package/src/react/pages/SettingsPage/PaymentTypesByWalletTab.js +484 -454
  104. package/src/react/pages/SettingsPage/index.js +1266 -1167
  105. package/src/react/pages/WhatsAppConnectionPage.js +459 -0
  106. package/src/react/pages/WhatsAppConnectionPage.styles.js +284 -0
  107. package/src/react/pages/integrationsCatalog.js +399 -0
  108. package/src/react/print/providers/local.js +1 -1
  109. package/src/react/router/publicRoutes.js +25 -0
  110. package/src/react/services/Cielo/Checkout.js +39 -0
  111. package/src/react/services/Cielo/Cielo.js +193 -0
  112. package/src/react/services/Cielo/Print.js +7 -0
  113. package/src/react/services/InfinitePay/Checkout.js +33 -0
  114. package/src/react/services/InfinitePay/InfinitePay.js +24 -0
  115. package/src/react/{utils → services}/paymentGatewayExecution.js +91 -91
  116. package/src/react/styles/global.js +115 -0
  117. package/src/react/utils/categoryContexts.js +46 -0
  118. package/src/react/utils/entityDisplay.js +222 -217
  119. package/src/react/utils/fileUrl.js +201 -16
  120. package/src/react/utils/integrationPage.js +16 -0
  121. package/src/react/utils/menuNavigation.js +31 -0
  122. package/src/react/utils/orderIdentity.js +277 -0
  123. package/src/react/utils/remotePayment.js +115 -61
  124. package/src/react/utils/runtimeFooter.js +40 -22
  125. package/src/react/utils/runtimeLanguage.js +50 -9
  126. package/src/react/utils/runtimeMenu.js +190 -98
  127. package/src/react/utils/runtimeZoom.js +43 -0
  128. package/src/react/utils/shopConfig.js +72 -34
  129. package/src/react/utils/shopFranchises.js +182 -23
  130. package/src/react/utils/socketRuntimePipeline.js +14 -14
  131. package/src/react/utils/storeColumns.js +23 -4
  132. package/src/react/utils/userAvatar.js +104 -0
  133. package/src/store/categories/index.js +48 -20
  134. package/src/store/configs/index.js +2 -2
  135. package/src/store/device_config/index.js +62 -21
  136. package/src/store/entity_log/index.js +22 -1
  137. package/src/store/imports/customActions.js +51 -0
  138. package/src/store/imports/index.js +89 -88
  139. package/src/store/status/index.js +11 -11
  140. package/src/store/themes/index.js +22 -0
  141. package/src/store/timezones/index.js +3 -1
  142. package/src/tests/browser/manager/connections.spec.js +143 -0
  143. package/src/tests/browser/manager/devices-current.spec.js +343 -0
  144. package/src/tests/browser/manager/mercado-livre-integration.spec.js +268 -0
  145. package/src/tests/react/api/deviceHeaders.test.js +114 -0
  146. package/src/tests/react/api/loadSmokeIndex.test.js +75 -0
  147. package/src/tests/react/components/AddImportModal.test.js +166 -0
  148. package/src/tests/react/components/BottomNavigationBar.config.test.js +20 -0
  149. package/src/tests/react/components/UserAvatar.test.js +52 -0
  150. package/src/tests/react/config/deviceConfigBootstrap.test.js +47 -0
  151. package/src/tests/react/pages/DevicePickerStyles.test.js +47 -0
  152. package/src/tests/react/pages/Devices/deviceListHelpers.test.js +39 -0
  153. package/src/tests/react/pages/DevicesCurrentDevice.test.js +198 -0
  154. package/src/tests/react/pages/connectionsHelpers.test.js +34 -0
  155. package/src/tests/react/pages/mercadoLivreIntegrationPageContract.test.mjs +16 -0
  156. package/src/tests/react/print/localPrintProvider.test.js +1 -1
  157. package/src/tests/react/services/Cielo.test.js +190 -0
  158. package/src/tests/react/store/categoriesOrdering.test.js +26 -0
  159. package/src/tests/react/utils/appDomain.test.js +62 -0
  160. package/src/tests/react/utils/entityDisplay.test.js +2 -2
  161. package/src/tests/react/utils/fileUrl.test.js +62 -0
  162. package/src/tests/react/utils/formatter.test.mjs +33 -0
  163. package/src/tests/react/utils/importStatus.test.js +2 -2
  164. package/src/tests/react/utils/orderIdentity.test.js +139 -0
  165. package/src/tests/react/utils/remotePayment.test.js +62 -0
  166. package/src/tests/react/utils/runtimeFooter.test.js +32 -9
  167. package/src/tests/react/utils/runtimeLanguage.test.js +80 -10
  168. package/src/tests/react/utils/runtimeMenu.test.js +195 -111
  169. package/src/tests/react/utils/runtimeZoom.test.js +38 -0
  170. package/src/tests/react/utils/shopConfig.test.js +75 -0
  171. package/src/tests/react/utils/shopFranchises.test.js +122 -12
  172. package/src/tests/react/utils/translate.discovery.test.mjs +170 -0
  173. package/src/tests/react/utils/translate.test.helpers.mjs +152 -0
  174. package/src/tests/react/utils/translate.test.mjs +252 -396
  175. package/src/tests/react/utils/userAvatar.test.js +63 -0
  176. package/src/utils/appDomain.js +15 -4
  177. package/src/utils/formatter.js +122 -16
  178. package/src/utils/integrationConfigs.js +1 -0
  179. package/src/utils/translate.js +277 -398
  180. package/src/utils/translateDiscovery.js +96 -0
  181. package/src/utils/translateNormalize.js +33 -0
  182. package/src/utils/translatePending.js +92 -0
  183. package/src/utils/translateStorage.js +142 -0
  184. package/.github/agents/developer.agent.md +0 -30
  185. package/.github/agents/devops.agent.md +0 -30
  186. package/.github/agents/qa.agent.md +0 -30
  187. package/.github/agents/security.agent.md +0 -30
  188. package/.github/workflows/npmjs.yml +0 -35
package/src/api/index.js CHANGED
@@ -1,11 +1,217 @@
1
1
  import myFetch from '@controleonline/ui-common/src/api/fetch';
2
2
  import axios from 'axios';
3
- import { APP_ENV } from '../../../../../config/env';
3
+ import {env as APP_ENV} from '@env';
4
4
  import { resolveAppDomain } from '@controleonline/ui-common/src/utils/appDomain';
5
5
  import { resolveApiEntryPoint } from '@controleonline/ui-common/src/utils/apiEntryPoint';
6
-
7
- const MIME_TYPE = 'application/ld+json';
8
-
6
+
7
+ const MIME_TYPE = 'application/ld+json';
8
+ const DOMAIN_QUERY_PARAM_KEYS = new Set(['app-domain', 'appdomain']);
9
+
10
+ function stripDomainQueryParams(uri) {
11
+ const value = String(uri || '');
12
+ const hashIndex = value.indexOf('#');
13
+ const withoutHash = hashIndex >= 0 ? value.slice(0, hashIndex) : value;
14
+ const hash = hashIndex >= 0 ? value.slice(hashIndex) : '';
15
+ const queryIndex = withoutHash.indexOf('?');
16
+
17
+ if (queryIndex < 0) {
18
+ return value;
19
+ }
20
+
21
+ const path = withoutHash.slice(0, queryIndex);
22
+ const params = new URLSearchParams(withoutHash.slice(queryIndex + 1));
23
+
24
+ for (const key of [...params.keys()]) {
25
+ if (DOMAIN_QUERY_PARAM_KEYS.has(String(key || '').trim().toLowerCase())) {
26
+ params.delete(key);
27
+ }
28
+ }
29
+
30
+ const query = params.toString();
31
+ return `${path}${query ? `?${query}` : ''}${hash}`;
32
+ }
33
+
34
+ function encodeBasicAuth(user, password) {
35
+ const credentials = `${String(user || '').trim()}:${String(password || '').trim()}`;
36
+
37
+ if (credentials === ':') {
38
+ return '';
39
+ }
40
+
41
+ if (typeof btoa === 'function') {
42
+ return btoa(credentials);
43
+ }
44
+
45
+ if (typeof Buffer !== 'undefined') {
46
+ return Buffer.from(credentials, 'utf8').toString('base64');
47
+ }
48
+
49
+ return '';
50
+ }
51
+
52
+ function resolveApiRuntimeConfig(config = {}) {
53
+ return {
54
+ apiBaseUrl: String(config.apiBaseUrl || APP_ENV.API_ENTRYPOINT || '').trim(),
55
+ domain: String(config.domain || APP_ENV.DOMAIN || '').trim(),
56
+ htaccessUser: String(config.htaccessUser || APP_ENV.HTACCESS_USER || '').trim(),
57
+ htaccessPassword: String(config.htaccessPassword || APP_ENV.HTACCESS_PASSWORD || '').trim(),
58
+ };
59
+ }
60
+
61
+ function buildApiUrl(apiBaseUrl, path) {
62
+ const apiEntryPoint = resolveApiEntryPoint(apiBaseUrl);
63
+ const entryPoint = apiEntryPoint + (apiEntryPoint.endsWith('/') ? '' : '/');
64
+ const rawPath = String(path || '').startsWith('/')
65
+ ? String(path || '').substring(1)
66
+ : String(path || '');
67
+ const normalizedPath = stripDomainQueryParams(rawPath);
68
+
69
+ return new URL(normalizedPath, entryPoint).href;
70
+ }
71
+
72
+ function resolveRequestDevice(masterDevice, device, bodyDeviceId) {
73
+ if (masterDevice?.id) {
74
+ return masterDevice;
75
+ }
76
+
77
+ if (device?.id) {
78
+ return device;
79
+ }
80
+
81
+ return bodyDeviceId ? {id: bodyDeviceId} : null;
82
+ }
83
+
84
+ async function readSmokeResponseBody(response, responseType) {
85
+ if (responseType === 'blob') {
86
+ return await response.blob();
87
+ }
88
+
89
+ if (responseType === 'text') {
90
+ return await response.text();
91
+ }
92
+
93
+ const text = await response.text();
94
+
95
+ if (!text) {
96
+ return null;
97
+ }
98
+
99
+ try {
100
+ return JSON.parse(text);
101
+ } catch (error) {
102
+ return text;
103
+ }
104
+ }
105
+
106
+ function buildSmokeHttpError(response, body) {
107
+ const message =
108
+ String(body?.message || body?.error || body || response.statusText || 'Request failed')
109
+ .trim() || 'Request failed';
110
+
111
+ const error = new Error(message);
112
+ error.code = response.status;
113
+ error.status = response.status;
114
+ error.body = body;
115
+ return error;
116
+ }
117
+
118
+ async function requestSmoke(path, options = {}, config = {}) {
119
+ const runtimeConfig = resolveApiRuntimeConfig(config);
120
+ const url = buildApiUrl(runtimeConfig.apiBaseUrl, path);
121
+ const headers = new Headers(options.headers || undefined);
122
+ const accept =
123
+ options.responseType === 'blob' || options.responseType === 'text'
124
+ ? '*/*'
125
+ : MIME_TYPE;
126
+
127
+ if (accept !== '' && !headers.has('Accept')) {
128
+ headers.set('Accept', accept);
129
+ }
130
+
131
+ if (options.responseType !== 'blob' && !headers.has('Content-Type')) {
132
+ headers.set('Content-Type', MIME_TYPE);
133
+ }
134
+
135
+ const sessionToken = await api.getToken();
136
+
137
+ if (sessionToken && !headers.has('API-TOKEN')) {
138
+ headers.set('API-TOKEN', sessionToken);
139
+ }
140
+
141
+ const basicAuth = encodeBasicAuth(runtimeConfig.htaccessUser, runtimeConfig.htaccessPassword);
142
+ if (basicAuth !== '' && !headers.has('Authorization')) {
143
+ headers.set('Authorization', `Basic ${basicAuth}`);
144
+ }
145
+
146
+ const appDomain = resolveAppDomain(runtimeConfig.domain);
147
+ if (appDomain !== '' && !headers.has('App-Domain')) {
148
+ headers.set('App-Domain', appDomain);
149
+ }
150
+
151
+ let body = options.body;
152
+ const hasBody =
153
+ body !== undefined &&
154
+ body !== null &&
155
+ options.method !== 'GET' &&
156
+ options.method !== 'HEAD';
157
+
158
+ if (hasBody && typeof body !== 'string' && !(body instanceof FormData) && !(body instanceof Blob)) {
159
+ body = JSON.stringify(body);
160
+ }
161
+
162
+ if (options.params) {
163
+ const params = api.serialize(options.params);
164
+ if (params.length > 0) {
165
+ const urlObject = new URL(url);
166
+ urlObject.search = params.join('&');
167
+ return requestSmokeUrl(urlObject.toString(), {
168
+ ...options,
169
+ body,
170
+ headers,
171
+ });
172
+ }
173
+ }
174
+
175
+ return requestSmokeUrl(url, {
176
+ ...options,
177
+ body,
178
+ headers,
179
+ });
180
+ }
181
+
182
+ async function requestSmokeUrl(url, options) {
183
+ const response = await fetch(url, options);
184
+
185
+ if (options.responseType === 'blob') {
186
+ if (!response.ok) {
187
+ throw buildSmokeHttpError(response, await response.text().catch(() => ''));
188
+ }
189
+
190
+ return await response.blob();
191
+ }
192
+
193
+ const responseBody = await readSmokeResponseBody(response, options.responseType);
194
+
195
+ if (!response.ok) {
196
+ throw buildSmokeHttpError(response, responseBody);
197
+ }
198
+
199
+ if (
200
+ responseBody &&
201
+ typeof responseBody === 'object' &&
202
+ (responseBody['@type'] === 'Error' ||
203
+ responseBody['@type'] === 'ConstraintViolationList')
204
+ ) {
205
+ throw buildSmokeHttpError(response, responseBody);
206
+ }
207
+
208
+ if (options.responseType === 'text') {
209
+ return typeof responseBody === 'string' ? responseBody : '';
210
+ }
211
+
212
+ return responseBody;
213
+ }
214
+
9
215
  export const api = {
10
216
  device: JSON.parse(localStorage.getItem('device') || '{}'),
11
217
  masterDevice: JSON.parse(localStorage.getItem('master-device') || '{}'),
@@ -17,19 +223,25 @@ export const api = {
17
223
  const apiEntryPoint = resolveApiEntryPoint(APP_ENV.API_ENTRYPOINT);
18
224
  const entryPoint = apiEntryPoint + (apiEntryPoint.endsWith("/") ? "" : "/");
19
225
  const url = new URL(uri.startsWith('/') ? uri.substring(1) : uri, entryPoint).href;
226
+ const basicAuth = encodeBasicAuth(APP_ENV.HTACCESS_USER, APP_ENV.HTACCESS_PASSWORD);
227
+ const headers = {
228
+ 'API-TOKEN': token,
229
+ 'App-Domain': appDomain,
230
+ 'Accept': '*/*',
231
+ // IMPORTANTE: Deixe vazio para o Axios/Browser definir o boundary do multipart
232
+ };
233
+
234
+ if (basicAuth !== '') {
235
+ headers.Authorization = `Basic ${basicAuth}`;
236
+ }
20
237
 
21
238
  return axios.post(url, formData, {
22
- headers: {
23
- 'API-TOKEN': token,
24
- 'App-Domain': appDomain,
25
- 'Accept': '*/*',
26
- // IMPORTANTE: Deixe vazio para o Axios/Browser definir o boundary do multipart
27
- }
28
- });
29
- },
30
- fetch: async function (uri, options = {}) {
31
- if (typeof options.headers === 'undefined')
32
- Object.assign(options, { headers: new Headers() });
239
+ headers,
240
+ });
241
+ },
242
+ fetch: async function (uri, options = {}) {
243
+ if (typeof options.headers === 'undefined')
244
+ Object.assign(options, { headers: new Headers() });
33
245
 
34
246
  // Always refresh device from localStorage in case it was set after module load
35
247
  try {
@@ -45,14 +257,19 @@ export const api = {
45
257
  } catch (e) {
46
258
  this.masterDevice = {};
47
259
  }
48
-
49
- let token = await this.getToken();
50
- if (token) options.headers.set('API-TOKEN', token);
51
-
52
- let bodyDeviceId = null;
53
- if (options.body) {
54
- if (typeof options.body === 'object') {
55
- bodyDeviceId = options.body.device || null;
260
+
261
+ let token = await this.getToken();
262
+ if (token) options.headers.set('API-TOKEN', token);
263
+
264
+ const basicAuth = encodeBasicAuth(APP_ENV.HTACCESS_USER, APP_ENV.HTACCESS_PASSWORD);
265
+ if (basicAuth !== '' && !options.headers.has('Authorization')) {
266
+ options.headers.set('Authorization', `Basic ${basicAuth}`);
267
+ }
268
+
269
+ let bodyDeviceId = null;
270
+ if (options.body) {
271
+ if (typeof options.body === 'object') {
272
+ bodyDeviceId = options.body.device || null;
56
273
  } else if (typeof options.body === 'string') {
57
274
  try {
58
275
  const parsedBody = JSON.parse(options.body);
@@ -63,21 +280,36 @@ export const api = {
63
280
  }
64
281
  }
65
282
 
66
- const headerDeviceId =
67
- this.masterDevice?.id || this.device?.id || bodyDeviceId;
68
- if (headerDeviceId) options.headers.set('DEVICE', headerDeviceId);
283
+ const requestDevice = resolveRequestDevice(
284
+ this.masterDevice,
285
+ this.device,
286
+ bodyDeviceId,
287
+ );
288
+
289
+ if (requestDevice?.id) {
290
+ options.headers.set('DEVICE', requestDevice.id);
291
+
292
+ const requestDeviceType = String(requestDevice.type || '')
293
+ .trim()
294
+ .toUpperCase();
295
+ if (requestDeviceType !== '') {
296
+ options.headers.set('DEVICE-TYPE', requestDeviceType);
297
+ }
298
+ }
69
299
  if (options.responseType != 'text') {
70
300
  options.headers.set('Content-Type', MIME_TYPE);
71
301
  options.headers.set('Accept', MIME_TYPE);
72
302
  }
73
303
  options.headers.set('App-Domain', resolveAppDomain(APP_ENV.DOMAIN));
74
-
75
- if (options.body && typeof options.body != 'string') {
76
- options.body = JSON.stringify(options.body);
77
- }
78
- if (options.params) {
79
- uri = this.buildQueryString(uri, options);
80
- }
304
+
305
+ if (options.body && typeof options.body != 'string') {
306
+ options.body = JSON.stringify(options.body);
307
+ }
308
+ uri = stripDomainQueryParams(uri);
309
+
310
+ if (options.params) {
311
+ uri = this.buildQueryString(uri, options);
312
+ }
81
313
  return myFetch(uri, options).catch(e => {
82
314
  throw e;
83
315
  });
@@ -100,13 +332,18 @@ export const api = {
100
332
 
101
333
  return session?.token || session?.api_key;
102
334
  },
103
- serialize(obj, prefix = '') {
104
- const pairs = [];
105
-
106
- for (const key in obj) {
107
- if (obj.hasOwnProperty(key)) {
108
- const value = obj[key];
109
- let fullKey = prefix ? `${prefix}.${key}` : key; // Usa ponto para objetos
335
+ serialize(obj, prefix = '') {
336
+ const pairs = [];
337
+
338
+ for (const key in obj) {
339
+ if (obj.hasOwnProperty(key)) {
340
+ const normalizedKey = String(key || '').trim().toLowerCase();
341
+ if (DOMAIN_QUERY_PARAM_KEYS.has(normalizedKey)) {
342
+ continue;
343
+ }
344
+
345
+ const value = obj[key];
346
+ let fullKey = prefix ? `${prefix}.${key}` : key; // Usa ponto para objetos
110
347
 
111
348
  if (value === null || value === undefined) {
112
349
  pairs.push(`${fullKey}=`);
@@ -135,21 +372,54 @@ export const api = {
135
372
  return pairs;
136
373
  },
137
374
 
138
- buildQueryString(uri, options) {
139
- if (options.params) {
140
- const params = this.serialize(options.params);
141
- uri = `${uri}?${params.join('&')}`;
142
- }
143
- return uri;
144
- },
145
- post: async function (uri, body = {}) {
146
- const options = {
147
- method: 'POST',
148
- body: body,
149
- };
150
- return await this.fetch(uri, options);
151
- },
152
- execute: function (params) {
153
- return axios(params);
154
- },
155
- };
375
+ buildQueryString(uri, options) {
376
+ const safeUri = stripDomainQueryParams(uri);
377
+
378
+ if (options.params) {
379
+ const params = this.serialize(options.params);
380
+ if (params.length === 0) {
381
+ return safeUri;
382
+ }
383
+
384
+ const hashIndex = safeUri.indexOf('#');
385
+ const withoutHash = hashIndex >= 0 ? safeUri.slice(0, hashIndex) : safeUri;
386
+ const hash = hashIndex >= 0 ? safeUri.slice(hashIndex) : '';
387
+ const separator = withoutHash.includes('?') ? '&' : '?';
388
+
389
+ return `${withoutHash}${separator}${params.join('&')}${hash}`;
390
+ }
391
+
392
+ return safeUri;
393
+ },
394
+ post: async function (uri, body = {}) {
395
+ const options = {
396
+ method: 'POST',
397
+ body: body,
398
+ };
399
+ return await this.fetch(uri, options);
400
+ },
401
+ loadSmokeIndex: async function (config = {}) {
402
+ return await requestSmoke('/tests', {}, config);
403
+ },
404
+ loadArtifactBlob: async function (config = {}, artifact) {
405
+ if (!artifact || typeof artifact !== 'object') {
406
+ throw new Error('Artefato inválido.');
407
+ }
408
+
409
+ if (!artifact.url) {
410
+ throw new Error('Artefato inválido.');
411
+ }
412
+
413
+ return await requestSmoke(artifact.url, {
414
+ responseType: 'blob',
415
+ }, config);
416
+ },
417
+ triggerSmokeRun: async function (config = {}) {
418
+ return await requestSmoke('/tests/run', {
419
+ method: 'POST',
420
+ }, config);
421
+ },
422
+ execute: function (params) {
423
+ return axios(params);
424
+ },
425
+ };
@@ -1,108 +1,68 @@
1
- import React, { useState } from 'react';
2
- import {
3
- View,
4
- Text,
5
- TouchableOpacity,
6
- ScrollView,
7
- ActivityIndicator,
8
- Platform,
9
- } from 'react-native';
10
-
11
- import Icon from 'react-native-vector-icons/MaterialIcons';
12
- import * as DocumentPicker from 'expo-document-picker';
1
+ import React from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ ScrollView,
7
+ ActivityIndicator,
8
+ } from 'react-native';
9
+
10
+ import Icon from 'react-native-vector-icons/MaterialIcons';
13
11
  import { useStore } from '@store';
14
- import AnimatedModal from '@controleonline/ui-crm/src/react/components/AnimatedModal';
15
- import { api } from '@controleonline/ui-common/src/api';
12
+ import AnimatedModal from './AnimatedModal';
16
13
  import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
14
+ import DefaultUpload from '@controleonline/ui-default/src/react/components/upload/DefaultUpload';
17
15
  import styles from './AddImportModal.styles';
18
-
19
- const AddImportModal = ({ visible, onClose, onSuccess, context = {} }) => {
20
- const { showError, showSuccess } = useMessage();
21
- const peopleStore = useStore('people');
22
- const getters = peopleStore.getters;
23
- const { currentCompany } = getters;
24
-
25
- const [file, setFile] = useState(null);
26
- const [loading, setLoading] = useState(false);
16
+
17
+ const AddImportModal = ({ visible, onClose, onSuccess, context = {} }) => {
18
+ const { showError, showSuccess } = useMessage();
19
+ const peopleStore = useStore('people');
20
+ const importsStore = useStore('imports');
21
+ const themeStore = useStore('theme');
22
+ const getters = peopleStore.getters;
23
+ const { currentCompany } = getters;
24
+ const importActions = importsStore.actions || {};
25
+ const themeColors = themeStore?.getters?.colors || {};
26
+ const buttonPalette = {
27
+ buttonBackground: themeColors.buttonBackground,
28
+ buttonBorder: themeColors.buttonBorder,
29
+ buttonText: themeColors.buttonText,
30
+ buttonIcon: themeColors.buttonIcon || themeColors.buttonText,
31
+ };
32
+
27
33
  const modalTitle = global.t?.t('imports', 'title', 'new_import');
28
34
  const csvLabel = global.t?.t('imports', 'label', 'csv_file');
29
35
  const selectFileLabel = global.t?.t('imports', 'button', 'select_file');
30
36
  const cancelLabel = global.t?.t('imports', 'button', 'cancel');
31
- const importLabel = global.t?.t('imports', 'button', 'import');
32
37
  const csvOnlyLabel = global.t?.t('imports', 'message', 'only_csv_files_are_allowed');
33
- const pickFileErrorLabel = global.t?.t('imports', 'error', 'error_selecting_file');
34
38
  const importSuccessLabel = global.t?.t('imports', 'success', 'import_sent_successfully');
35
39
  const importErrorLabel = global.t?.t('imports', 'error', 'error_sending_import');
36
- const missingFileLabel = global.t?.t('imports', 'error', 'select_a_csv_file');
37
-
38
- const handlePickFile = async () => {
39
- try {
40
- const result = await DocumentPicker.getDocumentAsync({
41
- type: 'text/csv',
42
- copyToCacheDirectory: true,
43
- multiple: false,
44
- });
45
-
46
- if (result.canceled) return;
47
-
48
- const pickedFile = result.assets[0];
49
-
50
- if (!pickedFile.name?.toLowerCase().endsWith('.csv')) {
51
- showError(csvOnlyLabel);
52
- return;
53
- }
54
-
55
- setFile(pickedFile);
56
- } catch {
57
- showError(pickFileErrorLabel);
58
- }
59
- };
60
40
 
61
- const handleSave = async () => {
62
- if (!file) {
63
- showError(missingFileLabel);
64
- return;
41
+ const handleUploadImportFile = async ({ file }) => {
42
+ if (!file?.name?.toLowerCase().endsWith('.csv')) {
43
+ throw new Error(csvOnlyLabel);
65
44
  }
66
-
67
- setLoading(true);
68
-
69
- try {
70
- const formData = new FormData();
71
- formData.append('importType', context.context);
72
-
73
-
74
- formData.append('people', String(currentCompany.id));
75
-
76
-
77
- if (Platform.OS === 'web') {
78
- const response = await fetch(file.uri);
79
- const blob = await response.blob();
80
- formData.append('file', blob, file.name || 'import.csv');
81
- } else {
82
- formData.append('file', {
83
- uri: file.uri,
84
- name: file.name || 'import.csv',
85
- type: file.mimeType || 'text/csv',
86
- });
87
- }
88
45
 
89
- await api.upload('/imports/upload', formData);
46
+ try {
47
+ await importActions.uploadImportFile({
48
+ file,
49
+ importType: context.context,
50
+ peopleId: currentCompany.id,
51
+ });
90
52
  showSuccess(importSuccessLabel);
91
53
  if (onSuccess) onSuccess();
92
54
  handleClose();
55
+ return file;
93
56
  } catch {
94
- showError(importErrorLabel);
95
- } finally {
96
- setLoading(false);
57
+ throw new Error(importErrorLabel);
97
58
  }
98
- };
99
-
100
- const handleClose = () => {
101
- setFile(null);
102
- onClose();
103
- };
104
-
105
- return (
59
+ };
60
+
61
+ const handleClose = () => {
62
+ onClose();
63
+ };
64
+
65
+ return (
106
66
  <AnimatedModal
107
67
  visible={visible}
108
68
  onRequestClose={handleClose}
@@ -118,15 +78,50 @@ const AddImportModal = ({ visible, onClose, onSuccess, context = {} }) => {
118
78
 
119
79
  <ScrollView style={styles.content}>
120
80
  <Text style={styles.label}>{csvLabel}</Text>
121
- <TouchableOpacity
122
- onPress={handlePickFile}
123
- style={styles.filePicker}
124
- >
125
- <Text numberOfLines={1} style={styles.fileName}>
126
- {file ? file.name : selectFileLabel}
127
- </Text>
128
- <Icon name="upload-file" size={22} color={file ? "#007bff" : "#666"} />
129
- </TouchableOpacity>
81
+ <DefaultUpload
82
+ relationStoreName="imports"
83
+ relationField="import"
84
+ relationResource="imports"
85
+ entityId={context.context || 'import'}
86
+ companyId={currentCompany.id}
87
+ context={`imports-${context.context || 'default'}`}
88
+ libraryContexts={[`imports-${context.context || 'default'}`]}
89
+ acceptedTypes="text/csv,.csv"
90
+ fileType=""
91
+ title={csvLabel}
92
+ triggerLabel={selectFileLabel}
93
+ managerTitle={modalTitle}
94
+ searchPlaceholder={selectFileLabel}
95
+ uploadButtonLabel={selectFileLabel}
96
+ emptyAttachmentLabel=""
97
+ emptyLibraryLabel={selectFileLabel}
98
+ showInlineContent={false}
99
+ uploadResultAlreadyAttached
100
+ requireEntity={false}
101
+ onUploadFile={handleUploadImportFile}
102
+ renderTrigger={({openManager, uploading}) => (
103
+ <TouchableOpacity
104
+ onPress={openManager}
105
+ disabled={uploading}
106
+ style={[
107
+ styles.filePicker,
108
+ {
109
+ backgroundColor: buttonPalette.buttonBackground,
110
+ borderColor: buttonPalette.buttonBorder,
111
+ },
112
+ ]}
113
+ >
114
+ <Text numberOfLines={1} style={[styles.fileName, { color: buttonPalette.buttonText }]}>
115
+ {selectFileLabel}
116
+ </Text>
117
+ {uploading ? (
118
+ <ActivityIndicator color={buttonPalette.buttonIcon} />
119
+ ) : (
120
+ <Icon name="upload-file" size={22} color={buttonPalette.buttonIcon} />
121
+ )}
122
+ </TouchableOpacity>
123
+ )}
124
+ />
130
125
  <Text style={styles.helperText}>
131
126
  {csvOnlyLabel}
132
127
  </Text>
@@ -136,22 +131,11 @@ const AddImportModal = ({ visible, onClose, onSuccess, context = {} }) => {
136
131
  <TouchableOpacity onPress={handleClose} style={styles.secondaryButton}>
137
132
  <Text>{cancelLabel}</Text>
138
133
  </TouchableOpacity>
139
-
140
- <TouchableOpacity
141
- onPress={handleSave}
142
- disabled={loading}
143
- style={[
144
- styles.primaryButton,
145
- loading && styles.primaryButtonDisabled,
146
- ]}
147
- >
148
- {loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.primaryButtonText}>{importLabel}</Text>}
149
- </TouchableOpacity>
150
134
  </View>
151
135
  </View>
152
- </AnimatedModal>
153
- );
154
- };
155
-
136
+ </AnimatedModal>
137
+ );
138
+ };
139
+
156
140
  export default AddImportModal;
157
141
  // TODO(store-first): quando este arquivo for mexido, mover a leitura para stores e evitar chamadas HTTP diretas quando o store ja resolver isso.