@controleonline/ui-default 1.0.263

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 (56) 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/.scrutinizer.yml +20 -0
  6. package/AGENTS.md +47 -0
  7. package/FUNDING.yml +1 -0
  8. package/package.json +21 -0
  9. package/src/react/components/errors/DefaultErrors.js +360 -0
  10. package/src/react/components/files/DefaultFile.js +46 -0
  11. package/src/react/components/filters/CompactFilterSelector.js +262 -0
  12. package/src/react/components/filters/CompactFilterSelector.styles.js +124 -0
  13. package/src/react/components/filters/DateShortcutFilter.js +264 -0
  14. package/src/react/components/filters/DateShortcutFilter.styles.js +82 -0
  15. package/src/react/components/filters/DefaultColumnFilter.js +97 -0
  16. package/src/react/components/filters/DefaultColumnFilter.styles.js +21 -0
  17. package/src/react/components/filters/DefaultExternalFilters.js +441 -0
  18. package/src/react/components/filters/DefaultExternalFilters.styles.js +177 -0
  19. package/src/react/components/filters/DefaultSearch.js +103 -0
  20. package/src/react/components/filters/DefaultSearch.styles.js +70 -0
  21. package/src/react/components/filters/dateFilterSelection.js +29 -0
  22. package/src/react/components/form/DefaultForm.js +198 -0
  23. package/src/react/components/form/DefaultForm.styles.js +70 -0
  24. package/src/react/components/help/DefaultTooltip.js +87 -0
  25. package/src/react/components/help/DefaultTooltip.styles.js +61 -0
  26. package/src/react/components/inputs/DefaultInput.js +160 -0
  27. package/src/react/components/inputs/DefaultInput.styles.js +93 -0
  28. package/src/react/components/inputs/DefaultSelect.js +192 -0
  29. package/src/react/components/inputs/DefaultSelect.styles.js +65 -0
  30. package/src/react/components/inputs/defaultInputUtils.js +230 -0
  31. package/src/react/components/map/DefaultGoogleMap.styles.js +9 -0
  32. package/src/react/components/map/DefaultGoogleMap.web.js +698 -0
  33. package/src/react/components/map/DefaultMap.native.js +71 -0
  34. package/src/react/components/map/DefaultMap.shared.js +762 -0
  35. package/src/react/components/map/DefaultMap.styles.js +16 -0
  36. package/src/react/components/map/DefaultMap.web.js +66 -0
  37. package/src/react/components/map/DefaultNativeMap.native.js +615 -0
  38. package/src/react/components/map/DefaultNativeMap.shared.js +532 -0
  39. package/src/react/components/map/DefaultNativeMap.styles.js +122 -0
  40. package/src/react/components/table/DefaultTable.js +1897 -0
  41. package/src/react/components/table/DefaultTable.styles.js +610 -0
  42. package/src/react/index.js +10 -0
  43. package/src/react/utils/tableVisibleColumnsPreferences.js +264 -0
  44. package/src/store/default/actions.js +444 -0
  45. package/src/store/default/getters.js +28 -0
  46. package/src/store/default/mutation_types.js +26 -0
  47. package/src/store/default/mutations.js +138 -0
  48. package/src/tests/react/components/DateShortcutFilter.test.js +96 -0
  49. package/src/tests/react/components/errors/DefaultErrors.test.js +162 -0
  50. package/src/tests/react/components/files/DefaultFile.test.js +80 -0
  51. package/src/tests/react/components/map/DefaultMap.shared.test.js +162 -0
  52. package/src/tests/react/filters/dateFilterSelection.test.js +46 -0
  53. package/src/tests/react/inputs/defaultInputUtils.test.js +45 -0
  54. package/src/tests/react/store/defaultActions.test.js +112 -0
  55. package/src/tests/react/utils/tableVisibleColumnsPreferences.test.js +223 -0
  56. package/src/utils/filters.js +56 -0
