@controleonline/ui-common 1.2.79 → 1.2.81

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 (91) hide show
  1. package/AGENTS.md +8 -0
  2. package/package.json +9 -3
  3. package/src/api/fetch.js +7 -7
  4. package/src/react/components/AddImportModal.js +6 -2
  5. package/src/react/components/BackgroundRuntimeBridge.js +201 -215
  6. package/src/react/components/ConfirmModal.js +44 -9
  7. package/src/react/components/ConfirmModal.styles.js +55 -41
  8. package/src/react/components/CopyDeviceConfigModal.js +193 -0
  9. package/src/react/components/CopyDeviceConfigModal.styles.js +123 -0
  10. package/src/react/components/DefaultProvider.native.js +22 -0
  11. package/src/react/components/DefaultProvider.web.js +25 -1
  12. package/src/react/components/EntityLogBranch.js +482 -0
  13. package/src/react/components/EntityLogCards.js +369 -0
  14. package/src/react/components/EntityLogContent.js +113 -794
  15. package/src/react/components/LinkedOrderProductsTab.js +3 -3
  16. package/src/react/components/PrintService.js +130 -409
  17. package/src/react/components/RemoteCheckoutService.js +2 -2
  18. package/src/react/components/RuntimeBottomNavigationBar.js +141 -146
  19. package/src/react/components/RuntimeFooterMarqueeText.js +137 -0
  20. package/src/react/components/RuntimeInfoFooter.js +12 -15
  21. package/src/react/components/SystemErrorToast.js +49 -29
  22. package/src/react/components/SystemErrorToast.styles.js +43 -38
  23. package/src/react/components/UserAvatar.js +120 -28
  24. package/src/react/config/deviceConfigBootstrap.js +330 -304
  25. package/src/react/hooks/usePrintSpoolEffects.js +161 -0
  26. package/src/react/pages/DeviceDetailPage.js +6 -2882
  27. package/src/react/pages/Devices/detail/DeviceDetailAlertsCommandsSection.js +233 -0
  28. package/src/react/pages/Devices/detail/DeviceDetailHeader.js +138 -0
  29. package/src/react/pages/Devices/detail/DeviceDetailMovementSections.js +167 -0
  30. package/src/react/pages/Devices/detail/DeviceDetailOrdersPrintSection.js +216 -0
  31. package/src/react/pages/Devices/detail/DeviceDetailPaymentSection.js +40 -0
  32. package/src/react/pages/Devices/detail/DeviceDetailPdvConfigSection.js +390 -0
  33. package/src/react/pages/Devices/detail/DeviceDetailRenderers.js +135 -0
  34. package/src/react/pages/Devices/detail/DeviceDetailScreen.js +493 -0
  35. package/src/react/pages/Devices/detail/OptionButtonChip.js +65 -0
  36. package/src/react/pages/Devices/detail/deviceDetailConstants.js +36 -0
  37. package/src/react/pages/Devices/detail/deviceDetailHelpers.js +70 -0
  38. package/src/react/pages/Devices/detail/useDeviceDetailActions.js +483 -0
  39. package/src/react/pages/Devices/detail/useDeviceDetailCopyConfig.js +87 -0
  40. package/src/react/pages/Devices/detail/useDeviceDetailLoaders.js +327 -0
  41. package/src/react/pages/Devices/detail/useDeviceDetailSaves.js +429 -0
  42. package/src/react/pages/Devices/detail/useDeviceDetailStateA.js +484 -0
  43. package/src/react/pages/Devices/detail/useDeviceDetailStateB.js +71 -0
  44. package/src/react/pages/Devices/deviceTypes/index.js +2 -0
  45. package/src/react/pages/Devices/deviceTypes/pdv.js +11 -0
  46. package/src/react/pages/Imports.js +3 -2
  47. package/src/react/pages/IntegrationConfigFields.js +126 -0
  48. package/src/react/pages/IntegrationConfigPage.js +122 -591
  49. package/src/react/pages/IntegrationConfigPage.utils.js +95 -0
  50. package/src/react/print/usePrintButtonController.js +9 -1
  51. package/src/react/services/Getnet/Checkout.js +81 -0
  52. package/src/react/services/Getnet/Getnet.js +77 -0
  53. package/src/react/services/paymentGatewayExecution.js +20 -0
  54. package/src/react/utils/bottomNavigationToolbar.js +161 -0
  55. package/src/react/utils/commercialDocumentOrders.js +18 -6
  56. package/src/react/utils/copyDeviceConfigs.js +198 -0
  57. package/src/react/utils/fileUrl.js +43 -0
  58. package/src/react/utils/importStatus.js +19 -10
  59. package/src/react/utils/logSettings.js +7 -7
  60. package/src/react/utils/maintenanceSettings.js +16 -0
  61. package/src/react/utils/passwordPolicy.js +73 -0
  62. package/src/react/utils/paymentDevices.js +6 -0
  63. package/src/react/utils/printManagedPrinter.js +45 -0
  64. package/src/react/utils/printRouting.js +63 -0
  65. package/src/react/utils/printSpoolUtils.js +119 -0
  66. package/src/react/utils/printerDevices.js +368 -368
  67. package/src/react/utils/runtimeFooter.js +6 -1
  68. package/src/react/utils/shopFranchises.js +186 -65
  69. package/src/react/utils/systemErrorMessage.js +71 -67
  70. package/src/store/device_config/customActions.js +119 -30
  71. package/src/tests/browser/manager/bottom-navigation-role-toolbar.spec.js +353 -0
  72. package/src/tests/browser/manager/device-detail-alias-save.spec.js +345 -0
  73. package/src/tests/browser/manager/device-detail-copy-config.spec.js +365 -0
  74. package/src/tests/browser/manager/runtime-footer-marquee.spec.js +262 -0
  75. package/src/tests/browser/manager/theme-confirm-toast.spec.js +236 -0
  76. package/src/tests/react/components/RuntimeFooterMarqueeText.test.js +171 -0
  77. package/src/tests/react/components/UserAvatar.test.js +6 -2
  78. package/src/tests/react/components/themeAwareModals.test.mjs +60 -0
  79. package/src/tests/react/config/deviceConfigBootstrap.test.js +104 -66
  80. package/src/tests/react/pages/deviceDetailDelete.test.js +78 -0
  81. package/src/tests/react/pages/receitaFederalIntegrationContract.test.mjs +48 -0
  82. package/src/tests/react/utils/bottomNavigationToolbar.test.js +222 -0
  83. package/src/tests/react/utils/copyDeviceConfigs.test.mjs +127 -0
  84. package/src/tests/react/utils/fileUrl.test.js +38 -0
  85. package/src/tests/react/utils/importStatus.test.js +27 -10
  86. package/src/tests/react/utils/passwordPolicy.test.js +27 -0
  87. package/src/tests/react/utils/printRouting.test.mjs +86 -0
  88. package/src/tests/react/utils/printSpoolUtils.test.mjs +52 -0
  89. package/src/tests/react/utils/runtimeFooter.test.js +10 -8
  90. package/src/tests/react/utils/shopFranchises.test.js +121 -115
  91. package/src/tests/react/utils/systemErrorMessage.test.js +32 -59
