@controleonline/ui-common 1.2.70

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 (203) hide show
  1. package/.github/agents/developer.agent.md +30 -0
  2. package/.github/agents/devops.agent.md +30 -0
  3. package/.github/agents/qa.agent.md +30 -0
  4. package/.github/agents/security.agent.md +30 -0
  5. package/.github/workflows/npmjs.yml +35 -0
  6. package/.github/workflows/pull-request-checks.yml +19 -0
  7. package/.scrutinizer.yml +24 -0
  8. package/FUNDING.yml +1 -0
  9. package/package.json +28 -0
  10. package/src/api/fetch.js +74 -0
  11. package/src/api/index.js +155 -0
  12. package/src/api/localDB.js +131 -0
  13. package/src/api/queue.js +66 -0
  14. package/src/react/assets/tap.mp3 +0 -0
  15. package/src/react/components/AddImportModal.js +157 -0
  16. package/src/react/components/AddImportModal.styles.js +81 -0
  17. package/src/react/components/BackgroundRuntimeBridge.js +219 -0
  18. package/src/react/components/BottomNavigationBar.config.js +238 -0
  19. package/src/react/components/BottomNavigationBar.js +120 -0
  20. package/src/react/components/BottomNavigationBar.styles.js +110 -0
  21. package/src/react/components/CategoryForm.js +572 -0
  22. package/src/react/components/CategoryForm.styles.js +334 -0
  23. package/src/react/components/ConfirmModal.js +24 -0
  24. package/src/react/components/ConfirmModal.styles.js +43 -0
  25. package/src/react/components/ContextHelpButton.js +69 -0
  26. package/src/react/components/ContextHelpButton.styles.js +62 -0
  27. package/src/react/components/DefaultProvider.native.js +848 -0
  28. package/src/react/components/DefaultProvider.styles.js +21 -0
  29. package/src/react/components/DefaultProvider.web.js +909 -0
  30. package/src/react/components/DeliveryPushBridge.native.js +314 -0
  31. package/src/react/components/DeliveryPushBridge.web.js +5 -0
  32. package/src/react/components/DeviceAlertSoundService.js +499 -0
  33. package/src/react/components/EntityLogContent.js +909 -0
  34. package/src/react/components/EntityLogContent.styles.js +400 -0
  35. package/src/react/components/EventBus.js +12 -0
  36. package/src/react/components/KioskModeBridge.js +65 -0
  37. package/src/react/components/LauncherModeBridge.js +47 -0
  38. package/src/react/components/LinkedOrderProductsTab.js +388 -0
  39. package/src/react/components/LinkedOrderProductsTab.styles.js +180 -0
  40. package/src/react/components/ManagerPushBridge.native.js +345 -0
  41. package/src/react/components/MessageService.js +314 -0
  42. package/src/react/components/Paywall/Google.js +89 -0
  43. package/src/react/components/PrintService.js +778 -0
  44. package/src/react/components/ProductCatalogCacheService.js +45 -0
  45. package/src/react/components/RemoteCheckoutService.js +292 -0
  46. package/src/react/components/RuntimeBottomNavigationBar.js +151 -0
  47. package/src/react/components/RuntimeInfoFooter.js +214 -0
  48. package/src/react/components/RuntimeInfoFooter.styles.js +56 -0
  49. package/src/react/components/SystemErrorToast.js +29 -0
  50. package/src/react/components/SystemErrorToast.styles.js +38 -0
  51. package/src/react/components/TouchFeedbackProvider.js +65 -0
  52. package/src/react/components/UnifiedPaymentBar.js +155 -0
  53. package/src/react/components/UnifiedPaymentBar.styles.js +174 -0
  54. package/src/react/components/WebsocketListener.native.js +487 -0
  55. package/src/react/components/WebsocketListener.web.js +102 -0
  56. package/src/react/components/inputs/IdInput.js +49 -0
  57. package/src/react/components/inputs/IdInput.styles.js +26 -0
  58. package/src/react/components/inputs/InvoiceDueDateFilterInput.js +74 -0
  59. package/src/react/components/inputs/InvoiceDueDateFilterInput.styles.js +34 -0
  60. package/src/react/components/inputs/InvoiceDueDateRangeFilterInput.js +115 -0
  61. package/src/react/components/inputs/InvoiceDueDateRangeFilterInput.styles.js +39 -0
  62. package/src/react/components/lists/CategoriesList.js +56 -0
  63. package/src/react/components/lists/PaymentTypeList.js +55 -0
  64. package/src/react/components/lists/ReceiverList.js +56 -0
  65. package/src/react/components/lists/SelectList.styles.js +18 -0
  66. package/src/react/components/lists/StatusList.js +55 -0
  67. package/src/react/components/lists/WalletList.js +54 -0
  68. package/src/react/components/radio/Spotify.js +144 -0
  69. package/src/react/components/radio/Spotify.styles.js +32 -0
  70. package/src/react/components/radio/YouTube.js +82 -0
  71. package/src/react/components/radio/YouTube.styles.js +16 -0
  72. package/src/react/components/toastConfig.js +16 -0
  73. package/src/react/config/deviceConfigBootstrap.js +571 -0
  74. package/src/react/index.js +4 -0
  75. package/src/react/localStorage.js +30 -0
  76. package/src/react/pages/EntityLogPage.js +124 -0
  77. package/src/react/pages/EntityLogPage.styles.js +17 -0
  78. package/src/react/pages/GenericLogPage.js +760 -0
  79. package/src/react/pages/GenericLogPage.styles.js +401 -0
  80. package/src/react/pages/Imports.js +361 -0
  81. package/src/react/pages/Imports.styles.js +223 -0
  82. package/src/react/pages/SettingsPage/PaymentTypesByWalletTab.js +462 -0
  83. package/src/react/pages/SettingsPage/index.js +1171 -0
  84. package/src/react/pages/SettingsPage/index.styles.js +122 -0
  85. package/src/react/print/jobs.js +147 -0
  86. package/src/react/print/providers/local.js +53 -0
  87. package/src/react/print/providers/remote.js +81 -0
  88. package/src/react/print/selection.js +296 -0
  89. package/src/react/print/usePrintButtonController.js +474 -0
  90. package/src/react/router/routes.js +42 -0
  91. package/src/react/services/NetworkPrinterService.native.js +391 -0
  92. package/src/react/services/NetworkPrinterService.web.js +15 -0
  93. package/src/react/stores/index.js +128 -0
  94. package/src/react/stores/storeErrorBridge.js +36 -0
  95. package/src/react/utils/backgroundRuntimeRegistration.js +69 -0
  96. package/src/react/utils/cashPayment.js +87 -0
  97. package/src/react/utils/commercialDocumentOrders.js +269 -0
  98. package/src/react/utils/dateRangeFilter.js +264 -0
  99. package/src/react/utils/deviceRuntime.js +193 -0
  100. package/src/react/utils/entityDisplay.js +231 -0
  101. package/src/react/utils/entityLog.js +649 -0
  102. package/src/react/utils/fileUrl.js +102 -0
  103. package/src/react/utils/frontendDebugLog.js +45 -0
  104. package/src/react/utils/googleMapsConfig.js +49 -0
  105. package/src/react/utils/importStatus.js +100 -0
  106. package/src/react/utils/invoiceDueDateMessages.js +14 -0
  107. package/src/react/utils/invoicePresentation.js +46 -0
  108. package/src/react/utils/logSettings.js +143 -0
  109. package/src/react/utils/maintenanceSettings.js +53 -0
  110. package/src/react/utils/managerOrderNotifications.js +523 -0
  111. package/src/react/utils/mapNavigation.js +76 -0
  112. package/src/react/utils/menuCatalogConfig.js +8 -0
  113. package/src/react/utils/menuCatalogDownload.js +157 -0
  114. package/src/react/utils/networkCameraDevices.js +175 -0
  115. package/src/react/utils/networkDeviceProfiles.js +135 -0
  116. package/src/react/utils/normalizedCatalogDownload.js +157 -0
  117. package/src/react/utils/notificationNavigation.js +94 -0
  118. package/src/react/utils/notificationSound.js +42 -0
  119. package/src/react/utils/paymentDevices.js +397 -0
  120. package/src/react/utils/paymentGatewayExecution.js +91 -0
  121. package/src/react/utils/paymentOptions.js +88 -0
  122. package/src/react/utils/peopleDisplay.js +39 -0
  123. package/src/react/utils/printerDevices.js +465 -0
  124. package/src/react/utils/remotePayment.js +61 -0
  125. package/src/react/utils/runtimeFooter.js +475 -0
  126. package/src/react/utils/runtimeLanguage.js +50 -0
  127. package/src/react/utils/runtimeMenu.js +224 -0
  128. package/src/react/utils/screenMetrics.js +30 -0
  129. package/src/react/utils/shopConfig.js +294 -0
  130. package/src/react/utils/shopFranchises.js +114 -0
  131. package/src/react/utils/socketRuntimePipeline.js +454 -0
  132. package/src/react/utils/spoolAckTracker.js +77 -0
  133. package/src/react/utils/storeColumns.js +427 -0
  134. package/src/react/utils/systemErrorChannel.js +109 -0
  135. package/src/react/utils/systemErrorMessage.js +67 -0
  136. package/src/react/utils/toastPresentation.js +53 -0
  137. package/src/react/utils/websocketSharedRuntime.js +826 -0
  138. package/src/store/acl/getters.js +4 -0
  139. package/src/store/acl/index.js +19 -0
  140. package/src/store/acl/mutation_types.js +4 -0
  141. package/src/store/acl/mutations.js +17 -0
  142. package/src/store/address/city.js +22 -0
  143. package/src/store/categories/category_file/index.js +25 -0
  144. package/src/store/categories/index.js +93 -0
  145. package/src/store/configs/customActions.js +131 -0
  146. package/src/store/configs/index.js +25 -0
  147. package/src/store/connections/customActions.js +26 -0
  148. package/src/store/connections/index.js +124 -0
  149. package/src/store/device/index.js +22 -0
  150. package/src/store/device_config/customActions.js +190 -0
  151. package/src/store/device_config/index.js +28 -0
  152. package/src/store/entity_log/customActions.js +63 -0
  153. package/src/store/entity_log/index.js +25 -0
  154. package/src/store/extra/data.js +85 -0
  155. package/src/store/extra/fields.js +88 -0
  156. package/src/store/file/actions.js +27 -0
  157. package/src/store/file/index.js +24 -0
  158. package/src/store/imports/index.js +88 -0
  159. package/src/store/print/customActions.js +251 -0
  160. package/src/store/print/index.js +28 -0
  161. package/src/store/printer/customActions.js +54 -0
  162. package/src/store/printer/index.js +26 -0
  163. package/src/store/runtime_debug/customActions.js +71 -0
  164. package/src/store/runtime_debug/index.js +30 -0
  165. package/src/store/status/index.js +65 -0
  166. package/src/store/timezones/index.js +43 -0
  167. package/src/store/websocket/customActions.js +24 -0
  168. package/src/store/websocket/index.js +28 -0
  169. package/src/tests/react/components/LauncherModeBridge.test.js +93 -0
  170. package/src/tests/react/config/deviceConfigBootstrap.test.js +337 -0
  171. package/src/tests/react/print/localPrintProvider.test.js +45 -0
  172. package/src/tests/react/utils/backgroundRuntimeRegistration.test.js +123 -0
  173. package/src/tests/react/utils/cashPayment.test.js +41 -0
  174. package/src/tests/react/utils/dateRangeFilter.test.mjs +29 -0
  175. package/src/tests/react/utils/entityDisplay.test.js +18 -0
  176. package/src/tests/react/utils/googleMapsConfig.test.js +32 -0
  177. package/src/tests/react/utils/importStatus.test.js +85 -0
  178. package/src/tests/react/utils/invoicePresentation.test.mjs +40 -0
  179. package/src/tests/react/utils/managerOrderNotifications.test.js +127 -0
  180. package/src/tests/react/utils/mapNavigation.test.js +58 -0
  181. package/src/tests/react/utils/notificationNavigation.test.js +39 -0
  182. package/src/tests/react/utils/notificationSound.test.js +27 -0
  183. package/src/tests/react/utils/paymentDevices.test.js +88 -0
  184. package/src/tests/react/utils/runtimeFooter.test.js +429 -0
  185. package/src/tests/react/utils/runtimeLanguage.test.js +80 -0
  186. package/src/tests/react/utils/runtimeMenu.test.js +174 -0
  187. package/src/tests/react/utils/shopFranchises.test.js +82 -0
  188. package/src/tests/react/utils/socketRuntimePipeline.test.js +124 -0
  189. package/src/tests/react/utils/spoolAckTracker.test.js +40 -0
  190. package/src/tests/react/utils/storeErrorBridge.test.js +66 -0
  191. package/src/tests/react/utils/systemErrorChannel.test.js +58 -0
  192. package/src/tests/react/utils/systemErrorMessage.test.js +59 -0
  193. package/src/tests/react/utils/toastPresentation.test.js +75 -0
  194. package/src/tests/react/utils/translate.test.mjs +396 -0
  195. package/src/tests/react/utils/websocketSharedRuntime.test.js +245 -0
  196. package/src/utils/apiEntryPoint.js +31 -0
  197. package/src/utils/appDomain.js +50 -0
  198. package/src/utils/config.js +14 -0
  199. package/src/utils/formatter.js +419 -0
  200. package/src/utils/integrationConfigs.js +79 -0
  201. package/src/utils/methods.js +23 -0
  202. package/src/utils/oauth.js +42 -0
  203. package/src/utils/translate.js +620 -0
