@visns-studio/visns-components 5.8.7 → 5.8.8
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.
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import '../../styles/global.css';
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
3
|
+
import React, {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
4
10
|
import { useParams, Outlet } from 'react-router-dom';
|
|
5
11
|
import { toast } from 'react-toastify';
|
|
6
12
|
import { saveAs } from 'file-saver';
|
|
7
13
|
import _ from 'lodash';
|
|
8
14
|
import parse from 'html-react-parser';
|
|
9
15
|
import moment from 'moment';
|
|
16
|
+
import numeral from 'numeral';
|
|
10
17
|
import { CloudDownload } from 'akar-icons';
|
|
11
18
|
|
|
12
19
|
import CustomFetch from '../Fetch';
|
|
@@ -33,6 +40,8 @@ function useWindowHeight() {
|
|
|
33
40
|
function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
34
41
|
const [config, setConfig] = useState({});
|
|
35
42
|
const [subnav, setSubnav] = useState(filters || []);
|
|
43
|
+
// Add a ref to track previous active item to prevent unnecessary updates
|
|
44
|
+
const prevActiveItemRef = useRef(null);
|
|
36
45
|
|
|
37
46
|
useEffect(() => {
|
|
38
47
|
const newConfig = {
|
|
@@ -114,6 +123,21 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
|
114
123
|
if (activeItemResult) {
|
|
115
124
|
const activeItem = activeItemResult.item;
|
|
116
125
|
|
|
126
|
+
// Create a string representation of the active item for comparison
|
|
127
|
+
const activeItemString = JSON.stringify({
|
|
128
|
+
id: activeItem.id,
|
|
129
|
+
isChild: activeItemResult.isChild,
|
|
130
|
+
parentId: activeItemResult.parent?.id,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Skip update if the active item hasn't changed
|
|
134
|
+
if (prevActiveItemRef.current === activeItemString) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Update the ref with current active item
|
|
139
|
+
prevActiveItemRef.current = activeItemString;
|
|
140
|
+
|
|
117
141
|
// Find the tab config for this item
|
|
118
142
|
const activeTabConfig = tabs.find(
|
|
119
143
|
(t) => t.id === activeItem.id
|
|
@@ -121,6 +145,9 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
|
121
145
|
|
|
122
146
|
if (activeTabConfig) {
|
|
123
147
|
// We found a matching tab config, use it
|
|
148
|
+
// Get current style to preserve it
|
|
149
|
+
const currentStyle = config.style || {};
|
|
150
|
+
|
|
124
151
|
const updatedConfig = {
|
|
125
152
|
...config,
|
|
126
153
|
id: activeTabConfig.id,
|
|
@@ -130,7 +157,7 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
|
130
157
|
settings: activeTabConfig.settings,
|
|
131
158
|
// Preserve the style from the original config or use the tab config's style
|
|
132
159
|
style:
|
|
133
|
-
|
|
160
|
+
currentStyle ||
|
|
134
161
|
activeTabConfig.style ||
|
|
135
162
|
setting.style ||
|
|
136
163
|
{},
|
|
@@ -149,6 +176,9 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
|
149
176
|
// If this is a child item but we couldn't find a tab config,
|
|
150
177
|
// try to use the base config but update the filter
|
|
151
178
|
|
|
179
|
+
// Get current style to preserve it
|
|
180
|
+
const currentStyle = config.style || {};
|
|
181
|
+
|
|
152
182
|
// Create a new config based on the base settings
|
|
153
183
|
const baseConfig = {
|
|
154
184
|
export: setting.export || {},
|
|
@@ -160,7 +190,7 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
|
160
190
|
setRowsSelected,
|
|
161
191
|
settings: setting.settings,
|
|
162
192
|
// Make sure we have a style property
|
|
163
|
-
style:
|
|
193
|
+
style: currentStyle || setting.style || {},
|
|
164
194
|
tableSetting,
|
|
165
195
|
type: setting.type ? setting.type : 'table',
|
|
166
196
|
// Preserve functions from the original config or setting
|
|
@@ -189,11 +219,1070 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
|
189
219
|
}
|
|
190
220
|
}
|
|
191
221
|
}
|
|
192
|
-
}, [subnav, tabs,
|
|
222
|
+
}, [subnav, tabs, setting, setRowsSelected, tableSetting]);
|
|
193
223
|
|
|
194
224
|
return { config, setConfig, subnav, setSubnav };
|
|
195
225
|
}
|
|
196
226
|
|
|
227
|
+
const Grid = ({ config, userProfile }) => {
|
|
228
|
+
const [gridData, setGridData] = useState([]);
|
|
229
|
+
const [sortedData, setSortedData] = useState([]);
|
|
230
|
+
const [filteredData, setFilteredData] = useState([]);
|
|
231
|
+
const [filterValues, setFilterValues] = useState({});
|
|
232
|
+
const [settingsFilterValues, setSettingsFilterValues] = useState({});
|
|
233
|
+
const [loading, setLoading] = useState(false);
|
|
234
|
+
const [filtersCollapsed, setFiltersCollapsed] = useState(false);
|
|
235
|
+
const [sortConfig, setSortConfig] = useState({
|
|
236
|
+
key: null,
|
|
237
|
+
direction: 'asc',
|
|
238
|
+
});
|
|
239
|
+
const prevFetchParamsRef = useRef(null);
|
|
240
|
+
|
|
241
|
+
// Extract data and settings from config using useMemo to prevent unnecessary re-renders
|
|
242
|
+
const {
|
|
243
|
+
settings,
|
|
244
|
+
columns = [],
|
|
245
|
+
data = [],
|
|
246
|
+
} = useMemo(
|
|
247
|
+
() => ({
|
|
248
|
+
settings: config.settings || {},
|
|
249
|
+
columns: config.columns || [],
|
|
250
|
+
data: config.data || [],
|
|
251
|
+
}),
|
|
252
|
+
[config.settings, config.columns, config.data]
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Create table headers from columns
|
|
256
|
+
const headers = useMemo(
|
|
257
|
+
() => columns.map((col) => col.header || col.name || ''),
|
|
258
|
+
[columns]
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Format cell content based on column type or value type
|
|
262
|
+
const formatCellContent = (value, column) => {
|
|
263
|
+
if (value === null || value === undefined) return '';
|
|
264
|
+
if (value === 0) return '0'; // Explicitly handle zero values
|
|
265
|
+
if (value === '') return '';
|
|
266
|
+
|
|
267
|
+
// Try to infer the type if not provided
|
|
268
|
+
const valueType = column.type || typeof value;
|
|
269
|
+
|
|
270
|
+
switch (valueType) {
|
|
271
|
+
case 'date':
|
|
272
|
+
return moment(value).isValid()
|
|
273
|
+
? moment(value).format(column.format || 'DD/MM/YYYY')
|
|
274
|
+
: value;
|
|
275
|
+
case 'datetime':
|
|
276
|
+
return moment(value).isValid()
|
|
277
|
+
? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
|
|
278
|
+
: value;
|
|
279
|
+
case 'currency':
|
|
280
|
+
return numeral(value).format(column.format || '$0,0.00');
|
|
281
|
+
case 'number':
|
|
282
|
+
// Check if it's actually a number before formatting
|
|
283
|
+
return !isNaN(parseFloat(value)) && isFinite(value)
|
|
284
|
+
? numeral(value).format(column.format || '0,0')
|
|
285
|
+
: value;
|
|
286
|
+
case 'boolean':
|
|
287
|
+
if (
|
|
288
|
+
value === 1 ||
|
|
289
|
+
value === true ||
|
|
290
|
+
value === 'true' ||
|
|
291
|
+
value === 'yes' ||
|
|
292
|
+
value === 'Yes'
|
|
293
|
+
) {
|
|
294
|
+
return 'Yes';
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
value === 0 ||
|
|
298
|
+
value === false ||
|
|
299
|
+
value === 'false' ||
|
|
300
|
+
value === 'no' ||
|
|
301
|
+
value === 'No'
|
|
302
|
+
) {
|
|
303
|
+
return 'No';
|
|
304
|
+
}
|
|
305
|
+
return value;
|
|
306
|
+
case 'year':
|
|
307
|
+
// Special handling for year values
|
|
308
|
+
return value.toString();
|
|
309
|
+
case 'quarter':
|
|
310
|
+
// Special handling for quarter values (Q1, Q2, etc.)
|
|
311
|
+
if (value && value.toString().startsWith('Q')) {
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
return value;
|
|
315
|
+
default:
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// State for headers from API response
|
|
321
|
+
const [gridHeaders, setGridHeaders] = useState([]);
|
|
322
|
+
|
|
323
|
+
// Function to sort data
|
|
324
|
+
const sortData = useCallback(
|
|
325
|
+
(data, key, direction) => {
|
|
326
|
+
if (!key) return [...data];
|
|
327
|
+
|
|
328
|
+
return [...data].sort((a, b) => {
|
|
329
|
+
// Find the header config for this key to determine type
|
|
330
|
+
const headerConfig = gridHeaders.find((h) => h.key === key);
|
|
331
|
+
const type = headerConfig?.type || 'text';
|
|
332
|
+
|
|
333
|
+
// Get values, handling null/undefined
|
|
334
|
+
let aValue =
|
|
335
|
+
a[key] === null || a[key] === undefined ? '' : a[key];
|
|
336
|
+
let bValue =
|
|
337
|
+
b[key] === null || b[key] === undefined ? '' : b[key];
|
|
338
|
+
|
|
339
|
+
// Sort based on type
|
|
340
|
+
switch (type) {
|
|
341
|
+
case 'number':
|
|
342
|
+
case 'year':
|
|
343
|
+
aValue = parseFloat(aValue) || 0;
|
|
344
|
+
bValue = parseFloat(bValue) || 0;
|
|
345
|
+
break;
|
|
346
|
+
case 'date':
|
|
347
|
+
// Convert to timestamp for comparison
|
|
348
|
+
aValue = aValue ? new Date(aValue).getTime() : 0;
|
|
349
|
+
bValue = bValue ? new Date(bValue).getTime() : 0;
|
|
350
|
+
break;
|
|
351
|
+
default:
|
|
352
|
+
// For text, convert to lowercase strings
|
|
353
|
+
aValue = String(aValue).toLowerCase();
|
|
354
|
+
bValue = String(bValue).toLowerCase();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Compare values
|
|
358
|
+
if (aValue < bValue) {
|
|
359
|
+
return direction === 'asc' ? -1 : 1;
|
|
360
|
+
}
|
|
361
|
+
if (aValue > bValue) {
|
|
362
|
+
return direction === 'asc' ? 1 : -1;
|
|
363
|
+
}
|
|
364
|
+
return 0;
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
[gridHeaders]
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Handle sort request
|
|
371
|
+
const requestSort = useCallback((key) => {
|
|
372
|
+
// If clicking the same column, toggle direction
|
|
373
|
+
// Otherwise, start with ascending sort for the new column
|
|
374
|
+
setSortConfig((prevConfig) => {
|
|
375
|
+
const newDirection =
|
|
376
|
+
prevConfig.key === key && prevConfig.direction === 'asc'
|
|
377
|
+
? 'desc'
|
|
378
|
+
: 'asc';
|
|
379
|
+
|
|
380
|
+
return { key, direction: newDirection };
|
|
381
|
+
});
|
|
382
|
+
}, []);
|
|
383
|
+
|
|
384
|
+
// Function to filter data based on filter values
|
|
385
|
+
const filterData = useCallback(
|
|
386
|
+
(data) => {
|
|
387
|
+
// If no filter values are set, return the original data
|
|
388
|
+
if (Object.keys(filterValues).length === 0) {
|
|
389
|
+
return data;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Filter the data based on the filter values
|
|
393
|
+
return data.filter((row) => {
|
|
394
|
+
// Check each filter
|
|
395
|
+
return Object.entries(filterValues).every(([key, value]) => {
|
|
396
|
+
// Skip empty filter values
|
|
397
|
+
if (!value) return true;
|
|
398
|
+
|
|
399
|
+
// Get the row value
|
|
400
|
+
const rowValue = row[key];
|
|
401
|
+
if (rowValue === null || rowValue === undefined)
|
|
402
|
+
return false;
|
|
403
|
+
|
|
404
|
+
// Find the header config for this key
|
|
405
|
+
const headerConfig = gridHeaders.find((h) => h.key === key);
|
|
406
|
+
if (!headerConfig || !headerConfig.filter) return true;
|
|
407
|
+
|
|
408
|
+
const { type, operator } = headerConfig.filter;
|
|
409
|
+
|
|
410
|
+
// Apply filter based on type and operator
|
|
411
|
+
switch (type) {
|
|
412
|
+
case 'string':
|
|
413
|
+
const rowStr = String(rowValue).toLowerCase();
|
|
414
|
+
const filterStr = String(value).toLowerCase();
|
|
415
|
+
|
|
416
|
+
switch (operator) {
|
|
417
|
+
case 'equals':
|
|
418
|
+
return rowStr === filterStr;
|
|
419
|
+
case 'contains':
|
|
420
|
+
return rowStr.includes(filterStr);
|
|
421
|
+
case 'startsWith':
|
|
422
|
+
return rowStr.startsWith(filterStr);
|
|
423
|
+
case 'endsWith':
|
|
424
|
+
return rowStr.endsWith(filterStr);
|
|
425
|
+
default:
|
|
426
|
+
return rowStr.includes(filterStr);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
case 'number':
|
|
430
|
+
const rowNum = parseFloat(rowValue);
|
|
431
|
+
const filterNum = parseFloat(value);
|
|
432
|
+
|
|
433
|
+
if (isNaN(rowNum) || isNaN(filterNum)) return false;
|
|
434
|
+
|
|
435
|
+
switch (operator) {
|
|
436
|
+
case 'equals':
|
|
437
|
+
return rowNum === filterNum;
|
|
438
|
+
case 'greaterThan':
|
|
439
|
+
return rowNum > filterNum;
|
|
440
|
+
case 'lessThan':
|
|
441
|
+
return rowNum < filterNum;
|
|
442
|
+
case 'greaterThanOrEqual':
|
|
443
|
+
return rowNum >= filterNum;
|
|
444
|
+
case 'lessThanOrEqual':
|
|
445
|
+
return rowNum <= filterNum;
|
|
446
|
+
default:
|
|
447
|
+
return rowNum === filterNum;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
case 'date':
|
|
451
|
+
const rowDate = new Date(rowValue);
|
|
452
|
+
const filterDate = new Date(value);
|
|
453
|
+
|
|
454
|
+
if (
|
|
455
|
+
isNaN(rowDate.getTime()) ||
|
|
456
|
+
isNaN(filterDate.getTime())
|
|
457
|
+
)
|
|
458
|
+
return false;
|
|
459
|
+
|
|
460
|
+
switch (operator) {
|
|
461
|
+
case 'equals':
|
|
462
|
+
return (
|
|
463
|
+
rowDate.toDateString() ===
|
|
464
|
+
filterDate.toDateString()
|
|
465
|
+
);
|
|
466
|
+
case 'after':
|
|
467
|
+
return rowDate > filterDate;
|
|
468
|
+
case 'before':
|
|
469
|
+
return rowDate < filterDate;
|
|
470
|
+
default:
|
|
471
|
+
return (
|
|
472
|
+
rowDate.toDateString() ===
|
|
473
|
+
filterDate.toDateString()
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
default:
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
[filterValues, gridHeaders]
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Handle filter change
|
|
487
|
+
const handleFilterChange = useCallback((key, value) => {
|
|
488
|
+
setFilterValues((prev) => ({
|
|
489
|
+
...prev,
|
|
490
|
+
[key]: value,
|
|
491
|
+
}));
|
|
492
|
+
}, []);
|
|
493
|
+
|
|
494
|
+
// Effect to update filteredData and sortedData when gridData, filterValues, or sortConfig changes
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
if (gridData.length > 0) {
|
|
497
|
+
// First apply filters
|
|
498
|
+
const filtered = filterData(gridData);
|
|
499
|
+
setFilteredData(filtered);
|
|
500
|
+
|
|
501
|
+
// Then sort the filtered data
|
|
502
|
+
const sorted = sortData(
|
|
503
|
+
filtered,
|
|
504
|
+
sortConfig.key,
|
|
505
|
+
sortConfig.direction
|
|
506
|
+
);
|
|
507
|
+
setSortedData(sorted);
|
|
508
|
+
} else {
|
|
509
|
+
setFilteredData([]);
|
|
510
|
+
setSortedData([]);
|
|
511
|
+
}
|
|
512
|
+
}, [gridData, filterValues, sortConfig, sortData, filterData]);
|
|
513
|
+
|
|
514
|
+
// Load data from API if gridSetting.fetch is provided
|
|
515
|
+
useEffect(() => {
|
|
516
|
+
const fetchData = async () => {
|
|
517
|
+
// Only fetch if we have a URL
|
|
518
|
+
if (!settings?.fetch?.url) {
|
|
519
|
+
if (data && data.length > 0) {
|
|
520
|
+
// Use provided data if no fetch setting
|
|
521
|
+
setGridData(data);
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const url = settings.fetch.url;
|
|
527
|
+
const method = settings.fetch.method || 'POST';
|
|
528
|
+
|
|
529
|
+
// Combine base params with settings filter values
|
|
530
|
+
const params = {
|
|
531
|
+
...(settings.fetch.params || {}),
|
|
532
|
+
...settingsFilterValues,
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Create a string representation of the fetch parameters for comparison
|
|
536
|
+
const fetchParamsString = JSON.stringify({ url, method, params });
|
|
537
|
+
|
|
538
|
+
// Skip fetch if the parameters haven't changed
|
|
539
|
+
if (prevFetchParamsRef.current === fetchParamsString) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Update the ref with current parameters
|
|
544
|
+
prevFetchParamsRef.current = fetchParamsString;
|
|
545
|
+
|
|
546
|
+
setLoading(true);
|
|
547
|
+
try {
|
|
548
|
+
const response = await CustomFetch(url, method, params);
|
|
549
|
+
|
|
550
|
+
if (response.data.data) {
|
|
551
|
+
// Check if the response has the expected structure with header and rows
|
|
552
|
+
if (response.data.data.header && response.data.data.rows) {
|
|
553
|
+
// Set the headers from the response with the new structure
|
|
554
|
+
const headerEntries = Object.entries(
|
|
555
|
+
response.data.data.header
|
|
556
|
+
);
|
|
557
|
+
setGridHeaders(
|
|
558
|
+
headerEntries.map(([key, config]) => ({
|
|
559
|
+
key,
|
|
560
|
+
label: config.label || key,
|
|
561
|
+
sort: config.sort || key,
|
|
562
|
+
type: config.type || 'text',
|
|
563
|
+
filter: config.filter || null,
|
|
564
|
+
onClick: config.onClick || null,
|
|
565
|
+
}))
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
// Initialize filter values
|
|
569
|
+
const initialFilterValues = {};
|
|
570
|
+
headerEntries.forEach(([key, config]) => {
|
|
571
|
+
if (config.filter) {
|
|
572
|
+
initialFilterValues[key] = '';
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
setFilterValues(initialFilterValues);
|
|
576
|
+
|
|
577
|
+
// Set the rows data
|
|
578
|
+
setGridData(response.data.data.rows || []);
|
|
579
|
+
// Also set the initial sorted data
|
|
580
|
+
setSortedData(response.data.data.rows || []);
|
|
581
|
+
} else {
|
|
582
|
+
// Fallback to the previous behavior if the structure is different
|
|
583
|
+
setGridData(response.data.data.data || []);
|
|
584
|
+
setSortedData(response.data.data.data || []);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
console.error('Error fetching grid data:', error);
|
|
589
|
+
} finally {
|
|
590
|
+
setLoading(false);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
fetchData();
|
|
595
|
+
}, [
|
|
596
|
+
// Only depend on the specific properties we need, not the entire objects
|
|
597
|
+
settings?.fetch?.url,
|
|
598
|
+
settings?.fetch?.method,
|
|
599
|
+
// Use JSON.stringify for the params to detect deep changes
|
|
600
|
+
JSON.stringify(settings?.fetch?.params),
|
|
601
|
+
// Only depend on data if we're not fetching
|
|
602
|
+
!settings?.fetch?.url ? JSON.stringify(data) : null,
|
|
603
|
+
]);
|
|
604
|
+
|
|
605
|
+
// Generate year options for dropdown
|
|
606
|
+
const generateYearOptions = useCallback((startYear) => {
|
|
607
|
+
const currentYear = new Date().getFullYear();
|
|
608
|
+
const years = [];
|
|
609
|
+
|
|
610
|
+
// Start from the specified year and go up to current year
|
|
611
|
+
for (let year = startYear; year <= currentYear + 1; year++) {
|
|
612
|
+
years.push({ value: year.toString(), label: year.toString() });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return years;
|
|
616
|
+
}, []);
|
|
617
|
+
|
|
618
|
+
// Initialize settings filters
|
|
619
|
+
useEffect(() => {
|
|
620
|
+
if (settings?.filters && settings.filters.length > 0) {
|
|
621
|
+
const initialValues = {};
|
|
622
|
+
|
|
623
|
+
settings.filters.forEach((filter) => {
|
|
624
|
+
if (filter.type === 'dropdown' && filter.id === 'year') {
|
|
625
|
+
// For year dropdown with 'now' value, set to current year
|
|
626
|
+
if (filter.value === 'now') {
|
|
627
|
+
initialValues[filter.id] = new Date()
|
|
628
|
+
.getFullYear()
|
|
629
|
+
.toString();
|
|
630
|
+
} else {
|
|
631
|
+
initialValues[filter.id] = filter.value || '';
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
initialValues[filter.id] = filter.value || '';
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
setSettingsFilterValues(initialValues);
|
|
639
|
+
}
|
|
640
|
+
}, [settings?.filters]);
|
|
641
|
+
|
|
642
|
+
// Handle settings filter change
|
|
643
|
+
const handleSettingsFilterChange = useCallback(
|
|
644
|
+
(filterId, value) => {
|
|
645
|
+
setSettingsFilterValues((prev) => ({
|
|
646
|
+
...prev,
|
|
647
|
+
[filterId]: value,
|
|
648
|
+
}));
|
|
649
|
+
|
|
650
|
+
// Update fetch params with the new filter value
|
|
651
|
+
if (settings?.fetch?.params) {
|
|
652
|
+
const updatedParams = {
|
|
653
|
+
...settings.fetch.params,
|
|
654
|
+
[filterId]: value,
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Update the settings object
|
|
658
|
+
if (config.settings && config.settings.fetch) {
|
|
659
|
+
config.settings.fetch.params = updatedParams;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Trigger a re-fetch with the new params
|
|
663
|
+
prevFetchParamsRef.current = null;
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
[settings, config]
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
// Apply custom styling from userProfile if available
|
|
670
|
+
const getCustomStyles = () => {
|
|
671
|
+
const customStyles = {};
|
|
672
|
+
|
|
673
|
+
if (userProfile?.settings?.style) {
|
|
674
|
+
const { style } = userProfile.settings;
|
|
675
|
+
|
|
676
|
+
if (style.hoverColor) {
|
|
677
|
+
customStyles.hoverColor = {
|
|
678
|
+
'--hover-color': style.hoverColor,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return customStyles;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// CSS for sort indicators
|
|
687
|
+
const getSortIndicatorStyle = (key) => {
|
|
688
|
+
if (sortConfig.key !== key) {
|
|
689
|
+
return { opacity: 0.3 }; // Dim the icon when not sorting by this column
|
|
690
|
+
}
|
|
691
|
+
return {}; // Full opacity when sorting by this column
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
return (
|
|
695
|
+
<div className={styles.gridContainer}>
|
|
696
|
+
{/* Settings Filters */}
|
|
697
|
+
{settings?.filters && settings.filters.length > 0 && (
|
|
698
|
+
<div className={styles.settingsFiltersContainer}>
|
|
699
|
+
<div className={styles.settingsFiltersHeader}>
|
|
700
|
+
<div
|
|
701
|
+
className={styles.headerLeft}
|
|
702
|
+
onClick={() =>
|
|
703
|
+
setFiltersCollapsed(!filtersCollapsed)
|
|
704
|
+
}
|
|
705
|
+
>
|
|
706
|
+
<div className={styles.settingsFiltersTitle}>
|
|
707
|
+
<svg
|
|
708
|
+
width="16"
|
|
709
|
+
height="16"
|
|
710
|
+
viewBox="0 0 24 24"
|
|
711
|
+
fill="none"
|
|
712
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
713
|
+
>
|
|
714
|
+
<path
|
|
715
|
+
d="M4 6H20M7 12H17M9 18H15"
|
|
716
|
+
stroke="currentColor"
|
|
717
|
+
strokeWidth="2"
|
|
718
|
+
strokeLinecap="round"
|
|
719
|
+
strokeLinejoin="round"
|
|
720
|
+
/>
|
|
721
|
+
</svg>
|
|
722
|
+
Filters
|
|
723
|
+
{/* Show active filters count when collapsed */}
|
|
724
|
+
{filtersCollapsed && (
|
|
725
|
+
<div
|
|
726
|
+
className={
|
|
727
|
+
styles.activeFiltersIndicator
|
|
728
|
+
}
|
|
729
|
+
>
|
|
730
|
+
{Object.values(
|
|
731
|
+
settingsFilterValues
|
|
732
|
+
).some((value) => value !== '') && (
|
|
733
|
+
<>
|
|
734
|
+
<span>Active:</span>
|
|
735
|
+
<span
|
|
736
|
+
className={
|
|
737
|
+
styles.filterBadge
|
|
738
|
+
}
|
|
739
|
+
>
|
|
740
|
+
{
|
|
741
|
+
Object.values(
|
|
742
|
+
settingsFilterValues
|
|
743
|
+
).filter(
|
|
744
|
+
(value) =>
|
|
745
|
+
value !== ''
|
|
746
|
+
).length
|
|
747
|
+
}
|
|
748
|
+
</span>
|
|
749
|
+
</>
|
|
750
|
+
)}
|
|
751
|
+
</div>
|
|
752
|
+
)}
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
{/* Toggle icon */}
|
|
756
|
+
<svg
|
|
757
|
+
className={`${styles.toggleIcon} ${
|
|
758
|
+
filtersCollapsed ? styles.collapsed : ''
|
|
759
|
+
}`}
|
|
760
|
+
width="16"
|
|
761
|
+
height="16"
|
|
762
|
+
viewBox="0 0 24 24"
|
|
763
|
+
fill="none"
|
|
764
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
765
|
+
>
|
|
766
|
+
<path
|
|
767
|
+
d="M19 9l-7 7-7-7"
|
|
768
|
+
stroke="currentColor"
|
|
769
|
+
strokeWidth="2"
|
|
770
|
+
strokeLinecap="round"
|
|
771
|
+
strokeLinejoin="round"
|
|
772
|
+
/>
|
|
773
|
+
</svg>
|
|
774
|
+
</div>
|
|
775
|
+
|
|
776
|
+
{/* Clear filters button */}
|
|
777
|
+
<button
|
|
778
|
+
className={styles.clearFiltersButton}
|
|
779
|
+
onClick={(e) => {
|
|
780
|
+
e.stopPropagation(); // Prevent toggling when clicking the button
|
|
781
|
+
// Initialize with default values
|
|
782
|
+
const initialValues = {};
|
|
783
|
+
settings.filters.forEach((filter) => {
|
|
784
|
+
if (
|
|
785
|
+
filter.type === 'dropdown' &&
|
|
786
|
+
filter.id === 'year' &&
|
|
787
|
+
filter.value === 'now'
|
|
788
|
+
) {
|
|
789
|
+
initialValues[filter.id] = new Date()
|
|
790
|
+
.getFullYear()
|
|
791
|
+
.toString();
|
|
792
|
+
} else {
|
|
793
|
+
initialValues[filter.id] =
|
|
794
|
+
filter.value || '';
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
setSettingsFilterValues(initialValues);
|
|
798
|
+
|
|
799
|
+
// Reset fetch params
|
|
800
|
+
prevFetchParamsRef.current = null;
|
|
801
|
+
}}
|
|
802
|
+
>
|
|
803
|
+
<svg
|
|
804
|
+
viewBox="0 0 24 24"
|
|
805
|
+
fill="none"
|
|
806
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
807
|
+
>
|
|
808
|
+
<path
|
|
809
|
+
d="M19 7l-7 7-7-7"
|
|
810
|
+
stroke="currentColor"
|
|
811
|
+
strokeWidth="2"
|
|
812
|
+
strokeLinecap="round"
|
|
813
|
+
strokeLinejoin="round"
|
|
814
|
+
transform="rotate(90, 12, 12)"
|
|
815
|
+
/>
|
|
816
|
+
</svg>
|
|
817
|
+
Clear
|
|
818
|
+
</button>
|
|
819
|
+
</div>
|
|
820
|
+
|
|
821
|
+
<div
|
|
822
|
+
className={`${styles.settingsFiltersContent} ${
|
|
823
|
+
filtersCollapsed ? styles.collapsed : ''
|
|
824
|
+
}`}
|
|
825
|
+
>
|
|
826
|
+
{settings.filters.map((filter, index) => (
|
|
827
|
+
<div
|
|
828
|
+
key={`settings-filter-${index}`}
|
|
829
|
+
className={styles.settingsFilterItem}
|
|
830
|
+
>
|
|
831
|
+
<label className={styles.settingsFilterLabel}>
|
|
832
|
+
{filter.label}
|
|
833
|
+
</label>
|
|
834
|
+
{filter.type === 'dropdown' &&
|
|
835
|
+
filter.id === 'year' ? (
|
|
836
|
+
<select
|
|
837
|
+
className={
|
|
838
|
+
styles.settingsFilterDropdown
|
|
839
|
+
}
|
|
840
|
+
value={
|
|
841
|
+
settingsFilterValues[filter.id] ||
|
|
842
|
+
''
|
|
843
|
+
}
|
|
844
|
+
onChange={(e) =>
|
|
845
|
+
handleSettingsFilterChange(
|
|
846
|
+
filter.id,
|
|
847
|
+
e.target.value
|
|
848
|
+
)
|
|
849
|
+
}
|
|
850
|
+
>
|
|
851
|
+
{generateYearOptions(
|
|
852
|
+
filter.startYear || 2000
|
|
853
|
+
).map((option) => (
|
|
854
|
+
<option
|
|
855
|
+
key={option.value}
|
|
856
|
+
value={option.value}
|
|
857
|
+
>
|
|
858
|
+
{option.label}
|
|
859
|
+
</option>
|
|
860
|
+
))}
|
|
861
|
+
</select>
|
|
862
|
+
) : filter.type === 'dropdown' ? (
|
|
863
|
+
<select
|
|
864
|
+
className={
|
|
865
|
+
styles.settingsFilterDropdown
|
|
866
|
+
}
|
|
867
|
+
value={
|
|
868
|
+
settingsFilterValues[filter.id] ||
|
|
869
|
+
''
|
|
870
|
+
}
|
|
871
|
+
onChange={(e) =>
|
|
872
|
+
handleSettingsFilterChange(
|
|
873
|
+
filter.id,
|
|
874
|
+
e.target.value
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
>
|
|
878
|
+
{filter.options &&
|
|
879
|
+
filter.options.map((option) => (
|
|
880
|
+
<option
|
|
881
|
+
key={option.value}
|
|
882
|
+
value={option.value}
|
|
883
|
+
>
|
|
884
|
+
{option.label}
|
|
885
|
+
</option>
|
|
886
|
+
))}
|
|
887
|
+
</select>
|
|
888
|
+
) : filter.type === 'text' ? (
|
|
889
|
+
<input
|
|
890
|
+
type="text"
|
|
891
|
+
className={styles.settingsFilterInput}
|
|
892
|
+
value={
|
|
893
|
+
settingsFilterValues[filter.id] ||
|
|
894
|
+
''
|
|
895
|
+
}
|
|
896
|
+
onChange={(e) =>
|
|
897
|
+
handleSettingsFilterChange(
|
|
898
|
+
filter.id,
|
|
899
|
+
e.target.value
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
placeholder={filter.placeholder || ''}
|
|
903
|
+
/>
|
|
904
|
+
) : null}
|
|
905
|
+
</div>
|
|
906
|
+
))}
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
)}
|
|
910
|
+
|
|
911
|
+
{loading ? (
|
|
912
|
+
<div className={styles.loadingContainer}>
|
|
913
|
+
<div className={styles.loadingSpinner}></div>
|
|
914
|
+
<p>Loading data...</p>
|
|
915
|
+
</div>
|
|
916
|
+
) : (
|
|
917
|
+
<table className={styles.gridTable}>
|
|
918
|
+
<thead>
|
|
919
|
+
<tr>
|
|
920
|
+
{/* Use gridHeaders if available, otherwise fall back to headers from columns */}
|
|
921
|
+
{gridHeaders.length > 0
|
|
922
|
+
? gridHeaders.map((header, index) => (
|
|
923
|
+
<th
|
|
924
|
+
key={`header-${index}`}
|
|
925
|
+
className={`${styles.gridHeader} ${styles.sortableHeader}`}
|
|
926
|
+
onClick={() =>
|
|
927
|
+
requestSort(header.key)
|
|
928
|
+
}
|
|
929
|
+
style={{ cursor: 'pointer' }}
|
|
930
|
+
>
|
|
931
|
+
<div className={styles.headerContent}>
|
|
932
|
+
{header.label}
|
|
933
|
+
<span
|
|
934
|
+
className={
|
|
935
|
+
styles.sortIndicator
|
|
936
|
+
}
|
|
937
|
+
style={getSortIndicatorStyle(
|
|
938
|
+
header.key
|
|
939
|
+
)}
|
|
940
|
+
>
|
|
941
|
+
{sortConfig.key ===
|
|
942
|
+
header.key &&
|
|
943
|
+
sortConfig.direction === 'asc'
|
|
944
|
+
? ' ▲'
|
|
945
|
+
: ' ▼'}
|
|
946
|
+
</span>
|
|
947
|
+
</div>
|
|
948
|
+
</th>
|
|
949
|
+
))
|
|
950
|
+
: headers.map((header, index) => (
|
|
951
|
+
<th
|
|
952
|
+
key={`header-${index}`}
|
|
953
|
+
className={styles.gridHeader}
|
|
954
|
+
>
|
|
955
|
+
{header}
|
|
956
|
+
</th>
|
|
957
|
+
))}
|
|
958
|
+
</tr>
|
|
959
|
+
{/* Filter row */}
|
|
960
|
+
{gridHeaders.some((header) => header.filter) && (
|
|
961
|
+
<tr className={styles.filterRow}>
|
|
962
|
+
{gridHeaders.map((header, index) => (
|
|
963
|
+
<th
|
|
964
|
+
key={`filter-${index}`}
|
|
965
|
+
className={styles.filterCell}
|
|
966
|
+
>
|
|
967
|
+
{header.filter ? (
|
|
968
|
+
<input
|
|
969
|
+
type={
|
|
970
|
+
header.filter.type ===
|
|
971
|
+
'date'
|
|
972
|
+
? 'date'
|
|
973
|
+
: 'text'
|
|
974
|
+
}
|
|
975
|
+
className={styles.filterInput}
|
|
976
|
+
placeholder={`Filter ${header.label}...`}
|
|
977
|
+
value={
|
|
978
|
+
filterValues[header.key] ||
|
|
979
|
+
''
|
|
980
|
+
}
|
|
981
|
+
onChange={(e) =>
|
|
982
|
+
handleFilterChange(
|
|
983
|
+
header.key,
|
|
984
|
+
e.target.value
|
|
985
|
+
)
|
|
986
|
+
}
|
|
987
|
+
/>
|
|
988
|
+
) : null}
|
|
989
|
+
</th>
|
|
990
|
+
))}
|
|
991
|
+
</tr>
|
|
992
|
+
)}
|
|
993
|
+
</thead>
|
|
994
|
+
<tbody>
|
|
995
|
+
{sortedData && sortedData.length > 0 ? (
|
|
996
|
+
sortedData.map((row, rowIndex) => (
|
|
997
|
+
<tr
|
|
998
|
+
key={`row-${rowIndex}`}
|
|
999
|
+
className={styles.gridRow}
|
|
1000
|
+
style={getCustomStyles().hoverColor}
|
|
1001
|
+
>
|
|
1002
|
+
{/* Use gridHeaders if available, otherwise fall back to columns */}
|
|
1003
|
+
{gridHeaders.length > 0
|
|
1004
|
+
? gridHeaders.map(
|
|
1005
|
+
(header, colIndex) => (
|
|
1006
|
+
<td
|
|
1007
|
+
key={`cell-${rowIndex}-${colIndex}`}
|
|
1008
|
+
className={`${
|
|
1009
|
+
styles.gridCell
|
|
1010
|
+
} ${
|
|
1011
|
+
header.onClick &&
|
|
1012
|
+
!row[header.key]
|
|
1013
|
+
? styles.clickableCell
|
|
1014
|
+
: ''
|
|
1015
|
+
}`}
|
|
1016
|
+
onClick={
|
|
1017
|
+
header.onClick &&
|
|
1018
|
+
!row[header.key]
|
|
1019
|
+
? async () => {
|
|
1020
|
+
try {
|
|
1021
|
+
// Replace {id} in the URL with the row's ID
|
|
1022
|
+
const url =
|
|
1023
|
+
header.onClick.url.replace(
|
|
1024
|
+
'{id}',
|
|
1025
|
+
row.id
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
// Show loading toast
|
|
1029
|
+
toast.info(
|
|
1030
|
+
'Updating...',
|
|
1031
|
+
{
|
|
1032
|
+
autoClose: 1000,
|
|
1033
|
+
}
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
// Extract field and value from params
|
|
1037
|
+
const {
|
|
1038
|
+
field,
|
|
1039
|
+
value,
|
|
1040
|
+
...otherParams
|
|
1041
|
+
} =
|
|
1042
|
+
header
|
|
1043
|
+
.onClick
|
|
1044
|
+
.params;
|
|
1045
|
+
|
|
1046
|
+
// Create a new params object with the field as the key
|
|
1047
|
+
const params =
|
|
1048
|
+
{
|
|
1049
|
+
...otherParams,
|
|
1050
|
+
_method:
|
|
1051
|
+
header
|
|
1052
|
+
.onClick
|
|
1053
|
+
.method, // Add _method for PUT/DELETE requests
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
// Set the field as the key with the value
|
|
1057
|
+
// If value is "now", replace it with current date in ISO format
|
|
1058
|
+
params[
|
|
1059
|
+
field
|
|
1060
|
+
] =
|
|
1061
|
+
value ===
|
|
1062
|
+
'now'
|
|
1063
|
+
? new Date().toISOString()
|
|
1064
|
+
: value;
|
|
1065
|
+
|
|
1066
|
+
// Make the API call
|
|
1067
|
+
const response =
|
|
1068
|
+
await CustomFetch(
|
|
1069
|
+
url,
|
|
1070
|
+
header
|
|
1071
|
+
.onClick
|
|
1072
|
+
.method,
|
|
1073
|
+
params
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
if (
|
|
1077
|
+
response
|
|
1078
|
+
.data
|
|
1079
|
+
.error
|
|
1080
|
+
) {
|
|
1081
|
+
toast.error(
|
|
1082
|
+
response
|
|
1083
|
+
.data
|
|
1084
|
+
.error
|
|
1085
|
+
);
|
|
1086
|
+
} else {
|
|
1087
|
+
toast.success(
|
|
1088
|
+
'Updated successfully',
|
|
1089
|
+
{
|
|
1090
|
+
autoClose: 1000,
|
|
1091
|
+
}
|
|
1092
|
+
);
|
|
1093
|
+
// Refresh the data
|
|
1094
|
+
prevFetchParamsRef.current =
|
|
1095
|
+
null;
|
|
1096
|
+
// Force a re-fetch
|
|
1097
|
+
const fetchUrl =
|
|
1098
|
+
settings
|
|
1099
|
+
.fetch
|
|
1100
|
+
.url;
|
|
1101
|
+
const fetchMethod =
|
|
1102
|
+
settings
|
|
1103
|
+
.fetch
|
|
1104
|
+
.method ||
|
|
1105
|
+
'POST';
|
|
1106
|
+
const fetchParams =
|
|
1107
|
+
{
|
|
1108
|
+
...(settings
|
|
1109
|
+
.fetch
|
|
1110
|
+
.params ||
|
|
1111
|
+
{}),
|
|
1112
|
+
...settingsFilterValues,
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
setLoading(
|
|
1116
|
+
true
|
|
1117
|
+
);
|
|
1118
|
+
try {
|
|
1119
|
+
const fetchResponse =
|
|
1120
|
+
await CustomFetch(
|
|
1121
|
+
fetchUrl,
|
|
1122
|
+
fetchMethod,
|
|
1123
|
+
fetchParams
|
|
1124
|
+
);
|
|
1125
|
+
if (
|
|
1126
|
+
fetchResponse
|
|
1127
|
+
.data
|
|
1128
|
+
.data
|
|
1129
|
+
) {
|
|
1130
|
+
if (
|
|
1131
|
+
fetchResponse
|
|
1132
|
+
.data
|
|
1133
|
+
.data
|
|
1134
|
+
.header &&
|
|
1135
|
+
fetchResponse
|
|
1136
|
+
.data
|
|
1137
|
+
.data
|
|
1138
|
+
.rows
|
|
1139
|
+
) {
|
|
1140
|
+
setGridData(
|
|
1141
|
+
fetchResponse
|
|
1142
|
+
.data
|
|
1143
|
+
.data
|
|
1144
|
+
.rows ||
|
|
1145
|
+
[]
|
|
1146
|
+
);
|
|
1147
|
+
setSortedData(
|
|
1148
|
+
fetchResponse
|
|
1149
|
+
.data
|
|
1150
|
+
.data
|
|
1151
|
+
.rows ||
|
|
1152
|
+
[]
|
|
1153
|
+
);
|
|
1154
|
+
} else {
|
|
1155
|
+
setGridData(
|
|
1156
|
+
fetchResponse
|
|
1157
|
+
.data
|
|
1158
|
+
.data
|
|
1159
|
+
.data ||
|
|
1160
|
+
[]
|
|
1161
|
+
);
|
|
1162
|
+
setSortedData(
|
|
1163
|
+
fetchResponse
|
|
1164
|
+
.data
|
|
1165
|
+
.data
|
|
1166
|
+
.data ||
|
|
1167
|
+
[]
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
console.error(
|
|
1173
|
+
'Error refreshing data:',
|
|
1174
|
+
error
|
|
1175
|
+
);
|
|
1176
|
+
} finally {
|
|
1177
|
+
setLoading(
|
|
1178
|
+
false
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
console.error(
|
|
1184
|
+
'Error updating cell:',
|
|
1185
|
+
error
|
|
1186
|
+
);
|
|
1187
|
+
toast.error(
|
|
1188
|
+
'An error occurred while updating'
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
: undefined
|
|
1193
|
+
}
|
|
1194
|
+
>
|
|
1195
|
+
{row[header.key] ? (
|
|
1196
|
+
header.onClick ? (
|
|
1197
|
+
<span
|
|
1198
|
+
className={
|
|
1199
|
+
styles.cellWithValue
|
|
1200
|
+
}
|
|
1201
|
+
>
|
|
1202
|
+
{formatCellContent(
|
|
1203
|
+
row[
|
|
1204
|
+
header
|
|
1205
|
+
.key
|
|
1206
|
+
],
|
|
1207
|
+
{
|
|
1208
|
+
type:
|
|
1209
|
+
header.type ||
|
|
1210
|
+
(typeof row[
|
|
1211
|
+
header
|
|
1212
|
+
.key
|
|
1213
|
+
] ===
|
|
1214
|
+
'number'
|
|
1215
|
+
? 'number'
|
|
1216
|
+
: ''),
|
|
1217
|
+
}
|
|
1218
|
+
)}
|
|
1219
|
+
</span>
|
|
1220
|
+
) : (
|
|
1221
|
+
formatCellContent(
|
|
1222
|
+
row[
|
|
1223
|
+
header.key
|
|
1224
|
+
],
|
|
1225
|
+
{
|
|
1226
|
+
type:
|
|
1227
|
+
header.type ||
|
|
1228
|
+
(typeof row[
|
|
1229
|
+
header
|
|
1230
|
+
.key
|
|
1231
|
+
] ===
|
|
1232
|
+
'number'
|
|
1233
|
+
? 'number'
|
|
1234
|
+
: ''),
|
|
1235
|
+
}
|
|
1236
|
+
)
|
|
1237
|
+
)
|
|
1238
|
+
) : header.onClick ? (
|
|
1239
|
+
<span
|
|
1240
|
+
className={
|
|
1241
|
+
styles.placeholderText
|
|
1242
|
+
}
|
|
1243
|
+
>
|
|
1244
|
+
{header.onClick
|
|
1245
|
+
.placeholder ||
|
|
1246
|
+
'Click to set'}
|
|
1247
|
+
</span>
|
|
1248
|
+
) : (
|
|
1249
|
+
''
|
|
1250
|
+
)}
|
|
1251
|
+
</td>
|
|
1252
|
+
)
|
|
1253
|
+
)
|
|
1254
|
+
: columns.map((col, colIndex) => (
|
|
1255
|
+
<td
|
|
1256
|
+
key={`cell-${rowIndex}-${colIndex}`}
|
|
1257
|
+
className={styles.gridCell}
|
|
1258
|
+
>
|
|
1259
|
+
{formatCellContent(
|
|
1260
|
+
row[col.name],
|
|
1261
|
+
col
|
|
1262
|
+
)}
|
|
1263
|
+
</td>
|
|
1264
|
+
))}
|
|
1265
|
+
</tr>
|
|
1266
|
+
))
|
|
1267
|
+
) : (
|
|
1268
|
+
<tr>
|
|
1269
|
+
<td
|
|
1270
|
+
colSpan={
|
|
1271
|
+
gridHeaders.length || headers.length
|
|
1272
|
+
}
|
|
1273
|
+
className={styles.noData}
|
|
1274
|
+
>
|
|
1275
|
+
No data available
|
|
1276
|
+
</td>
|
|
1277
|
+
</tr>
|
|
1278
|
+
)}
|
|
1279
|
+
</tbody>
|
|
1280
|
+
</table>
|
|
1281
|
+
)}
|
|
1282
|
+
</div>
|
|
1283
|
+
);
|
|
1284
|
+
};
|
|
1285
|
+
|
|
197
1286
|
const ReportForm = ({ config, userProfile }) => {
|
|
198
1287
|
const [formData, setFormData] = useState({});
|
|
199
1288
|
|
|
@@ -410,8 +1499,6 @@ function GenericIndex({
|
|
|
410
1499
|
|
|
411
1500
|
const { data, method, message, url } = checkboxUpdateConfig;
|
|
412
1501
|
|
|
413
|
-
console.info('Selected rows:', rowsSelectedRef.current);
|
|
414
|
-
|
|
415
1502
|
if (!Object.keys(rowsSelectedRef.current).length) {
|
|
416
1503
|
toast.warning(message.warning);
|
|
417
1504
|
return;
|
|
@@ -585,6 +1672,8 @@ function GenericIndex({
|
|
|
585
1672
|
|
|
586
1673
|
const renderContentBasedOnType = useCallback(() => {
|
|
587
1674
|
switch (config.type) {
|
|
1675
|
+
case 'grid':
|
|
1676
|
+
return <Grid config={config} userProfile={userProfile} />;
|
|
588
1677
|
case 'table':
|
|
589
1678
|
return renderTable();
|
|
590
1679
|
case 'report':
|
|
@@ -664,7 +1753,13 @@ function GenericIndex({
|
|
|
664
1753
|
</div>
|
|
665
1754
|
);
|
|
666
1755
|
}
|
|
667
|
-
}, [
|
|
1756
|
+
}, [
|
|
1757
|
+
layout,
|
|
1758
|
+
subnav,
|
|
1759
|
+
renderContentBasedOnType,
|
|
1760
|
+
handleFilterChange,
|
|
1761
|
+
setting.tabs,
|
|
1762
|
+
]);
|
|
668
1763
|
|
|
669
1764
|
useEffect(() => {
|
|
670
1765
|
if (setting.page?.tableInfo?.hasOwnProperty('url')) {
|
|
@@ -696,6 +1791,14 @@ function GenericIndex({
|
|
|
696
1791
|
|
|
697
1792
|
// Add an effect to update the config when the active tab changes
|
|
698
1793
|
useEffect(() => {
|
|
1794
|
+
// Skip if no subnav or tabs
|
|
1795
|
+
if (!subnav?.length || !setting.tabs?.length) {
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// Create a ref to track if we've already processed this combination
|
|
1800
|
+
const activeFilterRef = useRef(null);
|
|
1801
|
+
|
|
699
1802
|
// Find the active filter and its child
|
|
700
1803
|
const activeFilter = subnav?.find((filter) => {
|
|
701
1804
|
if (filter.isParent) {
|
|
@@ -707,6 +1810,11 @@ function GenericIndex({
|
|
|
707
1810
|
return filter.show === true;
|
|
708
1811
|
});
|
|
709
1812
|
|
|
1813
|
+
// Skip if no active filter
|
|
1814
|
+
if (!activeFilter) {
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
710
1818
|
if (
|
|
711
1819
|
activeFilter?.isParent &&
|
|
712
1820
|
activeFilter.children &&
|
|
@@ -722,6 +1830,20 @@ function GenericIndex({
|
|
|
722
1830
|
const firstChild = activeFilter.children[0];
|
|
723
1831
|
const firstChildId = firstChild.id;
|
|
724
1832
|
|
|
1833
|
+
// Create a string representation for comparison
|
|
1834
|
+
const activeFilterString = JSON.stringify({
|
|
1835
|
+
parentId: activeFilter.id,
|
|
1836
|
+
childId: firstChildId,
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
// Skip if we've already processed this combination
|
|
1840
|
+
if (activeFilterRef.current === activeFilterString) {
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// Update the ref
|
|
1845
|
+
activeFilterRef.current = activeFilterString;
|
|
1846
|
+
|
|
725
1847
|
// Find the tab config for the first child
|
|
726
1848
|
const firstChildTabConfig = setting.tabs?.find(
|
|
727
1849
|
(tab) => tab.id === firstChildId
|
|
@@ -729,18 +1851,25 @@ function GenericIndex({
|
|
|
729
1851
|
|
|
730
1852
|
if (firstChildTabConfig) {
|
|
731
1853
|
// Update the config with the first child's tab config
|
|
732
|
-
setConfig((prevConfig) =>
|
|
733
|
-
|
|
734
|
-
id
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1854
|
+
setConfig((prevConfig) => {
|
|
1855
|
+
// Skip update if the ID is already set correctly
|
|
1856
|
+
if (prevConfig.id === firstChildTabConfig.id) {
|
|
1857
|
+
return prevConfig;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
return {
|
|
1861
|
+
...prevConfig,
|
|
1862
|
+
id: firstChildTabConfig.id,
|
|
1863
|
+
functions:
|
|
1864
|
+
firstChildTabConfig.functions ||
|
|
1865
|
+
prevConfig.functions ||
|
|
1866
|
+
{},
|
|
1867
|
+
};
|
|
1868
|
+
});
|
|
740
1869
|
}
|
|
741
1870
|
}
|
|
742
1871
|
}
|
|
743
|
-
}, [subnav, setting.tabs
|
|
1872
|
+
}, [subnav, setting.tabs]);
|
|
744
1873
|
|
|
745
1874
|
return (
|
|
746
1875
|
<>
|
|
@@ -826,16 +1955,6 @@ function GenericIndex({
|
|
|
826
1955
|
(tab) => tab.id === firstChild.id
|
|
827
1956
|
);
|
|
828
1957
|
|
|
829
|
-
// Log for debugging
|
|
830
|
-
console.log(
|
|
831
|
-
'No active child found, using first child:',
|
|
832
|
-
firstChild.id
|
|
833
|
-
);
|
|
834
|
-
console.log(
|
|
835
|
-
'First child tab config:',
|
|
836
|
-
firstChildTabConfig
|
|
837
|
-
);
|
|
838
|
-
|
|
839
1958
|
return {
|
|
840
1959
|
activeTabId: firstChild.id,
|
|
841
1960
|
activeTabConfig: firstChildTabConfig,
|