@@ -8,706 +8,25 @@ import {
8
8
  View,
9
9
  } from 'react-native';
10
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';
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
22
  import { resolveStoreConfigByEntity } from '@controleonline/ui-common/src/react/utils/storeColumns';
23
23
  import createStyles, {
24
24
  buildEntityLogPalette,
25
25
  } from './EntityLogContent.styles';
26
+ import {
27
+ EntityLogBranch,
28
+ } from './EntityLogBranch';
26
29
 
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
30
  const EntityLogContent = ({
712
31
  entity = null,
713
32
  entityClass = '',
@@ -724,18 +43,18 @@ const EntityLogContent = ({
724
43
  const logCacheRef = useRef(new Map());
725
44
  const entityCacheRef = useRef(new Map());
726
45
  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
-
46
+
47
+ const rootKey = useMemo(
48
+ () => buildEntityKey(entityClass, entityId),
49
+ [entityClass, entityId],
50
+ );
51
+
52
+ useEffect(() => {
53
+ logCacheRef.current = new Map();
54
+ entityCacheRef.current = new Map();
55
+ setCacheVersion(version => version + 1);
56
+ }, [rootKey]);
57
+
739
58
  const getLogsState = useCallback((className, rowId, targetEntityIri = '') => {
740
59
  const key = buildEntityKey(className, rowId, targetEntityIri);
741
60
  return logCacheRef.current.get(key) || {
@@ -764,47 +83,47 @@ const EntityLogContent = ({
764
83
  if (!key) {
765
84
  return;
766
85
  }
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
-
86
+
87
+ const current = logCacheRef.current.get(key);
88
+ if (current?.status === 'loading' || current?.status === 'success') {
89
+ return;
90
+ }
91
+
92
+ logCacheRef.current.set(key, {
93
+ error: '',
94
+ items: current?.items || [],
95
+ status: 'loading',
96
+ totalItems: current?.totalItems || 0,
97
+ });
98
+ setCacheVersion(version => version + 1);
99
+
781
100
  try {
782
101
  const response = await entityLogActions.getTimeline({
783
102
  ...(className ? { class: className } : {}),
784
103
  ...(targetEntityIri ? { entity: targetEntityIri } : {}),
785
104
  row: rowId,
786
105
  });
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
- }
106
+ const items = Array.isArray(response?.items) ? response.items : [];
107
+ const totalItems = Number(response?.totalItems || items.length || 0);
108
+
109
+ logCacheRef.current.set(key, {
110
+ error: '',
111
+ items,
112
+ status: 'success',
113
+ totalItems,
114
+ });
115
+ setCacheVersion(version => version + 1);
116
+ } catch (error) {
117
+ logCacheRef.current.set(key, {
118
+ error: error?.message || 'Erro ao buscar logs.',
119
+ items: [],
120
+ status: 'error',
121
+ totalItems: 0,
122
+ });
123
+ setCacheVersion(version => version + 1);
124
+ }
806
125
  }, [entityLogActions]);
807
-
126
+
808
127
  const fetchEntityDetails = useCallback(async ({
809
128
  className,
810
129
  rowId,
@@ -819,61 +138,61 @@ const EntityLogContent = ({
819
138
  if (!key) {
820
139
  return;
821
140
  }
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
-
141
+
142
+ const current = entityCacheRef.current.get(key);
143
+ if (current?.status === 'loading' || current?.status === 'success') {
144
+ return;
145
+ }
146
+
147
+ const resolvedEntityIri =
148
+ (typeof seedEntity?.['@id'] === 'string' && seedEntity['@id'].trim()) ||
149
+ (typeof explicitEntityIri === 'string' ? explicitEntityIri.trim() : '');
150
+
151
+ if (!resolvedEntityIri) {
152
+ entityCacheRef.current.set(key, {
153
+ error: 'Entidade sem identificador para consulta.',
154
+ item: seedEntity,
155
+ status: 'error',
156
+ });
157
+ setCacheVersion(version => version + 1);
158
+ return;
159
+ }
160
+
161
+ // O cache evita recarregar o mesmo detalhe toda vez que o usuario expande o ramo.
162
+ entityCacheRef.current.set(key, {
163
+ error: '',
164
+ item: current?.item || seedEntity,
165
+ status: 'loading',
166
+ });
167
+ setCacheVersion(version => version + 1);
168
+
169
+ try {
170
+ const item = await entityLogActions.getEntityDetail(resolvedEntityIri);
171
+
172
+ entityCacheRef.current.set(key, {
173
+ error: '',
174
+ item,
175
+ status: 'success',
176
+ });
177
+ setCacheVersion(version => version + 1);
178
+ } catch (error) {
179
+ entityCacheRef.current.set(key, {
180
+ error: error?.message || 'Erro ao buscar detalhes.',
181
+ item: current?.item || seedEntity,
182
+ status: 'error',
183
+ });
184
+ setCacheVersion(version => version + 1);
185
+ }
186
+ }, [entityLogActions]);
187
+
869
188
  const resolvedEntityLabel = useMemo(
870
189
  () =>
871
190
  entityLabel ||
872
- buildEntityLabel({
873
- entity,
874
- className: entityClass,
875
- id: entityId,
876
- }),
191
+ buildEntityLabel({
192
+ entity,
193
+ className: entityClass,
194
+ id: entityId,
195
+ }),
877
196
  [entity, entityClass, entityId, entityLabel],
878
197
  );
879
198
 
@@ -905,5 +224,5 @@ const EntityLogContent = ({
905
224
  </ScrollView>
906
225
  );
907
226
  };
908
-
227
+
909
228
  export default EntityLogContent;