@@ -0,0 +1,1897 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ FlatList,
4
+ Modal,
5
+ ScrollView,
6
+ Text,
7
+ TouchableOpacity,
8
+ View,
9
+ useWindowDimensions,
10
+ } from 'react-native';
11
+ import { useIsFocused } from '@react-navigation/native';
12
+ import Icon from 'react-native-vector-icons/Feather';
13
+ import { getAllStores, useStore } from '@store';
14
+ import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
15
+ import Formatter from '@controleonline/ui-common/src/utils/formatter.js';
16
+ import StateStore from '@controleonline/ui-layout/src/react/components/StateStore';
17
+ import { getDateRange } from '@controleonline/ui-common/src/react/utils/dateRangeFilter';
18
+ import { formatStoreColumnLabel } from '@controleonline/ui-common/src/react/utils/storeColumns';
19
+ import { resolveThemePalette, withOpacity } from '@controleonline/../../src/styles/branding';
20
+ import { colors } from '@controleonline/../../src/styles/colors';
21
+ import DefaultColumnFilter from '../filters/DefaultColumnFilter';
22
+ import DefaultSearch from '../filters/DefaultSearch';
23
+ import DefaultForm from '../form/DefaultForm';
24
+ import DefaultInput from '../inputs/DefaultInput';
25
+ import {
26
+ formatSaveValue,
27
+ getColumnKey,
28
+ isEditableColumn,
29
+ normalizeId,
30
+ normalizeOptionKey,
31
+ normalizeText,
32
+ resolveStoreNameFromList,
33
+ resolveCellText,
34
+ resolveEditValue,
35
+ } from '../inputs/defaultInputUtils';
36
+ import {
37
+ persistTableSortPreference,
38
+ persistTableViewModePreference,
39
+ persistVisibleColumnsPreference,
40
+ resolveStoredTableSortPreference,
41
+ resolveStoredTableViewModePreference,
42
+ resolveStoredVisibleColumnsPreference,
43
+ sanitizeVisibleColumnsPreference,
44
+ } from '../../utils/tableVisibleColumnsPreferences';
45
+ import styles from './DefaultTable.styles';
46
+
47
+ const DEFAULT_CELL_MIN_WIDTH = 118;
48
+ const DEFAULT_COMPACT_BREAKPOINT = 768;
49
+ const END_REACHED_THRESHOLD = 0.35;
50
+ const IDENTITY_CELL_MIN_WIDTH = 76;
51
+ const MONEY_CELL_MIN_WIDTH = 132;
52
+ const ACTIONS_CELL_WIDTH = 60;
53
+ const SUMMARY_OPERATIONS = ['sum', 'count', 'avg', 'min', 'max'];
54
+ const LIST_OPTIONS_PAGE_SIZE = 100;
55
+
56
+ const shouldIncludeColumn = column =>
57
+ Boolean(getColumnKey(column)) &&
58
+ column?.show !== false &&
59
+ column?.visible !== false &&
60
+ column?.table !== false;
61
+
62
+ const resolveListActionName = list =>
63
+ normalizeText(list).split('/')[1] || 'getItems';
64
+
65
+ const COMPANY_SCOPED_LIST_STORES = new Set([
66
+ 'categories',
67
+ 'paymentType',
68
+ 'wallet',
69
+ ]);
70
+
71
+ const resolveListLoadParams = ({currentCompanyId, listStoreName}) => {
72
+ if (!currentCompanyId || !COMPANY_SCOPED_LIST_STORES.has(listStoreName)) {
73
+ return {};
74
+ }
75
+
76
+ return {people: currentCompanyId};
77
+ };
78
+
79
+ const resolveColumnListLoadParams = ({column, currentCompanyId}) => {
80
+ const listStoreName = resolveStoreNameFromList(column?.list);
81
+ const companyScopedParams = resolveListLoadParams({
82
+ currentCompanyId,
83
+ listStoreName,
84
+ });
85
+ const customParams =
86
+ column?.listRequestParams &&
87
+ typeof column.listRequestParams === 'object' &&
88
+ !Array.isArray(column.listRequestParams)
89
+ ? column.listRequestParams
90
+ : {};
91
+
92
+ return {
93
+ ...companyScopedParams,
94
+ ...customParams,
95
+ };
96
+ };
97
+
98
+ const isObject = value =>
99
+ value !== null && typeof value === 'object' && !Array.isArray(value);
100
+
101
+ const getRowKey = (row, index = 0) =>
102
+ String(row?.['@id'] || row?.id || index);
103
+
104
+ const stableSerialize = value => {
105
+ if (value === null || value === undefined) {
106
+ return 'null';
107
+ }
108
+
109
+ if (Array.isArray(value)) {
110
+ return `[${value.map(item => stableSerialize(item)).join(',')}]`;
111
+ }
112
+
113
+ if (value instanceof Date) {
114
+ return JSON.stringify(value.toISOString());
115
+ }
116
+
117
+ if (isObject(value)) {
118
+ return `{${Object.keys(value)
119
+ .sort()
120
+ .map(key => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
121
+ .join(',')}}`;
122
+ }
123
+
124
+ return JSON.stringify(value);
125
+ };
126
+
127
+ const resolveDateRangeQuery = value => {
128
+ if (!value || typeof value !== 'object') {
129
+ return {};
130
+ }
131
+
132
+ const shortcut = value.shortcut || value.value || 'all';
133
+ const customRange = value.customRange || { from: '', to: '' };
134
+ const dateRange = getDateRange(shortcut, customRange, {
135
+ relativeMode: 'rolling',
136
+ useCurrentMoment: true,
137
+ });
138
+
139
+ return {
140
+ after: dateRange?.after || '',
141
+ before: dateRange?.before || '',
142
+ };
143
+ };
144
+
145
+ const resolveFilterQueryValue = value => {
146
+ if (Array.isArray(value)) {
147
+ return value
148
+ .map(item => resolveFilterQueryValue(item))
149
+ .filter(item => item !== '' && item !== null && item !== undefined);
150
+ }
151
+
152
+ if (value && typeof value === 'object') {
153
+ return resolveFilterQueryValue(
154
+ value.value ?? value.id ?? value['@id'] ?? value.key ?? '',
155
+ );
156
+ }
157
+
158
+ return normalizeText(value);
159
+ };
160
+
161
+ const normalizeCollectionItems = response => {
162
+ if (Array.isArray(response)) return response;
163
+ if (Array.isArray(response?.member)) return response.member;
164
+ if (Array.isArray(response?.['hydra:member'])) return response['hydra:member'];
165
+
166
+ return [];
167
+ };
168
+
169
+ const resolveHasMore = ({ hasMore, dataLength, totalItems }) => {
170
+ if (hasMore !== null && hasMore !== undefined) {
171
+ return hasMore;
172
+ }
173
+
174
+ const resolvedTotalItems = Number(totalItems);
175
+ if (!Number.isFinite(resolvedTotalItems)) {
176
+ return false;
177
+ }
178
+
179
+ return dataLength < resolvedTotalItems;
180
+ };
181
+
182
+ const humanizeSummaryLabel = value =>
183
+ normalizeText(value)
184
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
185
+ .replace(/[._-]+/g, ' ')
186
+ .replace(/\s+/g, ' ')
187
+ .trim();
188
+
189
+ const isMoneySummaryPath = path =>
190
+ /(amount|price|total|value|paid|open|receivable|payable|pending)/i.test(
191
+ Array.isArray(path) ? path.join('.') : normalizeText(path),
192
+ );
193
+
194
+ const normalizeSummaryMoneyValue = value => {
195
+ if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
196
+
197
+ const rawValue = normalizeText(value).replace(/[^0-9,.-]/g, '');
198
+ if (!rawValue) return 0;
199
+
200
+ const normalizedValue = rawValue.includes(',')
201
+ ? rawValue.replace(/\./g, '').replace(',', '.')
202
+ : rawValue;
203
+ const parsedValue = Number(normalizedValue);
204
+
205
+ return Number.isFinite(parsedValue) ? parsedValue : 0;
206
+ };
207
+
208
+ const getSummaryOperations = column => {
209
+ if (typeof column?.summary === 'string') return [column.summary];
210
+ if (Array.isArray(column?.summary)) return column.summary;
211
+ if (isObject(column?.summary)) {
212
+ return Object.entries(column.summary)
213
+ .filter(([, value]) => value)
214
+ .map(([operation]) => operation);
215
+ }
216
+
217
+ return SUMMARY_OPERATIONS.filter(operation => column?.[operation] === true);
218
+ };
219
+
220
+ const getSummaryField = (column, operation) => {
221
+ if (isObject(column?.summary)) {
222
+ const summaryField = column.summary[operation];
223
+ if (typeof summaryField === 'string') return summaryField;
224
+ }
225
+
226
+ return getColumnKey(column);
227
+ };
228
+
229
+ const formatSummaryValue = ({ column, columns, path, storeName, value }) => {
230
+ const columnKey = column ? getColumnKey(column) : '';
231
+ const shouldFormatMoney = isMoneySummaryPath(path) || isMoneySummaryPath(columnKey);
232
+
233
+ if (shouldFormatMoney) {
234
+ return Formatter.formatMoney(normalizeSummaryMoneyValue(value));
235
+ }
236
+
237
+ if (column) {
238
+ return resolveCellText({
239
+ column,
240
+ columns,
241
+ row: { [columnKey]: value },
242
+ storeName,
243
+ value,
244
+ });
245
+ }
246
+
247
+ return normalizeText(value);
248
+ };
249
+
250
+ const flattenSummaryEntries = ({
251
+ path = [],
252
+ summaryLabels = {},
253
+ usedPaths,
254
+ value,
255
+ }) => {
256
+ if (!isObject(value)) {
257
+ const pathKey = path.join('.');
258
+ if (!pathKey || usedPaths.has(pathKey)) return [];
259
+
260
+ const fallbackLabel = humanizeSummaryLabel(path[path.length - 1] || pathKey);
261
+
262
+ return [{
263
+ key: pathKey,
264
+ label: summaryLabels[pathKey] || summaryLabels[path[path.length - 1]] || fallbackLabel,
265
+ path,
266
+ value,
267
+ }];
268
+ }
269
+
270
+ return Object.entries(value).flatMap(([key, childValue]) =>
271
+ flattenSummaryEntries({
272
+ path: [...path, key],
273
+ summaryLabels,
274
+ usedPaths,
275
+ value: childValue,
276
+ }),
277
+ );
278
+ };
279
+
280
+ const isSortableColumn = column => column?.sortable === true;
281
+
282
+ const getSortField = column => column?.sortField || getColumnKey(column);
283
+
284
+ const resolveDefaultSort = columns => {
285
+ if (!Array.isArray(columns)) {
286
+ return null;
287
+ }
288
+
289
+ for (const column of columns) {
290
+ if (!column || column?.defaultSort === undefined || column?.defaultSort === null || column?.defaultSort === false) {
291
+ continue;
292
+ }
293
+
294
+ const field = getSortField(column);
295
+ const defaultSort = column.defaultSort;
296
+
297
+ if (defaultSort && typeof defaultSort === 'object' && !Array.isArray(defaultSort)) {
298
+ const resolvedDirection = normalizeText(defaultSort.direction || defaultSort.order || 'desc').toLowerCase();
299
+ const resolvedField = normalizeText(
300
+ defaultSort.field || defaultSort.sortField || defaultSort.key || defaultSort.name || field,
301
+ );
302
+
303
+ return {
304
+ direction: resolvedDirection === 'asc' ? 'asc' : 'desc',
305
+ field: resolvedField || field,
306
+ };
307
+ }
308
+
309
+ if (typeof defaultSort === 'string') {
310
+ const normalizedSort = normalizeText(defaultSort).toLowerCase();
311
+ if (normalizedSort === 'asc' || normalizedSort === 'desc') {
312
+ return {
313
+ direction: normalizedSort,
314
+ field,
315
+ };
316
+ }
317
+
318
+ return {
319
+ direction: 'desc',
320
+ field: normalizeText(defaultSort) || field,
321
+ };
322
+ }
323
+
324
+ if (defaultSort === true) {
325
+ return {
326
+ direction: 'asc',
327
+ field,
328
+ };
329
+ }
330
+ }
331
+
332
+ return null;
333
+ };
334
+
335
+ const sanitizeStoredSortPreference = ({
336
+ columns = [],
337
+ fallbackSort = null,
338
+ sort = null,
339
+ }) => {
340
+ const normalizedFallback =
341
+ fallbackSort &&
342
+ typeof fallbackSort === 'object' &&
343
+ typeof fallbackSort.field === 'string' &&
344
+ fallbackSort.field.trim()
345
+ ? {
346
+ direction: fallbackSort.direction === 'asc' ? 'asc' : 'desc',
347
+ field: fallbackSort.field.trim(),
348
+ }
349
+ : null;
350
+
351
+ if (
352
+ !sort ||
353
+ typeof sort !== 'object' ||
354
+ typeof sort.field !== 'string' ||
355
+ !sort.field.trim()
356
+ ) {
357
+ return normalizedFallback;
358
+ }
359
+
360
+ const normalizedDirection = sort.direction === 'asc' ? 'asc' : 'desc';
361
+ const normalizedField = sort.field.trim();
362
+ const sortableFields = new Set(
363
+ (Array.isArray(columns) ? columns : [])
364
+ .filter(isSortableColumn)
365
+ .map(column => getSortField(column))
366
+ .filter(Boolean),
367
+ );
368
+
369
+ if (!sortableFields.has(normalizedField)) {
370
+ return normalizedFallback;
371
+ }
372
+
373
+ return {
374
+ direction: normalizedDirection,
375
+ field: normalizedField,
376
+ };
377
+ };
378
+
379
+ const readValueByPath = (object, path) => {
380
+ if (!object || !path) return object;
381
+
382
+ return String(path)
383
+ .split('.')
384
+ .reduce((currentValue, key) => {
385
+ if (currentValue === null || currentValue === undefined) return currentValue;
386
+ return currentValue?.[key];
387
+ }, object);
388
+ };
389
+
390
+ const normalizeSortText = value =>
391
+ normalizeText(value)
392
+ .normalize('NFD')
393
+ .replace(/[\u0300-\u036f]/g, '')
394
+ .toLowerCase();
395
+
396
+ const isDateLikeColumn = column =>
397
+ column?.inputType === 'date' ||
398
+ column?.inputType === 'date-range' ||
399
+ column?.type === 'date' ||
400
+ column?.type === 'range-date' ||
401
+ /date/i.test(getColumnKey(column));
402
+
403
+ const resolveSortComparable = ({ column, row, storeName, columns }) => {
404
+ const fieldName = getColumnKey(column);
405
+ const sortField = getSortField(column);
406
+ const rawValue = sortField === fieldName ? row?.[fieldName] : readValueByPath(row, sortField);
407
+
408
+ if (isDateLikeColumn(column)) {
409
+ const dateValue =
410
+ rawValue && typeof rawValue === 'object'
411
+ ? rawValue?.value ??
412
+ rawValue?.date ??
413
+ rawValue?.createdAt ??
414
+ rawValue?.updatedAt ??
415
+ rawValue?.['@id'] ??
416
+ rawValue?.[fieldName] ??
417
+ rawValue
418
+ : rawValue;
419
+ const parsedDate = Date.parse(dateValue);
420
+ return Number.isFinite(parsedDate) ? parsedDate : Number.NEGATIVE_INFINITY;
421
+ }
422
+
423
+ const resolvedValue = sortField === fieldName
424
+ ? resolveCellText({
425
+ column,
426
+ columns,
427
+ row,
428
+ storeName,
429
+ })
430
+ : normalizeText(rawValue ?? resolveCellText({
431
+ column,
432
+ columns,
433
+ row,
434
+ storeName,
435
+ }));
436
+
437
+ const normalizedNumber = Number(
438
+ String(resolvedValue).replace(/[^0-9,.-]/g, '').replace(',', '.'),
439
+ );
440
+
441
+ if (Number.isFinite(normalizedNumber) && String(resolvedValue).match(/[0-9]/)) {
442
+ return normalizedNumber;
443
+ }
444
+
445
+ return normalizeSortText(resolvedValue);
446
+ };
447
+
448
+ const getColumnStyle = column => {
449
+ const key = getColumnKey(column);
450
+ if (column?.isIdentity) return [styles.cell, styles.identityCell];
451
+ if (['price', 'total', 'amount', 'value'].includes(key)) {
452
+ return [styles.cell, styles.moneyCell];
453
+ }
454
+ return styles.cell;
455
+ };
456
+
457
+ const getColumnMinWidth = column => {
458
+ const key = getColumnKey(column);
459
+ if (column?.isIdentity) return IDENTITY_CELL_MIN_WIDTH;
460
+ if (['price', 'total', 'amount', 'value'].includes(key)) {
461
+ return MONEY_CELL_MIN_WIDTH;
462
+ }
463
+ return DEFAULT_CELL_MIN_WIDTH;
464
+ };
465
+
466
+ const DefaultTable = ({
467
+ accentColor = null,
468
+ actions = {},
469
+ add = null,
470
+ compactBreakpoint = DEFAULT_COMPACT_BREAKPOINT,
471
+ columns = [],
472
+ data = undefined,
473
+ filters = {},
474
+ forceCardsOnCompact = true,
475
+ getOptionsForColumn = null,
476
+ hasMore = null,
477
+ initialViewMode = 'table',
478
+ pageSize = null,
479
+ isLoading = false,
480
+ onEditRow = null,
481
+ onEndReached = null,
482
+ onMomentumScrollBegin = null,
483
+ onScrollBeginDrag = null,
484
+ onAdd = null,
485
+ onFilterChange = null,
486
+ onRowPress = null,
487
+ onSaved = null,
488
+ onSortChange = null,
489
+ onDataLoaded = null,
490
+ requestParams = {},
491
+ renderCard = null,
492
+ searchProps = null,
493
+ toolbarActions = [],
494
+ showColumnFiltersButton = true,
495
+ showTotalItemsInFooter = true,
496
+ showTotalItemsInCompactToolbar = false,
497
+ showRowActions = true,
498
+ sort = null,
499
+ storeName = '',
500
+ summary = null,
501
+ summaryLabels = null,
502
+ totalItems = null,
503
+ totalItemsLabel = null,
504
+ totalItemsText = null,
505
+ visibleColumnsPreferenceKey = '',
506
+ }) => {
507
+ const { width } = useWindowDimensions();
508
+ const store = useStore(storeName);
509
+ const peopleStore = useStore('people');
510
+ const themeStore = useStore('theme');
511
+ const [editingCell, setEditingCell] = useState(null);
512
+ const [editingRow, setEditingRow] = useState(null);
513
+ const [formMode, setFormMode] = useState('edit');
514
+ const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
515
+ const [, setListOptionsVersion] = useState(0);
516
+ const [savingCell, setSavingCell] = useState(null);
517
+ const [showColumnFilters, setShowColumnFilters] = useState(false);
518
+ const [tableContainerWidth, setTableContainerWidth] = useState(0);
519
+ const endReachedLockRef = useRef(false);
520
+ const loadedListStoresRef = useRef(new Set());
521
+ const listOptionsWasFocusedRef = useRef(false);
522
+ const previousPaginationStateRef = useRef({
523
+ dataLength: Array.isArray(data) ? data.length : 0,
524
+ filtersKey: JSON.stringify(filters || {}),
525
+ sortDirection: sort?.direction,
526
+ sortField: sort?.field,
527
+ });
528
+ const visibleColumnsSeed = useMemo(
529
+ () =>
530
+ sanitizeVisibleColumnsPreference({
531
+ columns: Array.isArray(store?.getters?.columns) && store.getters.columns.length > 0
532
+ ? store.getters.columns
533
+ : columns,
534
+ visibleColumns: resolveStoredVisibleColumnsPreference(
535
+ visibleColumnsPreferenceKey,
536
+ ),
537
+ }),
538
+ [columns, store?.getters?.columns, visibleColumnsPreferenceKey],
539
+ );
540
+ const viewModeSeed = useMemo(
541
+ () =>
542
+ resolveStoredTableViewModePreference(
543
+ visibleColumnsPreferenceKey,
544
+ initialViewMode,
545
+ ),
546
+ [initialViewMode, visibleColumnsPreferenceKey],
547
+ );
548
+ const [visibleColumns, setVisibleColumns] = useState(() => visibleColumnsSeed);
549
+ const [viewMode, setViewMode] = useState(() => viewModeSeed);
550
+ const { currentCompany } = peopleStore.getters || {};
551
+ const { colors: themeColors } = themeStore.getters || {};
552
+ const themeTokens = useMemo(
553
+ () => ({...themeColors, ...(currentCompany?.theme?.colors || {})}),
554
+ [currentCompany?.theme?.colors, themeColors],
555
+ );
556
+ const palette = useMemo(
557
+ () => resolveThemePalette(themeTokens, colors),
558
+ [themeTokens],
559
+ );
560
+ const resolvedAccentColor = accentColor || palette.primary;
561
+ const tableHeaderColor = themeTokens['bg-headers-light'] || resolvedAccentColor;
562
+ const tableEvenColor = themeTokens.listItemEvenRow || themeTokens['bg-even-light'] || palette.background;
563
+ const tableOddColor = themeTokens.listItemOddRow || themeTokens['bg-odd-light'] || palette.background;
564
+ const tableBorderColor = palette.border;
565
+ const tableSurfaceColor = palette.background;
566
+ const tableTextColor = palette.text;
567
+ const tableMutedColor = palette.textSecondary;
568
+ const tableOnAccentColor = palette.secondary || palette.text;
569
+ const isFocused = useIsFocused();
570
+ const {showError} = useMessage() || {};
571
+ const autoMode = data === undefined && normalizeText(storeName) !== '';
572
+ const searchKey = searchProps?.searchKey || 'search';
573
+ const storeActions = store?.actions || {};
574
+ const storeColumns = Array.isArray(store?.getters?.columns) ? store.getters.columns : [];
575
+ const columnsForTable = storeColumns.length > 0 ? storeColumns : columns;
576
+ const listColumnsSignature = useMemo(
577
+ () =>
578
+ columnsForTable
579
+ .filter(column => normalizeText(column?.list))
580
+ .map(column => `${getColumnKey(column)}:${normalizeText(column.list)}`)
581
+ .join('|'),
582
+ [columnsForTable],
583
+ );
584
+ const storeFilters = isObject(store?.getters?.filters) ? store.getters.filters : {};
585
+ const requestParamsSeed = isObject(requestParams) ? requestParams : {};
586
+ const initialFiltersSeed = autoMode
587
+ ? (Object.keys(filters || {}).length > 0 ? filters : storeFilters)
588
+ : (isObject(filters) ? filters : {});
589
+ const [autoFilters, setAutoFilters] = useState(() => initialFiltersSeed);
590
+ const defaultSortSeed = useMemo(
591
+ () => resolveDefaultSort(columnsForTable),
592
+ [columnsForTable],
593
+ );
594
+ const storedSortSeed = useMemo(
595
+ () =>
596
+ sanitizeStoredSortPreference({
597
+ columns: columnsForTable,
598
+ fallbackSort: sort || defaultSortSeed || null,
599
+ sort: resolveStoredTableSortPreference(visibleColumnsPreferenceKey),
600
+ }),
601
+ [columnsForTable, defaultSortSeed, sort, visibleColumnsPreferenceKey],
602
+ );
603
+ const [autoSort, setAutoSort] = useState(() => storedSortSeed);
604
+ const [autoHasLoaded, setAutoHasLoaded] = useState(false);
605
+ const [autoLoading, setAutoLoading] = useState(false);
606
+ const [autoLoadingMore, setAutoLoadingMore] = useState(false);
607
+ const [autoLastPageCount, setAutoLastPageCount] = useState(0);
608
+ const autoRequestIdRef = useRef(0);
609
+ const autoLoadedQueryKeyRef = useRef('');
610
+ const autoErroredQueryKeyRef = useRef('');
611
+ const autoPageRef = useRef(0);
612
+ const pageSizeNumber = Number(requestParamsSeed.itemsPerPage || pageSize || 50) || 50;
613
+ const resolvedSort = autoMode ? autoSort : sort;
614
+ const resolvedFilters = autoMode ? autoFilters : (filters || {});
615
+ const resolvedSearchValue = normalizeText(resolvedFilters?.[searchKey]);
616
+
617
+ useEffect(() => {
618
+ const shouldRefreshCompanyScopedLists =
619
+ isFocused && !listOptionsWasFocusedRef.current;
620
+
621
+ listOptionsWasFocusedRef.current = isFocused;
622
+
623
+ if (!isFocused || !listColumnsSignature) return;
624
+
625
+ const stores = getAllStores?.() || {};
626
+ const loadPromises = [];
627
+
628
+ columnsForTable.forEach(column => {
629
+ if (!column?.list) return;
630
+
631
+ const explicitOptions = getOptionsForColumn?.(column);
632
+ if (Array.isArray(explicitOptions) && explicitOptions.length > 0) return;
633
+
634
+ const listStoreName = resolveStoreNameFromList(column.list);
635
+ const actionName = resolveListActionName(column.list);
636
+ const listStore = stores?.[listStoreName];
637
+ const listAction = listStore?.actions?.[actionName];
638
+
639
+ const listLoadParams = resolveColumnListLoadParams({
640
+ column,
641
+ currentCompanyId: currentCompany?.id,
642
+ });
643
+ const isCompanyScopedList = Object.keys(listLoadParams).length > 0;
644
+
645
+ if (!listStoreName || typeof listAction !== 'function') return;
646
+ if (
647
+ !isCompanyScopedList &&
648
+ Array.isArray(listStore?.getters?.items) &&
649
+ listStore.getters.items.length > 0
650
+ ) {
651
+ return;
652
+ }
653
+
654
+ const loadKey = `${listStoreName}:${actionName}:${stableSerialize(listLoadParams)}`;
655
+ if (shouldRefreshCompanyScopedLists && isCompanyScopedList) {
656
+ loadedListStoresRef.current.delete(loadKey);
657
+ }
658
+ if (loadedListStoresRef.current.has(loadKey)) return;
659
+
660
+ loadedListStoresRef.current.add(loadKey);
661
+ loadPromises.push(
662
+ Promise.resolve(
663
+ listAction({
664
+ ...listLoadParams,
665
+ itemsPerPage: LIST_OPTIONS_PAGE_SIZE,
666
+ __storeMeta: {
667
+ dedupeKey: `default-table-list-options:${loadKey}`,
668
+ skipSystemError: true,
669
+ },
670
+ }),
671
+ ).catch(() => {
672
+ loadedListStoresRef.current.delete(loadKey);
673
+ }),
674
+ );
675
+ });
676
+
677
+ if (loadPromises.length === 0) return;
678
+
679
+ let cancelled = false;
680
+ Promise.allSettled(loadPromises).then(() => {
681
+ if (!cancelled) {
682
+ setListOptionsVersion(version => version + 1);
683
+ }
684
+ });
685
+
686
+ return () => {
687
+ cancelled = true;
688
+ };
689
+ }, [columnsForTable, currentCompany?.id, getOptionsForColumn, isFocused, listColumnsSignature]);
690
+
691
+ const buildRequestQuery = useCallback(
692
+ (page, append = false) => {
693
+ const query = {
694
+ ...requestParamsSeed,
695
+ itemsPerPage: pageSizeNumber,
696
+ page,
697
+ };
698
+
699
+ if (autoSort?.field && autoSort?.direction) {
700
+ query[`order[${autoSort.field}]`] = autoSort.direction;
701
+ }
702
+
703
+ Object.entries(autoFilters || {}).forEach(([fieldName, value]) => {
704
+ if (!fieldName) return;
705
+
706
+ if (fieldName === searchKey) {
707
+ const normalizedSearch = normalizeText(value).replace(/^#/, '');
708
+ if (normalizedSearch) {
709
+ query.search = normalizedSearch;
710
+ if (searchKey !== 'search') {
711
+ query[searchKey] = normalizedSearch;
712
+ }
713
+ }
714
+ return;
715
+ }
716
+
717
+ const column = columnsForTable.find(item => getColumnKey(item) === fieldName);
718
+
719
+ if (column?.inputType === 'date-range' || column?.type === 'range-date') {
720
+ const dateRange = resolveDateRangeQuery(value);
721
+ if (dateRange.after) {
722
+ query[`${fieldName}[after]`] = dateRange.after;
723
+ }
724
+ if (dateRange.before) {
725
+ query[`${fieldName}[before]`] = dateRange.before;
726
+ }
727
+ return;
728
+ }
729
+
730
+ const normalizedValue = resolveFilterQueryValue(value);
731
+ if (Array.isArray(normalizedValue)) {
732
+ if (normalizedValue.length > 0) {
733
+ query[fieldName] = normalizedValue;
734
+ }
735
+ return;
736
+ }
737
+
738
+ if (normalizedValue !== '') {
739
+ query[fieldName] = normalizedValue;
740
+ }
741
+ });
742
+
743
+ if (append) {
744
+ query.append = true;
745
+ }
746
+
747
+ return query;
748
+ },
749
+ [autoFilters, autoSort?.direction, autoSort?.field, columnsForTable, pageSizeNumber, requestParamsSeed, searchKey],
750
+ );
751
+ const autoQuerySignature = useMemo(
752
+ () =>
753
+ stableSerialize({
754
+ filters: autoFilters,
755
+ pageSize: pageSizeNumber,
756
+ requestParams: requestParamsSeed,
757
+ searchKey,
758
+ sort: autoSort,
759
+ }),
760
+ [autoFilters, autoSort, pageSizeNumber, requestParamsSeed, searchKey],
761
+ );
762
+
763
+ const loadAutoPage = useCallback(
764
+ (page, { append = false } = {}) => {
765
+ if (!autoMode || typeof storeActions.getItems !== 'function') {
766
+ return Promise.resolve([]);
767
+ }
768
+
769
+ const requestId = autoRequestIdRef.current + 1;
770
+ autoRequestIdRef.current = requestId;
771
+
772
+ if (append) {
773
+ setAutoLoadingMore(true);
774
+ } else {
775
+ setAutoLoading(true);
776
+ setAutoHasLoaded(false);
777
+ setAutoLastPageCount(0);
778
+ autoPageRef.current = 0;
779
+ }
780
+
781
+ const query = buildRequestQuery(page, append);
782
+
783
+ return Promise.resolve(storeActions.getItems(query))
784
+ .then(response => {
785
+ if (autoRequestIdRef.current !== requestId) {
786
+ return response;
787
+ }
788
+
789
+ const pageItems = normalizeCollectionItems(response);
790
+ autoPageRef.current = page;
791
+ autoLoadedQueryKeyRef.current = autoQuerySignature;
792
+ autoErroredQueryKeyRef.current = '';
793
+ setAutoHasLoaded(true);
794
+ setAutoLastPageCount(pageItems.length);
795
+ return pageItems;
796
+ })
797
+ .catch(error => {
798
+ if (autoRequestIdRef.current === requestId) {
799
+ autoErroredQueryKeyRef.current = autoQuerySignature;
800
+ showError?.(error?.message || 'Nao foi possivel carregar os registros.');
801
+ }
802
+
803
+ return [];
804
+ })
805
+ .finally(() => {
806
+ if (autoRequestIdRef.current === requestId) {
807
+ setAutoLoading(false);
808
+ setAutoLoadingMore(false);
809
+ endReachedLockRef.current = false;
810
+ }
811
+ });
812
+ },
813
+ [autoMode, autoQuerySignature, buildRequestQuery, showError, storeActions],
814
+ );
815
+
816
+ const availableColumns = useMemo(
817
+ () => columnsForTable.filter(column => shouldIncludeColumn(column)),
818
+ [columnsForTable],
819
+ );
820
+
821
+ useEffect(() => {
822
+ setVisibleColumns(prev =>
823
+ stableSerialize(prev) === stableSerialize(visibleColumnsSeed)
824
+ ? prev
825
+ : visibleColumnsSeed
826
+ );
827
+ }, [visibleColumnsSeed]);
828
+
829
+ useEffect(() => {
830
+ setViewMode(viewModeSeed);
831
+ }, [viewModeSeed]);
832
+
833
+ useEffect(() => {
834
+ if (!autoMode) return;
835
+
836
+ setAutoSort(prev =>
837
+ stableSerialize(prev) === stableSerialize(storedSortSeed)
838
+ ? prev
839
+ : storedSortSeed,
840
+ );
841
+ }, [autoMode, storedSortSeed]);
842
+
843
+ useEffect(() => {
844
+ if (!autoMode) return;
845
+
846
+ setAutoFilters(prev => {
847
+ if (stableSerialize(prev) === stableSerialize(storeFilters)) {
848
+ return prev;
849
+ }
850
+
851
+ return storeFilters;
852
+ });
853
+ }, [autoMode, storeFilters]);
854
+
855
+ useEffect(() => {
856
+ if (!autoMode || !isFocused) return;
857
+ if (typeof storeActions.getItems !== 'function') return;
858
+ if (
859
+ autoLoading ||
860
+ autoLoadingMore ||
861
+ autoLoadedQueryKeyRef.current === autoQuerySignature ||
862
+ autoErroredQueryKeyRef.current === autoQuerySignature
863
+ ) {
864
+ return;
865
+ }
866
+
867
+ endReachedLockRef.current = false;
868
+ loadAutoPage(1, { append: false });
869
+ }, [
870
+ autoLoading,
871
+ autoLoadingMore,
872
+ autoMode,
873
+ autoQuerySignature,
874
+ isFocused,
875
+ loadAutoPage,
876
+ storeActions,
877
+ ]);
878
+
879
+ const tableColumns = useMemo(
880
+ () => columnsForTable.filter(column => shouldIncludeColumn(column) && visibleColumns[getColumnKey(column)] !== false),
881
+ [columnsForTable, visibleColumns],
882
+ );
883
+
884
+ const editableColumns = useMemo(
885
+ () => tableColumns.filter(isEditableColumn),
886
+ [tableColumns],
887
+ );
888
+
889
+ const activeFilterCount = useMemo(
890
+ () => Object.values(resolvedFilters || {}).filter(value => normalizeText(value) !== '').length,
891
+ [resolvedFilters],
892
+ );
893
+ const hasRowActions =
894
+ showRowActions !== false &&
895
+ (editableColumns.length > 0 || typeof onEditRow === 'function');
896
+ const storeAddConfig = store?.getters?.add;
897
+ const addConfig = add !== null && add !== undefined ? add : storeAddConfig;
898
+ const hasAddInstruction = addConfig === true || (isObject(addConfig) && addConfig.enabled !== false);
899
+ const shouldRenderAddButton =
900
+ hasAddInstruction &&
901
+ (typeof onAdd === 'function' || typeof actions.save === 'function');
902
+ const resolvedIsLoading = autoMode ? (autoLoading || autoLoadingMore) : Boolean(isLoading);
903
+ const resolvedData = autoMode
904
+ ? (autoHasLoaded && Array.isArray(store?.getters?.items) ? store.getters.items : [])
905
+ : (Array.isArray(data) ? data : []);
906
+ const tableMinimumWidth = useMemo(
907
+ () =>
908
+ tableColumns.reduce(
909
+ (totalWidth, column) => totalWidth + getColumnMinWidth(column),
910
+ hasRowActions ? ACTIONS_CELL_WIDTH : 0,
911
+ ),
912
+ [hasRowActions, tableColumns],
913
+ );
914
+ const tableWidth = Math.max(tableContainerWidth, tableMinimumWidth);
915
+ const tableLayoutStyle = useMemo(
916
+ () => (tableWidth > 0 ? { minWidth: tableWidth, width: tableWidth } : null),
917
+ [tableWidth],
918
+ );
919
+ const isCompactView = width > 0 && width <= compactBreakpoint;
920
+ const shouldForceCardsOnCompact = forceCardsOnCompact !== false;
921
+ const effectiveViewMode = isCompactView && shouldForceCardsOnCompact ? 'cards' : viewMode;
922
+ const emptyStateLabel = resolvedIsLoading
923
+ ? global.t?.t(storeName, 'label', 'loading') || 'Carregando...'
924
+ : 'Nenhum registro encontrado';
925
+ const storeTotalItems = store?.getters?.totalItems;
926
+ const resolvedTotalItems = totalItems !== null && totalItems !== undefined
927
+ ? totalItems
928
+ : storeTotalItems;
929
+ const totalItemsNumber = Number(resolvedTotalItems);
930
+ const shouldRenderTotalItems =
931
+ resolvedTotalItems !== null &&
932
+ resolvedTotalItems !== undefined &&
933
+ Number.isFinite(totalItemsNumber);
934
+ const shouldRenderFooterTotalItems =
935
+ shouldRenderTotalItems &&
936
+ showTotalItemsInFooter !== false;
937
+ const shouldRenderCompactToolbarTotalItems =
938
+ showTotalItemsInCompactToolbar === true &&
939
+ isCompactView &&
940
+ shouldRenderTotalItems;
941
+ const resolvedTotalItemsText =
942
+ normalizeText(totalItemsText) !== ''
943
+ ? totalItemsText
944
+ : shouldRenderTotalItems
945
+ ? `${totalItemsNumber} ${totalItemsLabel || global.t?.t(storeName, 'label', 'items') || 'registros'}`
946
+ : '';
947
+ const storeSummary = store?.getters?.summary;
948
+ const resolvedSummary = summary !== null && summary !== undefined ? summary : storeSummary;
949
+ const shouldReadSummary = resolvedSummary !== false && isObject(resolvedSummary);
950
+ const summaryEntries = useMemo(() => {
951
+ if (!shouldReadSummary) return [];
952
+
953
+ const labels = summaryLabels || {};
954
+ const usedPaths = new Set();
955
+ const columnEntries = tableColumns.flatMap(column => {
956
+ const operations = getSummaryOperations(column);
957
+ if (!operations.length) return [];
958
+
959
+ const columnLabel = formatStoreColumnLabel({
960
+ columns: columnsForTable,
961
+ fieldName: getColumnKey(column),
962
+ fallbackLabel: column?.label || getColumnKey(column),
963
+ storeName,
964
+ });
965
+
966
+ return operations.map(operation => {
967
+ const fieldName = getSummaryField(column, operation);
968
+ const path = [operation, fieldName];
969
+ const pathKey = path.join('.');
970
+ const value = resolvedSummary?.[operation]?.[fieldName];
971
+ if (value === undefined) return null;
972
+
973
+ usedPaths.add(pathKey);
974
+
975
+ return {
976
+ key: pathKey,
977
+ label: labels[pathKey] || (operations.length > 1 ? `${columnLabel} ${operation}` : columnLabel),
978
+ path,
979
+ value,
980
+ column,
981
+ };
982
+ }).filter(Boolean);
983
+ });
984
+
985
+ const genericEntries = flattenSummaryEntries({
986
+ summaryLabels: labels,
987
+ usedPaths,
988
+ value: resolvedSummary,
989
+ });
990
+
991
+ return [...columnEntries, ...genericEntries].filter(entry =>
992
+ entry?.value !== undefined &&
993
+ entry?.value !== null &&
994
+ normalizeText(entry.value) !== '',
995
+ );
996
+ }, [columnsForTable, resolvedSummary, shouldReadSummary, storeName, summaryLabels, tableColumns]);
997
+ const shouldRenderFooterBar =
998
+ (shouldRenderFooterTotalItems && !shouldRenderCompactToolbarTotalItems) ||
999
+ summaryEntries.length > 0;
1000
+
1001
+ const sortedData = useMemo(() => {
1002
+ const items = Array.isArray(resolvedData) ? [...resolvedData] : [];
1003
+ const sortField = resolvedSort?.field;
1004
+ const sortDirection = resolvedSort?.direction === 'desc' ? 'desc' : 'asc';
1005
+ const sortColumn = tableColumns.find(column => getSortField(column) === sortField);
1006
+
1007
+ if (!sortField || !sortColumn) return items;
1008
+
1009
+ return items.sort((left, right) => {
1010
+ const leftValue = resolveSortComparable({
1011
+ column: sortColumn,
1012
+ columns: tableColumns,
1013
+ row: left,
1014
+ storeName,
1015
+ });
1016
+ const rightValue = resolveSortComparable({
1017
+ column: sortColumn,
1018
+ columns: tableColumns,
1019
+ row: right,
1020
+ storeName,
1021
+ });
1022
+
1023
+ const comparison =
1024
+ typeof leftValue === 'number' && typeof rightValue === 'number'
1025
+ ? leftValue - rightValue
1026
+ : normalizeSortText(leftValue).localeCompare(normalizeSortText(rightValue));
1027
+
1028
+ return sortDirection === 'desc' ? comparison * -1 : comparison;
1029
+ });
1030
+ }, [resolvedData, resolvedSort?.direction, resolvedSort?.field, storeName, tableColumns]);
1031
+
1032
+ const resolvedHasMore = useMemo(
1033
+ () => {
1034
+ if (autoMode) {
1035
+ if (Number.isFinite(Number(resolvedTotalItems)) && Number(resolvedTotalItems) > 0) {
1036
+ return sortedData.length < Number(resolvedTotalItems);
1037
+ }
1038
+
1039
+ return autoLastPageCount >= pageSizeNumber && sortedData.length > 0;
1040
+ }
1041
+
1042
+ return resolveHasMore({
1043
+ hasMore,
1044
+ dataLength: sortedData.length,
1045
+ totalItems: resolvedTotalItems,
1046
+ });
1047
+ },
1048
+ [autoLastPageCount, autoMode, hasMore, pageSizeNumber, resolvedTotalItems, sortedData.length],
1049
+ );
1050
+
1051
+ useEffect(() => {
1052
+ const nextPaginationState = {
1053
+ dataLength: sortedData.length,
1054
+ filtersKey: stableSerialize(resolvedFilters || {}),
1055
+ sortDirection: resolvedSort?.direction,
1056
+ sortField: resolvedSort?.field,
1057
+ };
1058
+
1059
+ const previousPaginationState = previousPaginationStateRef.current;
1060
+ const didPaginationStateChange =
1061
+ previousPaginationState.dataLength !== nextPaginationState.dataLength ||
1062
+ previousPaginationState.filtersKey !== nextPaginationState.filtersKey ||
1063
+ previousPaginationState.sortDirection !== nextPaginationState.sortDirection ||
1064
+ previousPaginationState.sortField !== nextPaginationState.sortField;
1065
+
1066
+ if (didPaginationStateChange) {
1067
+ endReachedLockRef.current = false;
1068
+ previousPaginationStateRef.current = nextPaginationState;
1069
+ }
1070
+ }, [resolvedFilters, resolvedSort?.direction, resolvedSort?.field, sortedData]);
1071
+
1072
+ useEffect(() => {
1073
+ if (typeof onDataLoaded !== 'function') {
1074
+ return;
1075
+ }
1076
+
1077
+ onDataLoaded(sortedData);
1078
+ }, [onDataLoaded, sortedData]);
1079
+
1080
+ const beginEdit = useCallback((row, column) => {
1081
+ if (!isEditableColumn(column)) return;
1082
+ setEditingCell(`${row?.id || row?.['@id']}:${getColumnKey(column)}`);
1083
+ }, []);
1084
+
1085
+ const clearEdit = useCallback(() => {
1086
+ setEditingCell(null);
1087
+ }, []);
1088
+
1089
+ const saveCell = useCallback((row, column, nextValue) => {
1090
+ const fieldName = getColumnKey(column);
1091
+ if (!fieldName || typeof actions.save !== 'function') {
1092
+ clearEdit();
1093
+ return Promise.resolve(null);
1094
+ }
1095
+
1096
+ const currentValue = resolveEditValue(column, row);
1097
+ const normalizedNextValue = nextValue && typeof nextValue === 'object'
1098
+ ? normalizeOptionKey(nextValue)
1099
+ : normalizeText(nextValue);
1100
+
1101
+ if (normalizeText(currentValue) === normalizeText(normalizedNextValue)) {
1102
+ clearEdit();
1103
+ return Promise.resolve(null);
1104
+ }
1105
+
1106
+ const cellKey = `${row?.id || row?.['@id']}:${fieldName}`;
1107
+ const payload = {
1108
+ id: normalizeId(row?.['@id'] || row?.id),
1109
+ [fieldName]: formatSaveValue(column, nextValue, row),
1110
+ };
1111
+
1112
+ setSavingCell(cellKey);
1113
+
1114
+ return actions.save(payload)
1115
+ .then(savedItem => {
1116
+ onSaved?.(savedItem || { ...row, [fieldName]: nextValue }, row);
1117
+ return savedItem;
1118
+ })
1119
+ .catch(error => {
1120
+ console.error(error);
1121
+ return null;
1122
+ })
1123
+ .finally(() => {
1124
+ setSavingCell(null);
1125
+ clearEdit();
1126
+ });
1127
+ }, [actions, clearEdit, onSaved]);
1128
+
1129
+ const requestSort = useCallback(column => {
1130
+ if (!isSortableColumn(column)) return;
1131
+
1132
+ const fieldName = getSortField(column);
1133
+ const nextDirection =
1134
+ resolvedSort?.field === fieldName && resolvedSort?.direction === 'asc'
1135
+ ? 'desc'
1136
+ : 'asc';
1137
+ const nextSort = {
1138
+ direction: nextDirection,
1139
+ field: fieldName,
1140
+ };
1141
+
1142
+ if (visibleColumnsPreferenceKey) {
1143
+ persistTableSortPreference(
1144
+ visibleColumnsPreferenceKey,
1145
+ nextSort,
1146
+ );
1147
+ }
1148
+
1149
+ if (autoMode) {
1150
+ setAutoSort(nextSort);
1151
+ return;
1152
+ }
1153
+
1154
+ onSortChange?.(nextSort);
1155
+ }, [
1156
+ autoMode,
1157
+ onSortChange,
1158
+ resolvedSort?.direction,
1159
+ resolvedSort?.field,
1160
+ visibleColumnsPreferenceKey,
1161
+ ]);
1162
+
1163
+ const openEditModal = useCallback(row => {
1164
+ if (typeof onEditRow === 'function') {
1165
+ onEditRow(row);
1166
+ return;
1167
+ }
1168
+
1169
+ setFormMode('edit');
1170
+ setEditingRow(row);
1171
+ }, [onEditRow]);
1172
+
1173
+ const openAddForm = useCallback(() => {
1174
+ if (typeof onAdd === 'function') {
1175
+ onAdd();
1176
+ return;
1177
+ }
1178
+
1179
+ if (typeof actions.save !== 'function') return;
1180
+
1181
+ setFormMode('create');
1182
+ setEditingRow({});
1183
+ }, [actions, onAdd]);
1184
+
1185
+ const closeEditModal = useCallback(() => {
1186
+ setEditingRow(null);
1187
+ setFormMode('edit');
1188
+ }, []);
1189
+
1190
+ const commitFilters = useCallback(nextFilters => {
1191
+ const resolvedNextFilters = isObject(nextFilters) ? nextFilters : {};
1192
+
1193
+ if (autoMode) {
1194
+ setAutoFilters(resolvedNextFilters);
1195
+ storeActions.setFilters?.(resolvedNextFilters);
1196
+ return;
1197
+ }
1198
+
1199
+ onFilterChange?.(resolvedNextFilters);
1200
+ }, [autoMode, onFilterChange, storeActions]);
1201
+
1202
+ const updateFilter = useCallback((fieldName, value) => {
1203
+ const nextFilters = { ...(resolvedFilters || {}) };
1204
+ const isEmpty =
1205
+ value === null ||
1206
+ value === undefined ||
1207
+ normalizeText(value) === '' ||
1208
+ (Array.isArray(value) && value.length === 0);
1209
+
1210
+ if (isEmpty) delete nextFilters[fieldName];
1211
+ else nextFilters[fieldName] = value;
1212
+
1213
+ commitFilters(nextFilters);
1214
+ }, [commitFilters, resolvedFilters]);
1215
+
1216
+ const toggleColumn = useCallback(column => {
1217
+ const fieldName = getColumnKey(column);
1218
+ if (!fieldName) return;
1219
+
1220
+ setVisibleColumns(prev => {
1221
+ const next = {
1222
+ ...sanitizeVisibleColumnsPreference({
1223
+ columns: columnsForTable,
1224
+ visibleColumns: prev,
1225
+ }),
1226
+ [fieldName]: prev[fieldName] === false,
1227
+ };
1228
+
1229
+ actions?.setVisibleColumns?.(next);
1230
+ if (visibleColumnsPreferenceKey) {
1231
+ persistVisibleColumnsPreference(
1232
+ visibleColumnsPreferenceKey,
1233
+ next,
1234
+ );
1235
+ }
1236
+ return next;
1237
+ });
1238
+ }, [
1239
+ actions,
1240
+ columnsForTable,
1241
+ visibleColumnsPreferenceKey,
1242
+ ]);
1243
+
1244
+ const toggleViewMode = useCallback(() => {
1245
+ setViewMode(prev => {
1246
+ const nextViewMode = prev === 'table' ? 'cards' : 'table';
1247
+
1248
+ if (!visibleColumnsPreferenceKey) {
1249
+ return nextViewMode;
1250
+ }
1251
+
1252
+ persistTableViewModePreference(
1253
+ visibleColumnsPreferenceKey,
1254
+ nextViewMode,
1255
+ );
1256
+
1257
+ return nextViewMode;
1258
+ });
1259
+ }, [
1260
+ visibleColumnsPreferenceKey,
1261
+ ]);
1262
+
1263
+ const handleEndReached = useCallback(() => {
1264
+ if (
1265
+ !resolvedHasMore ||
1266
+ resolvedIsLoading ||
1267
+ endReachedLockRef.current === true
1268
+ ) {
1269
+ return;
1270
+ }
1271
+
1272
+ endReachedLockRef.current = true;
1273
+
1274
+ if (autoMode) {
1275
+ const nextPage = autoPageRef.current + 1;
1276
+ loadAutoPage(nextPage, { append: true });
1277
+ return;
1278
+ }
1279
+
1280
+ if (typeof onEndReached === 'function') {
1281
+ onEndReached();
1282
+ }
1283
+ }, [autoMode, loadAutoPage, onEndReached, resolvedHasMore, resolvedIsLoading]);
1284
+
1285
+ const handleLayout = useCallback(event => {
1286
+ const nextWidth = Math.floor(event?.nativeEvent?.layout?.width || 0);
1287
+ if (nextWidth > 0) setTableContainerWidth(nextWidth);
1288
+ }, []);
1289
+
1290
+ const renderEditableCell = (row, column) => {
1291
+ const fieldName = getColumnKey(column);
1292
+ const cellKey = `${row?.id || row?.['@id']}:${fieldName}`;
1293
+ const isEditing = editingCell === cellKey;
1294
+ const isSaving = savingCell === cellKey;
1295
+ const shouldDelegatePress =
1296
+ typeof onRowPress === 'function' &&
1297
+ !isEditableColumn(column);
1298
+
1299
+ return (
1300
+ <View
1301
+ style={[getColumnStyle(column), isEditing ? styles.editingCell : null]}
1302
+ pointerEvents={shouldDelegatePress ? 'none' : 'auto'}
1303
+ >
1304
+ <DefaultInput
1305
+ accentColor={resolvedAccentColor}
1306
+ column={column}
1307
+ columns={columnsForTable}
1308
+ editing={isEditing}
1309
+ getOptionsForColumn={getOptionsForColumn}
1310
+ onCancelEditing={clearEdit}
1311
+ onSave={value => saveCell(row, column, value)}
1312
+ onStartEditing={() => beginEdit(row, column)}
1313
+ row={row}
1314
+ saving={isSaving}
1315
+ storeName={storeName}
1316
+ variant="cell"
1317
+ />
1318
+ </View>
1319
+ );
1320
+ };
1321
+
1322
+ const renderColumnFilter = column => {
1323
+ return (
1324
+ <DefaultColumnFilter
1325
+ accentColor={resolvedAccentColor}
1326
+ column={column}
1327
+ filters={resolvedFilters}
1328
+ getOptionsForColumn={getOptionsForColumn}
1329
+ onChange={updateFilter}
1330
+ storeName={storeName}
1331
+ style={getColumnStyle(column)}
1332
+ />
1333
+ );
1334
+ };
1335
+
1336
+ const renderToolbarAction = action => {
1337
+ if (!action || action.hidden) return null;
1338
+
1339
+ const isActive = action.active === true;
1340
+ const hasLabel = normalizeText(action.label) !== '';
1341
+ const actionColor = action.color || (isActive ? resolvedAccentColor : tableMutedColor);
1342
+
1343
+ return (
1344
+ <TouchableOpacity
1345
+ key={action.key || action.icon || action.label}
1346
+ style={[
1347
+ styles.toolbarButton,
1348
+ isActive
1349
+ ? { backgroundColor: withOpacity(resolvedAccentColor, 0.12), borderColor: resolvedAccentColor }
1350
+ : null,
1351
+ action.style,
1352
+ ]}
1353
+ activeOpacity={0.82}
1354
+ disabled={action.disabled === true}
1355
+ onPress={action.onPress}
1356
+ >
1357
+ {action.icon ? (
1358
+ <Icon
1359
+ name={action.icon}
1360
+ size={action.iconSize || 14}
1361
+ color={actionColor}
1362
+ />
1363
+ ) : null}
1364
+ {hasLabel ? (
1365
+ <Text style={[styles.toolbarActionLabel, { color: actionColor }, action.labelStyle]} numberOfLines={1}>
1366
+ {action.label}
1367
+ </Text>
1368
+ ) : null}
1369
+ {action.badge !== undefined && action.badge !== null ? (
1370
+ <Text style={[styles.toolbarBadgeText, { color: action.badgeColor || resolvedAccentColor }]}>
1371
+ {action.badge}
1372
+ </Text>
1373
+ ) : null}
1374
+ </TouchableOpacity>
1375
+ );
1376
+ };
1377
+
1378
+ const renderToolbarActions = () =>
1379
+ Array.isArray(toolbarActions) && toolbarActions.length > 0 ? (
1380
+ <View style={styles.toolbarActionGroup}>{toolbarActions.map(renderToolbarAction)}</View>
1381
+ ) : null;
1382
+
1383
+ const getColumnByField = useCallback(
1384
+ fieldName => columnsForTable.find(column => getColumnKey(column) === fieldName),
1385
+ [columnsForTable],
1386
+ );
1387
+
1388
+ const buildRowHelpers = useCallback(
1389
+ row => {
1390
+ const openEdit = () => openEditModal(row);
1391
+ const openRow = typeof onRowPress === 'function' ? () => onRowPress(row) : null;
1392
+ const renderValue = (fieldName, fallback = '-') => {
1393
+ const column = getColumnByField(fieldName);
1394
+ if (!column) return fallback;
1395
+ return resolveCellText({ column, columns: columnsForTable, row, storeName });
1396
+ };
1397
+ const renderField = (fieldName, options = {}) => {
1398
+ const column = getColumnByField(fieldName);
1399
+ if (!column) return null;
1400
+
1401
+ const cellKey = `${row?.id || row?.['@id']}:${fieldName}`;
1402
+ const isEditing = editingCell === cellKey;
1403
+ const isSaving = savingCell === cellKey;
1404
+
1405
+ return (
1406
+ <DefaultInput
1407
+ accentColor={options.accentColor || resolvedAccentColor}
1408
+ column={column}
1409
+ columns={columnsForTable}
1410
+ containerStyle={options.containerStyle}
1411
+ displayValue={options.displayValue}
1412
+ editing={isEditing}
1413
+ getOptionsForColumn={getOptionsForColumn}
1414
+ inputStyle={options.inputStyle}
1415
+ label={options.label}
1416
+ numberOfLines={options.numberOfLines}
1417
+ onCancelEditing={clearEdit}
1418
+ onSave={value => saveCell(row, column, value)}
1419
+ onStartEditing={() => beginEdit(row, column)}
1420
+ readTextStyle={options.readTextStyle || options.textStyle}
1421
+ row={row}
1422
+ saving={isSaving}
1423
+ showLabel={options.showLabel}
1424
+ storeName={storeName}
1425
+ variant={options.variant || 'card'}
1426
+ />
1427
+ );
1428
+ };
1429
+
1430
+ return {
1431
+ openEdit,
1432
+ openRow,
1433
+ renderField,
1434
+ renderValue,
1435
+ };
1436
+ },
1437
+ [
1438
+ resolvedAccentColor,
1439
+ beginEdit,
1440
+ clearEdit,
1441
+ columnsForTable,
1442
+ editingCell,
1443
+ getColumnByField,
1444
+ getOptionsForColumn,
1445
+ openEditModal,
1446
+ onRowPress,
1447
+ saveCell,
1448
+ savingCell,
1449
+ storeName,
1450
+ ],
1451
+ );
1452
+
1453
+ const renderCardItem = row => {
1454
+ const helpers = buildRowHelpers(row);
1455
+
1456
+ if (typeof renderCard === 'function') {
1457
+ return (
1458
+ <View key={row?.['@id'] || row?.id} style={styles.cardItem}>
1459
+ {renderCard({
1460
+ item: row,
1461
+ openEdit: helpers.openEdit,
1462
+ openRow: helpers.openRow,
1463
+ renderField: helpers.renderField,
1464
+ renderValue: helpers.renderValue,
1465
+ row,
1466
+ })}
1467
+ {hasRowActions ? (
1468
+ <View style={styles.cardActions}>
1469
+ <TouchableOpacity
1470
+ style={[styles.iconButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1471
+ activeOpacity={0.82}
1472
+ onPress={helpers.openEdit}>
1473
+ <Icon name="edit-2" size={14} color={tableMutedColor} />
1474
+ </TouchableOpacity>
1475
+ </View>
1476
+ ) : null}
1477
+ </View>
1478
+ );
1479
+ }
1480
+
1481
+ return (
1482
+ <View key={row?.['@id'] || row?.id} style={[styles.defaultCard, { backgroundColor: tableSurfaceColor, borderColor: tableBorderColor }]}>
1483
+ {tableColumns.map(column => (
1484
+ <View key={getColumnKey(column)} style={styles.defaultCardLine}>
1485
+ <Text style={[styles.defaultCardLabel, { color: tableMutedColor }]}>
1486
+ {formatStoreColumnLabel({
1487
+ columns: columnsForTable,
1488
+ fieldName: getColumnKey(column),
1489
+ fallbackLabel: column?.label || getColumnKey(column),
1490
+ storeName,
1491
+ })}
1492
+ </Text>
1493
+ {helpers.renderField(getColumnKey(column), {
1494
+ readTextStyle: [styles.defaultCardValue, { color: tableTextColor }],
1495
+ numberOfLines: 1,
1496
+ })}
1497
+ </View>
1498
+ ))}
1499
+ {hasRowActions ? (
1500
+ <TouchableOpacity
1501
+ style={[styles.cardEditButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1502
+ activeOpacity={0.82}
1503
+ onPress={helpers.openEdit}>
1504
+ <Icon name="edit-2" size={14} color={tableMutedColor} />
1505
+ </TouchableOpacity>
1506
+ ) : null}
1507
+ </View>
1508
+ );
1509
+ };
1510
+
1511
+ const renderEmptyState = isTable => {
1512
+ return (
1513
+ <View style={[styles.emptyBox, isTable ? tableLayoutStyle : null]}>
1514
+ {resolvedIsLoading ? (
1515
+ <StateStore compact loading={emptyStateLabel} />
1516
+ ) : (
1517
+ <Text style={[styles.emptyText, { color: tableMutedColor }]}>{emptyStateLabel}</Text>
1518
+ )}
1519
+ </View>
1520
+ );
1521
+ };
1522
+
1523
+ const renderLoadingOverlay = () => {
1524
+ if (!resolvedIsLoading || sortedData.length === 0) {
1525
+ return null;
1526
+ }
1527
+
1528
+ return (
1529
+ <View pointerEvents="none" style={styles.loadingOverlay}>
1530
+ <View
1531
+ style={[
1532
+ styles.loadingOverlayCard,
1533
+ { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor },
1534
+ ]}
1535
+ >
1536
+ <StateStore compact loading="Carregando mais registros..." />
1537
+ </View>
1538
+ </View>
1539
+ );
1540
+ };
1541
+
1542
+ const renderTableItem = ({ item: row, index }) => {
1543
+ const hasRowPress = typeof onRowPress === 'function';
1544
+ const RowComponent = hasRowPress ? TouchableOpacity : View;
1545
+ const rowPressProps = hasRowPress
1546
+ ? {
1547
+ activeOpacity: 0.84,
1548
+ onPress: () => onRowPress(row),
1549
+ }
1550
+ : {};
1551
+ const rowBackgroundColor = index % 2 === 0 ? tableOddColor : tableEvenColor;
1552
+
1553
+ return (
1554
+ <RowComponent
1555
+ key={getRowKey(row)}
1556
+ style={[styles.row, tableLayoutStyle, { backgroundColor: rowBackgroundColor, borderBottomColor: tableBorderColor }]}
1557
+ {...rowPressProps}
1558
+ >
1559
+ {tableColumns.map(column => (
1560
+ <React.Fragment key={getColumnKey(column)}>
1561
+ {renderEditableCell(row, column)}
1562
+ </React.Fragment>
1563
+ ))}
1564
+ {hasRowActions ? (
1565
+ <View style={[styles.cell, styles.actionsCell]}>
1566
+ <TouchableOpacity
1567
+ style={[styles.iconButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1568
+ activeOpacity={0.82}
1569
+ onPress={() => openEditModal(row)}>
1570
+ <Icon name="edit-2" size={14} color={tableMutedColor} />
1571
+ </TouchableOpacity>
1572
+ </View>
1573
+ ) : null}
1574
+ </RowComponent>
1575
+ );
1576
+ };
1577
+
1578
+ const renderEditModal = () => {
1579
+ const isCreate = formMode === 'create';
1580
+
1581
+ return (
1582
+ <Modal visible={Boolean(editingRow)} transparent animationType="fade" onRequestClose={closeEditModal}>
1583
+ <View style={[styles.modalOverlay, { backgroundColor: withOpacity(tableTextColor, 0.42) }]}>
1584
+ <View style={[styles.modalCard, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}>
1585
+ <View style={[styles.modalHeader, { borderBottomColor: tableBorderColor }]}>
1586
+ <Text style={[styles.modalTitle, { color: tableTextColor }]}>
1587
+ {isCreate
1588
+ ? global.t?.t(storeName, 'button', 'add') || 'Adicionar'
1589
+ : global.t?.t(storeName, 'button', 'edit') || 'Editar'}
1590
+ </Text>
1591
+ <TouchableOpacity
1592
+ style={[styles.modalCloseButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1593
+ onPress={closeEditModal}>
1594
+ <Icon name="x" size={18} color={tableMutedColor} />
1595
+ </TouchableOpacity>
1596
+ </View>
1597
+
1598
+ <DefaultForm
1599
+ accentColor={resolvedAccentColor}
1600
+ actions={actions}
1601
+ columns={isCreate ? columnsForTable : editableColumns}
1602
+ getOptionsForColumn={getOptionsForColumn}
1603
+ mode={formMode}
1604
+ onCancel={closeEditModal}
1605
+ onSaved={(savedItem, originalRow) => {
1606
+ onSaved?.(savedItem, originalRow);
1607
+ closeEditModal();
1608
+ }}
1609
+ row={editingRow || {}}
1610
+ storeName={storeName}
1611
+ />
1612
+ </View>
1613
+ </View>
1614
+ </Modal>
1615
+ );
1616
+ };
1617
+
1618
+ const renderColumnMenuModal = () => {
1619
+ if (!isColumnMenuOpen) return null;
1620
+
1621
+ return (
1622
+ <Modal visible transparent animationType="fade" onRequestClose={() => setIsColumnMenuOpen(false)}>
1623
+ <View style={[styles.modalOverlay, { backgroundColor: withOpacity(tableTextColor, 0.42) }]}>
1624
+ <View style={[styles.modalCard, styles.columnMenuModalCard, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}>
1625
+ <View style={[styles.modalHeader, { borderBottomColor: tableBorderColor }]}>
1626
+ <Text style={[styles.modalTitle, { color: tableTextColor }]} numberOfLines={1}>
1627
+ Colunas
1628
+ </Text>
1629
+ <TouchableOpacity
1630
+ style={[styles.modalCloseButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1631
+ activeOpacity={0.82}
1632
+ onPress={() => setIsColumnMenuOpen(false)}
1633
+ >
1634
+ <Icon name="x" size={16} color={tableMutedColor} />
1635
+ </TouchableOpacity>
1636
+ </View>
1637
+ <ScrollView style={styles.columnMenuModalBody} contentContainerStyle={styles.columnMenuModalList}>
1638
+ {availableColumns.map(column => {
1639
+ const fieldName = getColumnKey(column);
1640
+ const label = formatStoreColumnLabel({
1641
+ columns: columnsForTable,
1642
+ fieldName,
1643
+ fallbackLabel: column?.label || fieldName,
1644
+ storeName,
1645
+ });
1646
+ const checked = visibleColumns[fieldName] !== false;
1647
+
1648
+ return (
1649
+ <TouchableOpacity key={fieldName} style={styles.columnMenuItem} activeOpacity={0.82} onPress={() => toggleColumn(column)}>
1650
+ <Icon name={checked ? 'check-square' : 'square'} size={16} color={checked ? resolvedAccentColor : tableMutedColor} />
1651
+ <Text style={[styles.columnMenuText, { color: tableTextColor }]} numberOfLines={1}>{label}</Text>
1652
+ </TouchableOpacity>
1653
+ );
1654
+ })}
1655
+ </ScrollView>
1656
+ </View>
1657
+ </View>
1658
+ </Modal>
1659
+ );
1660
+ };
1661
+
1662
+ return (
1663
+ <View style={[styles.wrap, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]} onLayout={handleLayout}>
1664
+ <View style={[styles.toolbar, { borderBottomColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}>
1665
+ {shouldRenderCompactToolbarTotalItems ? (
1666
+ <View style={styles.toolbarCompactLead}>
1667
+ <View style={[styles.toolbarCountPill, { backgroundColor: withOpacity(resolvedAccentColor, 0.12) }]}>
1668
+ <Text
1669
+ style={[styles.toolbarCountText, { color: resolvedAccentColor }]}
1670
+ numberOfLines={1}
1671
+ adjustsFontSizeToFit
1672
+ minimumFontScale={0.82}
1673
+ >
1674
+ {resolvedTotalItemsText}
1675
+ </Text>
1676
+ </View>
1677
+ {searchProps ? (
1678
+ <DefaultSearch
1679
+ accentColor={resolvedAccentColor}
1680
+ compact
1681
+ storeName={storeName}
1682
+ {...searchProps}
1683
+ filters={autoMode ? resolvedFilters : searchProps?.filters}
1684
+ onChangeFilters={autoMode ? commitFilters : searchProps?.onChangeFilters}
1685
+ value={autoMode ? resolvedSearchValue : searchProps?.value}
1686
+ style={[styles.toolbarSearch, styles.toolbarCompactSearch, searchProps?.style]}
1687
+ />
1688
+ ) : null}
1689
+ {renderToolbarActions()}
1690
+ </View>
1691
+ ) : null}
1692
+
1693
+ <View style={shouldRenderCompactToolbarTotalItems ? styles.toolbarCompactActions : styles.toolbarLeft}>
1694
+ {!shouldRenderCompactToolbarTotalItems && searchProps ? (
1695
+ <DefaultSearch
1696
+ accentColor={resolvedAccentColor}
1697
+ compact
1698
+ storeName={storeName}
1699
+ {...searchProps}
1700
+ filters={autoMode ? resolvedFilters : searchProps?.filters}
1701
+ onChangeFilters={autoMode ? commitFilters : searchProps?.onChangeFilters}
1702
+ value={autoMode ? resolvedSearchValue : searchProps?.value}
1703
+ style={[styles.toolbarSearch, searchProps?.style]}
1704
+ />
1705
+ ) : null}
1706
+ {!shouldRenderCompactToolbarTotalItems ? renderToolbarActions() : null}
1707
+ {showColumnFiltersButton ? (
1708
+ <TouchableOpacity
1709
+ style={[
1710
+ styles.toolbarButton,
1711
+ { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor },
1712
+ showColumnFilters ? { backgroundColor: withOpacity(resolvedAccentColor, 0.12), borderColor: resolvedAccentColor } : null,
1713
+ ]}
1714
+ activeOpacity={0.82}
1715
+ onPress={() => setShowColumnFilters(prev => !prev)}
1716
+ >
1717
+ <Icon name="filter" size={14} color={showColumnFilters ? resolvedAccentColor : tableMutedColor} />
1718
+ {activeFilterCount > 0 ? (
1719
+ <Text style={[styles.toolbarBadgeText, { color: resolvedAccentColor }]}>{activeFilterCount}</Text>
1720
+ ) : null}
1721
+ </TouchableOpacity>
1722
+ ) : null}
1723
+ {(!isCompactView || !shouldForceCardsOnCompact) ? (
1724
+ <TouchableOpacity
1725
+ style={[styles.toolbarButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1726
+ activeOpacity={0.82}
1727
+ onPress={toggleViewMode}
1728
+ >
1729
+ <Icon name={viewMode === 'table' ? 'grid' : 'list'} size={14} color={tableMutedColor} />
1730
+ </TouchableOpacity>
1731
+ ) : null}
1732
+ <TouchableOpacity
1733
+ style={[styles.toolbarButton, { borderColor: tableBorderColor, backgroundColor: tableSurfaceColor }]}
1734
+ activeOpacity={0.82}
1735
+ onPress={() => setIsColumnMenuOpen(prev => !prev)}>
1736
+ <Icon name="columns" size={14} color={tableMutedColor} />
1737
+ </TouchableOpacity>
1738
+ {shouldRenderAddButton ? (
1739
+ <TouchableOpacity
1740
+ style={[
1741
+ styles.toolbarButton,
1742
+ styles.toolbarAddButton,
1743
+ { backgroundColor: resolvedAccentColor, borderColor: resolvedAccentColor },
1744
+ ]}
1745
+ activeOpacity={0.85}
1746
+ onPress={openAddForm}
1747
+ >
1748
+ <Icon name="plus" size={16} color={tableOnAccentColor} />
1749
+ </TouchableOpacity>
1750
+ ) : null}
1751
+ </View>
1752
+
1753
+ </View>
1754
+
1755
+ {effectiveViewMode === 'cards' ? (
1756
+ <FlatList
1757
+ data={sortedData}
1758
+ keyExtractor={getRowKey}
1759
+ renderItem={({ item }) => renderCardItem(item)}
1760
+ style={styles.cardsScroll}
1761
+ contentContainerStyle={styles.cardsGrid}
1762
+ ListEmptyComponent={renderEmptyState(false)}
1763
+ ListFooterComponent={null}
1764
+ nestedScrollEnabled
1765
+ onMomentumScrollBegin={onMomentumScrollBegin || undefined}
1766
+ onScrollBeginDrag={onScrollBeginDrag || undefined}
1767
+ onEndReached={handleEndReached}
1768
+ onEndReachedThreshold={END_REACHED_THRESHOLD}
1769
+ showsVerticalScrollIndicator={false}
1770
+ />
1771
+ ) : (
1772
+ <ScrollView horizontal style={styles.scroll}>
1773
+ <View style={[styles.content, tableLayoutStyle]}>
1774
+ <View style={[styles.headerRow, tableLayoutStyle, { backgroundColor: tableHeaderColor, borderBottomColor: tableBorderColor }]}>
1775
+ {tableColumns.map(column => {
1776
+ const fieldName = getColumnKey(column);
1777
+ const label = formatStoreColumnLabel({
1778
+ columns: columnsForTable,
1779
+ fieldName,
1780
+ fallbackLabel: column?.label || fieldName,
1781
+ storeName,
1782
+ });
1783
+
1784
+ const sortFieldName = getSortField(column);
1785
+
1786
+ return (
1787
+ <TouchableOpacity
1788
+ key={fieldName}
1789
+ style={getColumnStyle(column)}
1790
+ activeOpacity={isSortableColumn(column) ? 0.8 : 1}
1791
+ onPress={() => requestSort(column)}
1792
+ >
1793
+ <View style={styles.sortableHeader}>
1794
+ <Text style={[styles.headerText, { color: tableTextColor }]} numberOfLines={1}>{label}</Text>
1795
+ {isSortableColumn(column) && resolvedSort?.field === sortFieldName ? (
1796
+ <Icon name={resolvedSort?.direction === 'desc' ? 'chevron-down' : 'chevron-up'} size={12} color={tableTextColor} />
1797
+ ) : isSortableColumn(column) ? (
1798
+ <Icon name="chevrons-up" size={12} color={tableBorderColor} />
1799
+ ) : null}
1800
+ {resolvedFilters?.[fieldName] ? <Icon name="filter" size={11} color={tableTextColor} /> : null}
1801
+ </View>
1802
+ </TouchableOpacity>
1803
+ );
1804
+ })}
1805
+ {hasRowActions ? (
1806
+ <View style={[styles.cell, styles.actionsCell]}>
1807
+ <Text style={[styles.headerText, { color: tableTextColor }]}>Acoes</Text>
1808
+ </View>
1809
+ ) : null}
1810
+ </View>
1811
+
1812
+ {showColumnFiltersButton && showColumnFilters ? (
1813
+ <View style={[styles.filterRow, tableLayoutStyle, { backgroundColor: tableSurfaceColor, borderBottomColor: tableBorderColor }]}>
1814
+ {tableColumns.map(column => (
1815
+ <React.Fragment key={getColumnKey(column)}>
1816
+ {renderColumnFilter(column)}
1817
+ </React.Fragment>
1818
+ ))}
1819
+ {hasRowActions ? <View style={[styles.cell, styles.actionsCell]} /> : null}
1820
+ </View>
1821
+ ) : null}
1822
+
1823
+ <FlatList
1824
+ data={sortedData}
1825
+ keyExtractor={getRowKey}
1826
+ renderItem={renderTableItem}
1827
+ style={styles.tableList}
1828
+ contentContainerStyle={styles.tableListContent}
1829
+ ListEmptyComponent={renderEmptyState(true)}
1830
+ ListFooterComponent={null}
1831
+ nestedScrollEnabled
1832
+ onMomentumScrollBegin={onMomentumScrollBegin || undefined}
1833
+ onScrollBeginDrag={onScrollBeginDrag || undefined}
1834
+ onEndReached={handleEndReached}
1835
+ onEndReachedThreshold={END_REACHED_THRESHOLD}
1836
+ showsVerticalScrollIndicator={false}
1837
+ />
1838
+ </View>
1839
+ </ScrollView>
1840
+ )}
1841
+
1842
+ {renderLoadingOverlay()}
1843
+
1844
+ {shouldRenderFooterBar ? (
1845
+ <View style={[styles.footerBar, { backgroundColor: tableSurfaceColor, borderTopColor: tableBorderColor }]}>
1846
+ {summaryEntries.length > 0 ? (
1847
+ <View style={styles.footerSummaryList}>
1848
+ {summaryEntries.map(entry => (
1849
+ <View key={entry.key} style={styles.footerSummaryItem}>
1850
+ <Text style={[styles.footerSummaryLabel, { color: tableMutedColor }]} numberOfLines={1}>
1851
+ {entry.label}
1852
+ </Text>
1853
+ <Text
1854
+ style={[
1855
+ styles.footerSummaryValue,
1856
+ entry.path?.[0] === 'sum' || isMoneySummaryPath(entry.path)
1857
+ ? { color: resolvedAccentColor }
1858
+ : { color: tableTextColor },
1859
+ ]}
1860
+ numberOfLines={1}
1861
+ adjustsFontSizeToFit
1862
+ minimumFontScale={0.78}
1863
+ >
1864
+ {formatSummaryValue({
1865
+ column: entry.column,
1866
+ columns: columnsForTable,
1867
+ path: entry.path,
1868
+ storeName,
1869
+ value: entry.value,
1870
+ })}
1871
+ </Text>
1872
+ </View>
1873
+ ))}
1874
+ </View>
1875
+ ) : null}
1876
+ {shouldRenderFooterTotalItems && !shouldRenderCompactToolbarTotalItems ? (
1877
+ <View style={[styles.footerCountPill, { backgroundColor: withOpacity(resolvedAccentColor, 0.12) }]}>
1878
+ <Text
1879
+ style={[styles.footerCountText, { color: resolvedAccentColor }]}
1880
+ numberOfLines={1}
1881
+ adjustsFontSizeToFit
1882
+ minimumFontScale={0.82}
1883
+ >
1884
+ {resolvedTotalItemsText}
1885
+ </Text>
1886
+ </View>
1887
+ ) : null}
1888
+ </View>
1889
+ ) : null}
1890
+
1891
+ {renderColumnMenuModal()}
1892
+ {renderEditModal()}
1893
+ </View>
1894
+ );
1895
+ };
1896
+
1897
+ export default DefaultTable;