@@ -0,0 +1,909 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Image,
5
+ ScrollView,
6
+ Text,
7
+ TouchableOpacity,
8
+ View,
9
+ } from 'react-native';
10
+ import Icon from 'react-native-vector-icons/MaterialIcons';
11
+ import { useStore } from '@store';
12
+ import Formatter from '@controleonline/ui-common/src/utils/formatter';
13
+ import {
14
+ buildEntityChildren,
15
+ buildEntityKey,
16
+ buildEntityLabel,
17
+ buildEntitySummaryFields,
18
+ formatLogFieldLabel,
19
+ formatLogValue,
20
+ resolveEntityImageUrl,
21
+ } from '@controleonline/ui-common/src/react/utils/entityLog';
22
+ import { resolveStoreConfigByEntity } from '@controleonline/ui-common/src/react/utils/storeColumns';
23
+ import createStyles, {
24
+ buildEntityLogPalette,
25
+ } from './EntityLogContent.styles';
26
+
27
+ const ACTION_META = {
28
+ insert: {
29
+ color: '#16A34A',
30
+ icon: 'add-circle',
31
+ label: 'Criado',
32
+ },
33
+ update: {
34
+ color: '#2563EB',
35
+ icon: 'edit-note',
36
+ label: 'Alterado',
37
+ },
38
+ delete: {
39
+ color: '#DC2626',
40
+ icon: 'delete-outline',
41
+ label: 'Removido',
42
+ },
43
+ info: {
44
+ color: '#0F766E',
45
+ icon: 'info-outline',
46
+ label: 'Info',
47
+ },
48
+ notice: {
49
+ color: '#0369A1',
50
+ icon: 'notifications-none',
51
+ label: 'Aviso',
52
+ },
53
+ warning: {
54
+ color: '#D97706',
55
+ icon: 'warning-amber',
56
+ label: 'Alerta',
57
+ },
58
+ error: {
59
+ color: '#DC2626',
60
+ icon: 'error-outline',
61
+ label: 'Erro',
62
+ },
63
+ critical: {
64
+ color: '#991B1B',
65
+ icon: 'report-gmailerrorred',
66
+ label: 'Critico',
67
+ },
68
+ debug: {
69
+ color: '#475569',
70
+ icon: 'bug-report',
71
+ label: 'Debug',
72
+ },
73
+ };
74
+
75
+ const GENERIC_PAYLOAD_META_FIELDS = new Set(['channel', 'context', 'level', 'message']);
76
+
77
+ const buildContextEntries = context => {
78
+ if (!context) {
79
+ return [];
80
+ }
81
+
82
+ if (Array.isArray(context)) {
83
+ return context.map((value, index) => [`item_${index + 1}`, value]);
84
+ }
85
+
86
+ if (typeof context === 'object') {
87
+ return Object.entries(context);
88
+ }
89
+
90
+ return [['context', context]];
91
+ };
92
+
93
+ const formatContextValue = value => {
94
+ if (value === null || value === undefined || value === '') {
95
+ return '—';
96
+ }
97
+
98
+ if (typeof value === 'boolean') {
99
+ return value ? 'Sim' : 'Nao';
100
+ }
101
+
102
+ if (typeof value === 'string') {
103
+ return value;
104
+ }
105
+
106
+ if (typeof value === 'number') {
107
+ return String(value);
108
+ }
109
+
110
+ try {
111
+ return JSON.stringify(value, null, 2);
112
+ } catch {
113
+ return String(value);
114
+ }
115
+ };
116
+
117
+ const resolvePayload = log => {
118
+ if (log?.payload && typeof log.payload === 'object' && !Array.isArray(log.payload)) {
119
+ return log.payload;
120
+ }
121
+
122
+ if (typeof log?.object === 'string') {
123
+ try {
124
+ const parsed = JSON.parse(log.object);
125
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
126
+ } catch {
127
+ return {};
128
+ }
129
+ }
130
+
131
+ return {};
132
+ };
133
+
134
+ // O card de resumo mostra os dados essenciais da entidade expandida.
135
+ const EntitySummaryCard = ({
136
+ columns = [],
137
+ detailState,
138
+ entity,
139
+ entityClass,
140
+ storeName = '',
141
+ styles,
142
+ }) => {
143
+ const summaryFields = useMemo(
144
+ () => buildEntitySummaryFields({
145
+ entity,
146
+ className: entityClass,
147
+ columns,
148
+ storeName,
149
+ }),
150
+ [columns, entity, entityClass, storeName],
151
+ );
152
+ const imageUrl = useMemo(() => resolveEntityImageUrl(entity), [entity]);
153
+
154
+ if (detailState?.status === 'loading' && !imageUrl && !summaryFields.length) {
155
+ return (
156
+ <View style={styles.entityStateCard}>
157
+ <ActivityIndicator size="small" color="#2563EB" />
158
+ <Text style={styles.entityStateText}>Carregando detalhes...</Text>
159
+ </View>
160
+ );
161
+ }
162
+
163
+ if (
164
+ detailState?.status !== 'loading' &&
165
+ detailState?.status !== 'success' &&
166
+ !imageUrl &&
167
+ !summaryFields.length
168
+ ) {
169
+ return null;
170
+ }
171
+
172
+ return (
173
+ <View style={styles.entitySummaryCard}>
174
+ <View style={styles.entitySummaryHeader}>
175
+ <Text style={styles.sectionTitle}>Resumo</Text>
176
+ {detailState?.status === 'loading' ? (
177
+ <Text style={styles.entityStateText}>Atualizando...</Text>
178
+ ) : null}
179
+ </View>
180
+
181
+ <View style={styles.entitySummaryBody}>
182
+ {imageUrl ? (
183
+ <Image
184
+ source={{ uri: imageUrl }}
185
+ style={styles.entityPreviewImage}
186
+ resizeMode="cover"
187
+ />
188
+ ) : null}
189
+
190
+ <View style={styles.entitySummaryGrid}>
191
+ {summaryFields.length ? (
192
+ summaryFields.map(field => (
193
+ <View key={field.key} style={styles.entitySummaryField}>
194
+ <Text style={styles.entitySummaryLabel}>{field.label}</Text>
195
+ <Text style={styles.entitySummaryValue}>{field.value}</Text>
196
+ </View>
197
+ ))
198
+ ) : (
199
+ <Text style={styles.emptyInlineText}>Sem dados resumidos.</Text>
200
+ )}
201
+ </View>
202
+ </View>
203
+
204
+ {detailState?.status === 'error' ? (
205
+ <Text style={styles.entityStateErrorText}>
206
+ {detailState?.error || 'Nao foi possivel carregar os detalhes.'}
207
+ </Text>
208
+ ) : null}
209
+ </View>
210
+ );
211
+ };
212
+
213
+ const getActionMeta = action => ACTION_META[String(action || '').toLowerCase()] || {
214
+ color: '#64748B',
215
+ icon: 'history',
216
+ label: 'Evento',
217
+ };
218
+
219
+ const renderChangeValue = (styles, title, value) => (
220
+ <View style={styles.changeValueBox}>
221
+ {title ? <Text style={styles.changeValueTitle}>{title}</Text> : null}
222
+ <Text style={styles.changeValueText}>{value}</Text>
223
+ </View>
224
+ );
225
+
226
+ const LogCard = ({
227
+ columns = [],
228
+ log,
229
+ rowContext = null,
230
+ storeName = '',
231
+ styles,
232
+ }) => {
233
+ const payload = useMemo(() => resolvePayload(log), [log]);
234
+ const actionMeta = useMemo(() => getActionMeta(log?.action), [log?.action]);
235
+ const entries = useMemo(() => Object.entries(payload || {}), [payload]);
236
+ const detailEntries = useMemo(
237
+ () => entries.filter(([field]) => !GENERIC_PAYLOAD_META_FIELDS.has(field)),
238
+ [entries],
239
+ );
240
+ const genericMessage = useMemo(
241
+ () => (typeof payload?.message === 'string' ? payload.message.trim() : ''),
242
+ [payload?.message],
243
+ );
244
+ const genericChannel = useMemo(
245
+ () => (typeof payload?.channel === 'string' ? payload.channel.trim() : ''),
246
+ [payload?.channel],
247
+ );
248
+ const contextEntries = useMemo(
249
+ () => buildContextEntries(payload?.context),
250
+ [payload?.context],
251
+ );
252
+ const logDate = useMemo(
253
+ () => Formatter.formatDateYmdTodmY(log?.createdAt, true),
254
+ [log?.createdAt],
255
+ );
256
+
257
+ return (
258
+ <View style={styles.logCard}>
259
+ <View style={styles.logMetaRow}>
260
+ <View style={styles.logMetaLeft}>
261
+ <View
262
+ style={[
263
+ styles.actionBadge,
264
+ {
265
+ borderColor: `${actionMeta.color}44`,
266
+ backgroundColor: `${actionMeta.color}14`,
267
+ },
268
+ ]}>
269
+ <Icon name={actionMeta.icon} size={14} color={actionMeta.color} />
270
+ <Text style={[styles.actionText, { color: actionMeta.color }]}>
271
+ {actionMeta.label}
272
+ </Text>
273
+ </View>
274
+
275
+ <Text style={styles.logDate}>{logDate || 'Sem data'}</Text>
276
+ </View>
277
+
278
+ {!!log?.userDisplayName && (
279
+ <Text style={styles.logUser}>{log.userDisplayName}</Text>
280
+ )}
281
+ </View>
282
+
283
+ {!!genericChannel && (
284
+ <Text style={styles.logChannel}>{genericChannel}</Text>
285
+ )}
286
+
287
+ {!!genericMessage && (
288
+ <View style={styles.genericMessageBox}>
289
+ <Text style={styles.genericMessage}>{genericMessage}</Text>
290
+ </View>
291
+ )}
292
+
293
+ {!!contextEntries.length && (
294
+ <View style={styles.contextSection}>
295
+ {contextEntries.map(([field, value]) => (
296
+ <View key={`${log?.id || 'log'}-context-${field}`} style={styles.changeRow}>
297
+ <Text style={styles.changeLabel}>
298
+ {formatLogFieldLabel(field, {
299
+ columns,
300
+ storeName,
301
+ })}
302
+ </Text>
303
+
304
+ {renderChangeValue(styles, '', formatContextValue(value))}
305
+ </View>
306
+ ))}
307
+ </View>
308
+ )}
309
+
310
+ {detailEntries.length ? (
311
+ detailEntries.map(([field, value]) => {
312
+ const isDiffValue =
313
+ Array.isArray(value) &&
314
+ value.length === 2 &&
315
+ Object.prototype.hasOwnProperty.call(value, 0) &&
316
+ Object.prototype.hasOwnProperty.call(value, 1);
317
+
318
+ return (
319
+ <View key={`${log?.id || 'log'}-${field}`} style={styles.changeRow}>
320
+ <Text style={styles.changeLabel}>
321
+ {formatLogFieldLabel(field, {
322
+ columns,
323
+ storeName,
324
+ })}
325
+ </Text>
326
+
327
+ {isDiffValue ? (
328
+ <View style={styles.changeValuesRow}>
329
+ {renderChangeValue(styles, 'Antes', formatLogValue(value[0], {
330
+ columns,
331
+ fieldName: field,
332
+ row: rowContext,
333
+ storeName,
334
+ }))}
335
+ <View style={styles.arrowWrap}>
336
+ <Icon name="east" size={16} color="#94A3B8" />
337
+ </View>
338
+ {renderChangeValue(styles, 'Depois', formatLogValue(value[1], {
339
+ columns,
340
+ fieldName: field,
341
+ row: rowContext,
342
+ storeName,
343
+ }))}
344
+ </View>
345
+ ) : (
346
+ renderChangeValue(
347
+ styles,
348
+ '',
349
+ formatLogValue(value, {
350
+ columns,
351
+ fieldName: field,
352
+ row: rowContext,
353
+ storeName,
354
+ }),
355
+ )
356
+ )}
357
+ </View>
358
+ );
359
+ })
360
+ ) : (
361
+ !genericMessage &&
362
+ !contextEntries.length && (
363
+ <Text style={styles.emptyInlineText}>Sem detalhes adicionais.</Text>
364
+ )
365
+ )}
366
+ </View>
367
+ );
368
+ };
369
+
370
+ const EntityLogBranch = ({
371
+ autoLoad = true,
372
+ entity,
373
+ entityClass,
374
+ entityId,
375
+ entityIri = '',
376
+ entityLabel,
377
+ fetchEntityDetails,
378
+ getEntityState,
379
+ fetchLogs,
380
+ getLogsState,
381
+ onFocusBranch,
382
+ parentOffset = 0,
383
+ relationConfig,
384
+ styles,
385
+ trailKeys,
386
+ }) => {
387
+ const branchKey = useMemo(
388
+ () => buildEntityKey(entityClass, entityId),
389
+ [entityClass, entityId],
390
+ );
391
+ const [expandedRelations, setExpandedRelations] = useState({});
392
+ const [expandedItems, setExpandedItems] = useState({});
393
+ const [branchOffset, setBranchOffset] = useState(parentOffset);
394
+ const [nestedOffsets, setNestedOffsets] = useState({});
395
+ const logsState = getLogsState(entityClass, entityId);
396
+ const detailState = getEntityState(entityClass, entityId, entity);
397
+ const resolvedEntity = useMemo(() => {
398
+ if (!detailState?.item || typeof detailState.item !== 'object') {
399
+ return entity;
400
+ }
401
+
402
+ if (!entity || typeof entity !== 'object') {
403
+ return detailState.item;
404
+ }
405
+
406
+ // Mantem o seed local, mas o detalhe remoto prevalece quando trouxer mais dados.
407
+ return {
408
+ ...entity,
409
+ ...detailState.item,
410
+ };
411
+ }, [detailState?.item, entity]);
412
+ const isNestedBranch = trailKeys.length > 0;
413
+ const storeConfig = useMemo(
414
+ () =>
415
+ resolveStoreConfigByEntity({
416
+ entity: resolvedEntity,
417
+ entityIri,
418
+ }),
419
+ [entityIri, resolvedEntity],
420
+ );
421
+ const storeColumns = storeConfig?.columns || [];
422
+ const storeName = storeConfig?.storeName || '';
423
+
424
+ const branchTitle = useMemo(
425
+ () =>
426
+ entityLabel ||
427
+ buildEntityLabel({
428
+ entity: resolvedEntity,
429
+ className: entityClass,
430
+ id: entityId,
431
+ }),
432
+ [resolvedEntity, entityClass, entityId, entityLabel],
433
+ );
434
+
435
+ const children = useMemo(
436
+ () =>
437
+ buildEntityChildren(resolvedEntity, {
438
+ relationConfig,
439
+ ancestryKeys: [...trailKeys, branchKey],
440
+ }),
441
+ [branchKey, resolvedEntity, relationConfig, trailKeys],
442
+ );
443
+
444
+ useEffect(() => {
445
+ if (!autoLoad || !entityClass || !entityId) {
446
+ return;
447
+ }
448
+
449
+ void fetchLogs({
450
+ className: entityClass,
451
+ rowId: entityId,
452
+ });
453
+ void fetchEntityDetails({
454
+ className: entityClass,
455
+ rowId: entityId,
456
+ entity,
457
+ entityIri,
458
+ });
459
+ }, [
460
+ autoLoad,
461
+ entityClass,
462
+ entityId,
463
+ entity,
464
+ entityIri,
465
+ fetchEntityDetails,
466
+ fetchLogs,
467
+ ]);
468
+
469
+ useEffect(() => {
470
+ if (!isNestedBranch || !branchOffset || typeof onFocusBranch !== 'function') {
471
+ return;
472
+ }
473
+
474
+ // Depois de montar um ramo novo, trazemos o topo dele para a viewport.
475
+ const timeoutId = setTimeout(() => {
476
+ onFocusBranch(Math.max(0, branchOffset - 18));
477
+ }, 120);
478
+
479
+ return () => clearTimeout(timeoutId);
480
+ }, [branchOffset, isNestedBranch, onFocusBranch]);
481
+
482
+ const toggleRelation = useCallback(key => {
483
+ setExpandedRelations(prev => ({
484
+ ...prev,
485
+ [key]: !prev[key],
486
+ }));
487
+ if (typeof onFocusBranch === 'function') {
488
+ setTimeout(() => {
489
+ onFocusBranch(Math.max(0, branchOffset - 18));
490
+ }, 60);
491
+ }
492
+ }, [branchOffset, onFocusBranch]);
493
+
494
+ const toggleItem = useCallback(key => {
495
+ setExpandedItems(prev => ({
496
+ ...prev,
497
+ [key]: !prev[key],
498
+ }));
499
+ if (typeof onFocusBranch === 'function') {
500
+ setTimeout(() => {
501
+ onFocusBranch(Math.max(0, branchOffset - 18));
502
+ }, 60);
503
+ }
504
+ }, [branchOffset, onFocusBranch]);
505
+
506
+ const registerNestedOffset = useCallback((key, layoutY) => {
507
+ setNestedOffsets(prev => {
508
+ const nextOffset = branchOffset + layoutY;
509
+ if (prev[key] === nextOffset) {
510
+ return prev;
511
+ }
512
+
513
+ return {
514
+ ...prev,
515
+ [key]: nextOffset,
516
+ };
517
+ });
518
+ }, [branchOffset]);
519
+
520
+ return (
521
+ <View
522
+ style={styles.branchCard}
523
+ onLayout={event => {
524
+ setBranchOffset(parentOffset + event.nativeEvent.layout.y);
525
+ }}>
526
+ <View style={styles.branchHeader}>
527
+ <View style={styles.branchHeaderRow}>
528
+ <View style={styles.branchTitleWrap}>
529
+ <Text style={styles.branchTitle}>{branchTitle}</Text>
530
+ <Text style={styles.branchMeta}>
531
+ {entityClass?.split('\\').pop()} #{entityId || '--'}
532
+ </Text>
533
+ </View>
534
+ </View>
535
+ </View>
536
+
537
+ <View style={styles.branchBody}>
538
+ <EntitySummaryCard
539
+ columns={storeColumns}
540
+ detailState={detailState}
541
+ entity={resolvedEntity}
542
+ entityClass={entityClass}
543
+ storeName={storeName}
544
+ styles={styles}
545
+ />
546
+
547
+ {!!children.length && (
548
+ <View style={styles.relationSection}>
549
+ <Text style={styles.sectionTitle}>Relacionamentos</Text>
550
+
551
+ {children.map(candidate => (
552
+ <View key={`${branchKey}-${candidate.key}`} style={styles.relationCollectionWrap}>
553
+ <TouchableOpacity
554
+ activeOpacity={0.85}
555
+ onPress={() => toggleRelation(candidate.key)}
556
+ style={styles.relationButton}>
557
+ <View style={styles.relationButtonHeader}>
558
+ <Text style={styles.relationButtonLabel}>
559
+ {candidate.label}
560
+ </Text>
561
+ <Icon
562
+ name={expandedRelations[candidate.key] ? 'expand-less' : 'expand-more'}
563
+ size={20}
564
+ color="#64748B"
565
+ />
566
+ </View>
567
+ <Text style={styles.relationButtonMeta}>
568
+ {candidate.isCollection
569
+ ? `${candidate.items.length} item(ns)`
570
+ : candidate.items[0]?.label || 'Ver historico'}
571
+ </Text>
572
+ </TouchableOpacity>
573
+
574
+ {expandedRelations[candidate.key] && (
575
+ <View
576
+ style={styles.nestedWrap}
577
+ onLayout={event =>
578
+ registerNestedOffset(candidate.key, event.nativeEvent.layout.y)
579
+ }>
580
+ {candidate.items.map(item => (
581
+ <View key={item.key} style={styles.relationCollectionWrap}>
582
+ {candidate.isCollection ? (
583
+ <>
584
+ <TouchableOpacity
585
+ activeOpacity={0.85}
586
+ onPress={() => toggleItem(item.key)}
587
+ style={styles.relationButton}>
588
+ <View style={styles.relationButtonHeader}>
589
+ <Text style={styles.relationButtonLabel}>
590
+ {item.label}
591
+ </Text>
592
+ <Icon
593
+ name={expandedItems[item.key] ? 'expand-less' : 'expand-more'}
594
+ size={20}
595
+ color="#64748B"
596
+ />
597
+ </View>
598
+ <Text style={styles.relationButtonMeta}>
599
+ {item.className?.split('\\').pop()} #{item.id}
600
+ </Text>
601
+ </TouchableOpacity>
602
+
603
+ {expandedItems[item.key] && (
604
+ <View
605
+ style={styles.nestedWrap}
606
+ onLayout={event =>
607
+ registerNestedOffset(item.key, event.nativeEvent.layout.y)
608
+ }>
609
+ <EntityLogBranch
610
+ autoLoad
611
+ entity={item.entity}
612
+ entityClass={item.className}
613
+ entityId={item.id}
614
+ entityIri={item.iri}
615
+ entityLabel={item.label}
616
+ fetchEntityDetails={fetchEntityDetails}
617
+ fetchLogs={fetchLogs}
618
+ getEntityState={getEntityState}
619
+ getLogsState={getLogsState}
620
+ onFocusBranch={onFocusBranch}
621
+ parentOffset={nestedOffsets[item.key] || branchOffset}
622
+ relationConfig={relationConfig}
623
+ styles={styles}
624
+ trailKeys={[...trailKeys, branchKey]}
625
+ />
626
+ </View>
627
+ )}
628
+ </>
629
+ ) : (
630
+ <View
631
+ style={styles.nestedWrap}
632
+ onLayout={event =>
633
+ registerNestedOffset(item.key, event.nativeEvent.layout.y)
634
+ }>
635
+ <EntityLogBranch
636
+ autoLoad
637
+ entity={item.entity}
638
+ entityClass={item.className}
639
+ entityId={item.id}
640
+ entityIri={item.iri}
641
+ entityLabel={item.label}
642
+ fetchEntityDetails={fetchEntityDetails}
643
+ fetchLogs={fetchLogs}
644
+ getEntityState={getEntityState}
645
+ getLogsState={getLogsState}
646
+ onFocusBranch={onFocusBranch}
647
+ parentOffset={nestedOffsets[item.key] || branchOffset}
648
+ relationConfig={relationConfig}
649
+ styles={styles}
650
+ trailKeys={[...trailKeys, branchKey]}
651
+ />
652
+ </View>
653
+ )}
654
+ </View>
655
+ ))}
656
+ </View>
657
+ )}
658
+ </View>
659
+ ))}
660
+ </View>
661
+ )}
662
+
663
+ <View style={styles.relationSection}>
664
+ <Text style={styles.sectionTitle}>Timeline</Text>
665
+
666
+ {logsState.status === 'loading' ? (
667
+ <View style={styles.stateBox}>
668
+ <ActivityIndicator size="small" color="#2563EB" />
669
+ <Text style={styles.stateText}>Carregando historico...</Text>
670
+ </View>
671
+ ) : null}
672
+
673
+ {logsState.status === 'error' ? (
674
+ <View style={styles.stateBox}>
675
+ <Icon name="error-outline" size={20} color="#DC2626" />
676
+ <Text style={styles.stateTitle}>Nao foi possivel carregar</Text>
677
+ <Text style={styles.stateText}>{logsState.error || 'Erro desconhecido'}</Text>
678
+ </View>
679
+ ) : null}
680
+
681
+ {logsState.status === 'success' && !logsState.items.length ? (
682
+ <View style={styles.stateBox}>
683
+ <Icon name="history-toggle-off" size={22} color="#94A3B8" />
684
+ <Text style={styles.stateTitle}>Nenhum log encontrado</Text>
685
+ <Text style={styles.stateText}>
686
+ Ainda nao existem registros para esta entidade.
687
+ </Text>
688
+ </View>
689
+ ) : null}
690
+
691
+ {logsState.status === 'success' && !!logsState.items.length ? (
692
+ <View style={styles.logList}>
693
+ {logsState.items.map(log => (
694
+ <LogCard
695
+ key={log?.id || `${branchKey}-${log?.createdAt}`}
696
+ columns={storeColumns}
697
+ log={log}
698
+ rowContext={resolvedEntity}
699
+ storeName={storeName}
700
+ styles={styles}
701
+ />
702
+ ))}
703
+ </View>
704
+ ) : null}
705
+ </View>
706
+ </View>
707
+ </View>
708
+ );
709
+ };
710
+
711
+ const EntityLogContent = ({
712
+ entity = null,
713
+ entityClass = '',
714
+ entityId = null,
715
+ entityIri = '',
716
+ entityLabel = '',
717
+ relationConfig = {},
718
+ theme,
719
+ }) => {
720
+ const palette = useMemo(() => buildEntityLogPalette(theme), [theme]);
721
+ const styles = useMemo(() => createStyles(palette), [palette]);
722
+ const entityLogStore = useStore('entity_log');
723
+ const entityLogActions = entityLogStore?.actions || {};
724
+ const logCacheRef = useRef(new Map());
725
+ const entityCacheRef = useRef(new Map());
726
+ const [, setCacheVersion] = useState(0);
727
+
728
+ const rootKey = useMemo(
729
+ () => buildEntityKey(entityClass, entityId),
730
+ [entityClass, entityId],
731
+ );
732
+
733
+ useEffect(() => {
734
+ logCacheRef.current = new Map();
735
+ entityCacheRef.current = new Map();
736
+ setCacheVersion(version => version + 1);
737
+ }, [rootKey]);
738
+
739
+ const getLogsState = useCallback((className, rowId, targetEntityIri = '') => {
740
+ const key = buildEntityKey(className, rowId, targetEntityIri);
741
+ return logCacheRef.current.get(key) || {
742
+ error: '',
743
+ items: [],
744
+ status: 'idle',
745
+ totalItems: 0,
746
+ };
747
+ }, []);
748
+
749
+ const getEntityState = useCallback((className, rowId, seedEntity = null, targetEntityIri = '') => {
750
+ const key = buildEntityKey(
751
+ className,
752
+ rowId,
753
+ targetEntityIri || seedEntity?.['@id'] || '',
754
+ );
755
+ return entityCacheRef.current.get(key) || {
756
+ error: '',
757
+ item: seedEntity,
758
+ status: seedEntity ? 'seed' : 'idle',
759
+ };
760
+ }, []);
761
+
762
+ const fetchLogs = useCallback(async ({ className, rowId, entityIri: targetEntityIri = '' }) => {
763
+ const key = buildEntityKey(className, rowId, targetEntityIri);
764
+ if (!key) {
765
+ return;
766
+ }
767
+
768
+ const current = logCacheRef.current.get(key);
769
+ if (current?.status === 'loading' || current?.status === 'success') {
770
+ return;
771
+ }
772
+
773
+ logCacheRef.current.set(key, {
774
+ error: '',
775
+ items: current?.items || [],
776
+ status: 'loading',
777
+ totalItems: current?.totalItems || 0,
778
+ });
779
+ setCacheVersion(version => version + 1);
780
+
781
+ try {
782
+ const response = await entityLogActions.getTimeline({
783
+ ...(className ? { class: className } : {}),
784
+ ...(targetEntityIri ? { entity: targetEntityIri } : {}),
785
+ row: rowId,
786
+ });
787
+ const items = Array.isArray(response?.items) ? response.items : [];
788
+ const totalItems = Number(response?.totalItems || items.length || 0);
789
+
790
+ logCacheRef.current.set(key, {
791
+ error: '',
792
+ items,
793
+ status: 'success',
794
+ totalItems,
795
+ });
796
+ setCacheVersion(version => version + 1);
797
+ } catch (error) {
798
+ logCacheRef.current.set(key, {
799
+ error: error?.message || 'Erro ao buscar logs.',
800
+ items: [],
801
+ status: 'error',
802
+ totalItems: 0,
803
+ });
804
+ setCacheVersion(version => version + 1);
805
+ }
806
+ }, [entityLogActions]);
807
+
808
+ const fetchEntityDetails = useCallback(async ({
809
+ className,
810
+ rowId,
811
+ entity: seedEntity,
812
+ entityIri: explicitEntityIri = '',
813
+ }) => {
814
+ const key = buildEntityKey(
815
+ className,
816
+ rowId,
817
+ explicitEntityIri || seedEntity?.['@id'] || '',
818
+ );
819
+ if (!key) {
820
+ return;
821
+ }
822
+
823
+ const current = entityCacheRef.current.get(key);
824
+ if (current?.status === 'loading' || current?.status === 'success') {
825
+ return;
826
+ }
827
+
828
+ const resolvedEntityIri =
829
+ (typeof seedEntity?.['@id'] === 'string' && seedEntity['@id'].trim()) ||
830
+ (typeof explicitEntityIri === 'string' ? explicitEntityIri.trim() : '');
831
+
832
+ if (!resolvedEntityIri) {
833
+ entityCacheRef.current.set(key, {
834
+ error: 'Entidade sem identificador para consulta.',
835
+ item: seedEntity,
836
+ status: 'error',
837
+ });
838
+ setCacheVersion(version => version + 1);
839
+ return;
840
+ }
841
+
842
+ // O cache evita recarregar o mesmo detalhe toda vez que o usuario expande o ramo.
843
+ entityCacheRef.current.set(key, {
844
+ error: '',
845
+ item: current?.item || seedEntity,
846
+ status: 'loading',
847
+ });
848
+ setCacheVersion(version => version + 1);
849
+
850
+ try {
851
+ const item = await entityLogActions.getEntityDetail(resolvedEntityIri);
852
+
853
+ entityCacheRef.current.set(key, {
854
+ error: '',
855
+ item,
856
+ status: 'success',
857
+ });
858
+ setCacheVersion(version => version + 1);
859
+ } catch (error) {
860
+ entityCacheRef.current.set(key, {
861
+ error: error?.message || 'Erro ao buscar detalhes.',
862
+ item: current?.item || seedEntity,
863
+ status: 'error',
864
+ });
865
+ setCacheVersion(version => version + 1);
866
+ }
867
+ }, [entityLogActions]);
868
+
869
+ const resolvedEntityLabel = useMemo(
870
+ () =>
871
+ entityLabel ||
872
+ buildEntityLabel({
873
+ entity,
874
+ className: entityClass,
875
+ id: entityId,
876
+ }),
877
+ [entity, entityClass, entityId, entityLabel],
878
+ );
879
+
880
+ if (!entityId || (!entityClass && !entityIri)) {
881
+ return null;
882
+ }
883
+
884
+ return (
885
+ <ScrollView
886
+ style={styles.scroll}
887
+ contentContainerStyle={styles.scrollContent}
888
+ nestedScrollEnabled
889
+ showsVerticalScrollIndicator={false}>
890
+ <EntityLogBranch
891
+ autoLoad
892
+ entity={entity}
893
+ entityClass={entityClass}
894
+ entityId={entityId}
895
+ entityIri={entityIri}
896
+ entityLabel={resolvedEntityLabel}
897
+ fetchEntityDetails={fetchEntityDetails}
898
+ fetchLogs={fetchLogs}
899
+ getEntityState={getEntityState}
900
+ getLogsState={getLogsState}
901
+ relationConfig={relationConfig}
902
+ styles={styles}
903
+ trailKeys={[]}
904
+ />
905
+ </ScrollView>
906
+ );
907
+ };
908
+
909
+ export default EntityLogContent;