@visns-studio/visns-components 5.10.11 → 5.11.1
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.
- package/README.md +85 -0
- package/package.json +6 -6
- package/src/components/crm/generic/ContactSelectorModal.jsx +380 -0
- package/src/components/crm/generic/DatePickerDialog.jsx +302 -0
- package/src/components/crm/generic/GenericGrid.jsx +1534 -0
- package/src/components/crm/generic/GenericIndex.jsx +5 -1255
- package/src/components/crm/generic/GenericReportForm.jsx +194 -0
- package/src/components/crm/generic/shared/formatters.js +125 -0
- package/src/components/crm/generic/shared/gridUtils.js +194 -0
- package/src/components/crm/generic/styles/ContactSelectorModal.css +367 -0
- package/src/components/crm/generic/styles/DatePickerDialog.css +84 -0
- package/src/components/crm/generic/styles/GenericIndex.module.scss +176 -0
- package/src/index.js +4 -0
|
@@ -3,7 +3,6 @@ import '../../styles/global.css';
|
|
|
3
3
|
import React, {
|
|
4
4
|
useCallback,
|
|
5
5
|
useEffect,
|
|
6
|
-
useMemo,
|
|
7
6
|
useRef,
|
|
8
7
|
useState,
|
|
9
8
|
} from 'react';
|
|
@@ -13,19 +12,17 @@ import { saveAs } from 'file-saver';
|
|
|
13
12
|
import _ from 'lodash';
|
|
14
13
|
import parse from 'html-react-parser';
|
|
15
14
|
import moment from 'moment';
|
|
16
|
-
import numeral from 'numeral';
|
|
17
|
-
import { CloudDownload } from 'akar-icons';
|
|
18
15
|
import Popup from 'reactjs-popup';
|
|
19
16
|
|
|
20
17
|
import CustomFetch from '../Fetch';
|
|
21
18
|
import Download from '../Download';
|
|
22
|
-
import Field from '../Field';
|
|
23
19
|
import Form from '../Form';
|
|
24
20
|
import Table from '../DataGrid';
|
|
25
21
|
import TableFilter from '../TableFilter';
|
|
22
|
+
import GenericGrid from './GenericGrid';
|
|
23
|
+
import GenericReportForm from './GenericReportForm';
|
|
26
24
|
|
|
27
25
|
import styles from './styles/GenericIndex.module.scss';
|
|
28
|
-
import reportStyles from './styles/ReportForm.module.scss';
|
|
29
26
|
|
|
30
27
|
function useWindowHeight() {
|
|
31
28
|
const [windowHeight, setWindowHeight] = useState(window.innerHeight);
|
|
@@ -226,1253 +223,6 @@ function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
|
226
223
|
return { config, setConfig, subnav, setSubnav };
|
|
227
224
|
}
|
|
228
225
|
|
|
229
|
-
const Grid = ({ config, userProfile }) => {
|
|
230
|
-
const [gridData, setGridData] = useState([]);
|
|
231
|
-
const [sortedData, setSortedData] = useState([]);
|
|
232
|
-
const [filteredData, setFilteredData] = useState([]);
|
|
233
|
-
const [filterValues, setFilterValues] = useState({});
|
|
234
|
-
const [settingsFilterValues, setSettingsFilterValues] = useState({});
|
|
235
|
-
const [loading, setLoading] = useState(false);
|
|
236
|
-
const [filtersCollapsed, setFiltersCollapsed] = useState(false);
|
|
237
|
-
const [sortConfig, setSortConfig] = useState({
|
|
238
|
-
key: null,
|
|
239
|
-
direction: 'asc',
|
|
240
|
-
});
|
|
241
|
-
const prevFetchParamsRef = useRef(null);
|
|
242
|
-
|
|
243
|
-
// Extract data and settings from config using useMemo to prevent unnecessary re-renders
|
|
244
|
-
const {
|
|
245
|
-
settings,
|
|
246
|
-
columns = [],
|
|
247
|
-
data = [],
|
|
248
|
-
} = useMemo(
|
|
249
|
-
() => ({
|
|
250
|
-
settings: config.settings || {},
|
|
251
|
-
columns: config.columns || [],
|
|
252
|
-
data: config.data || [],
|
|
253
|
-
}),
|
|
254
|
-
[config.settings, config.columns, config.data]
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
// Create table headers from columns
|
|
258
|
-
const headers = useMemo(
|
|
259
|
-
() => columns.map((col) => col.header || col.name || ''),
|
|
260
|
-
[columns]
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
// Format cell content based on column type or value type
|
|
264
|
-
const formatCellContent = (value, column) => {
|
|
265
|
-
if (value === null || value === undefined) return '';
|
|
266
|
-
if (value === 0) return '0'; // Explicitly handle zero values
|
|
267
|
-
if (value === '') return '';
|
|
268
|
-
|
|
269
|
-
// Try to infer the type if not provided
|
|
270
|
-
const valueType = column.type || typeof value;
|
|
271
|
-
|
|
272
|
-
switch (valueType) {
|
|
273
|
-
case 'date':
|
|
274
|
-
return moment(value).isValid()
|
|
275
|
-
? moment(value).format(column.format || 'DD/MM/YYYY')
|
|
276
|
-
: value;
|
|
277
|
-
case 'datetime':
|
|
278
|
-
return moment(value).isValid()
|
|
279
|
-
? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
|
|
280
|
-
: value;
|
|
281
|
-
case 'currency':
|
|
282
|
-
return numeral(value).format(column.format || '$0,0.00');
|
|
283
|
-
case 'number':
|
|
284
|
-
// Check if it's actually a number before formatting
|
|
285
|
-
return !isNaN(parseFloat(value)) && isFinite(value)
|
|
286
|
-
? numeral(value).format(column.format || '0,0')
|
|
287
|
-
: value;
|
|
288
|
-
case 'boolean':
|
|
289
|
-
if (
|
|
290
|
-
value === 1 ||
|
|
291
|
-
value === true ||
|
|
292
|
-
value === 'true' ||
|
|
293
|
-
value === 'yes' ||
|
|
294
|
-
value === 'Yes'
|
|
295
|
-
) {
|
|
296
|
-
return 'Yes';
|
|
297
|
-
}
|
|
298
|
-
if (
|
|
299
|
-
value === 0 ||
|
|
300
|
-
value === false ||
|
|
301
|
-
value === 'false' ||
|
|
302
|
-
value === 'no' ||
|
|
303
|
-
value === 'No'
|
|
304
|
-
) {
|
|
305
|
-
return 'No';
|
|
306
|
-
}
|
|
307
|
-
return value;
|
|
308
|
-
case 'year':
|
|
309
|
-
// Special handling for year values
|
|
310
|
-
return value.toString();
|
|
311
|
-
case 'quarter':
|
|
312
|
-
// Special handling for quarter values (Q1, Q2, etc.)
|
|
313
|
-
if (value && value.toString().startsWith('Q')) {
|
|
314
|
-
return value;
|
|
315
|
-
}
|
|
316
|
-
return value;
|
|
317
|
-
case 'html':
|
|
318
|
-
// Explicitly parse HTML content
|
|
319
|
-
return parse(String(value));
|
|
320
|
-
case 'richtext':
|
|
321
|
-
// Parse rich text content
|
|
322
|
-
return parse(String(value));
|
|
323
|
-
default:
|
|
324
|
-
// For text values, check if it contains HTML tags
|
|
325
|
-
if (
|
|
326
|
-
typeof value === 'string' &&
|
|
327
|
-
(value.includes('<br') ||
|
|
328
|
-
value.includes('<p>') ||
|
|
329
|
-
value.includes('<div') ||
|
|
330
|
-
value.includes('<span'))
|
|
331
|
-
) {
|
|
332
|
-
// Ensure <br /> tags are properly handled
|
|
333
|
-
const processedValue = String(value)
|
|
334
|
-
.replace(/<br \/>/g, '<br>')
|
|
335
|
-
.replace(/<br\/>/g, '<br>');
|
|
336
|
-
return parse(processedValue);
|
|
337
|
-
}
|
|
338
|
-
return value;
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
// State for headers from API response
|
|
343
|
-
const [gridHeaders, setGridHeaders] = useState([]);
|
|
344
|
-
|
|
345
|
-
// Function to sort data
|
|
346
|
-
const sortData = useCallback(
|
|
347
|
-
(data, key, direction) => {
|
|
348
|
-
if (!key) return [...data];
|
|
349
|
-
|
|
350
|
-
return [...data].sort((a, b) => {
|
|
351
|
-
// Find the header config for this key to determine type
|
|
352
|
-
const headerConfig = gridHeaders.find((h) => h.key === key);
|
|
353
|
-
const type = headerConfig?.type || 'text';
|
|
354
|
-
|
|
355
|
-
// Get values, handling null/undefined
|
|
356
|
-
let aValue =
|
|
357
|
-
a[key] === null || a[key] === undefined ? '' : a[key];
|
|
358
|
-
let bValue =
|
|
359
|
-
b[key] === null || b[key] === undefined ? '' : b[key];
|
|
360
|
-
|
|
361
|
-
// Sort based on type
|
|
362
|
-
switch (type) {
|
|
363
|
-
case 'number':
|
|
364
|
-
case 'year':
|
|
365
|
-
aValue = parseFloat(aValue) || 0;
|
|
366
|
-
bValue = parseFloat(bValue) || 0;
|
|
367
|
-
break;
|
|
368
|
-
case 'date':
|
|
369
|
-
// Convert to timestamp for comparison
|
|
370
|
-
aValue = aValue ? new Date(aValue).getTime() : 0;
|
|
371
|
-
bValue = bValue ? new Date(bValue).getTime() : 0;
|
|
372
|
-
break;
|
|
373
|
-
default:
|
|
374
|
-
// For text, convert to lowercase strings
|
|
375
|
-
aValue = String(aValue).toLowerCase();
|
|
376
|
-
bValue = String(bValue).toLowerCase();
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Compare values
|
|
380
|
-
if (aValue < bValue) {
|
|
381
|
-
return direction === 'asc' ? -1 : 1;
|
|
382
|
-
}
|
|
383
|
-
if (aValue > bValue) {
|
|
384
|
-
return direction === 'asc' ? 1 : -1;
|
|
385
|
-
}
|
|
386
|
-
return 0;
|
|
387
|
-
});
|
|
388
|
-
},
|
|
389
|
-
[gridHeaders]
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
// Handle sort request
|
|
393
|
-
const requestSort = useCallback((key) => {
|
|
394
|
-
// If clicking the same column, toggle direction
|
|
395
|
-
// Otherwise, start with ascending sort for the new column
|
|
396
|
-
setSortConfig((prevConfig) => {
|
|
397
|
-
const newDirection =
|
|
398
|
-
prevConfig.key === key && prevConfig.direction === 'asc'
|
|
399
|
-
? 'desc'
|
|
400
|
-
: 'asc';
|
|
401
|
-
|
|
402
|
-
return { key, direction: newDirection };
|
|
403
|
-
});
|
|
404
|
-
}, []);
|
|
405
|
-
|
|
406
|
-
// Function to filter data based on filter values
|
|
407
|
-
const filterData = useCallback(
|
|
408
|
-
(data) => {
|
|
409
|
-
// If no filter values are set, return the original data
|
|
410
|
-
if (Object.keys(filterValues).length === 0) {
|
|
411
|
-
return data;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Filter the data based on the filter values
|
|
415
|
-
return data.filter((row) => {
|
|
416
|
-
// Check each filter
|
|
417
|
-
return Object.entries(filterValues).every(([key, value]) => {
|
|
418
|
-
// Skip empty filter values
|
|
419
|
-
if (!value) return true;
|
|
420
|
-
|
|
421
|
-
// Get the row value
|
|
422
|
-
const rowValue = row[key];
|
|
423
|
-
if (rowValue === null || rowValue === undefined)
|
|
424
|
-
return false;
|
|
425
|
-
|
|
426
|
-
// Find the header config for this key
|
|
427
|
-
const headerConfig = gridHeaders.find((h) => h.key === key);
|
|
428
|
-
if (!headerConfig || !headerConfig.filter) return true;
|
|
429
|
-
|
|
430
|
-
const { type, operator } = headerConfig.filter;
|
|
431
|
-
|
|
432
|
-
// Apply filter based on type and operator
|
|
433
|
-
switch (type) {
|
|
434
|
-
case 'string':
|
|
435
|
-
const rowStr = String(rowValue).toLowerCase();
|
|
436
|
-
const filterStr = String(value).toLowerCase();
|
|
437
|
-
|
|
438
|
-
switch (operator) {
|
|
439
|
-
case 'equals':
|
|
440
|
-
return rowStr === filterStr;
|
|
441
|
-
case 'contains':
|
|
442
|
-
return rowStr.includes(filterStr);
|
|
443
|
-
case 'startsWith':
|
|
444
|
-
return rowStr.startsWith(filterStr);
|
|
445
|
-
case 'endsWith':
|
|
446
|
-
return rowStr.endsWith(filterStr);
|
|
447
|
-
default:
|
|
448
|
-
return rowStr.includes(filterStr);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
case 'number':
|
|
452
|
-
const rowNum = parseFloat(rowValue);
|
|
453
|
-
const filterNum = parseFloat(value);
|
|
454
|
-
|
|
455
|
-
if (isNaN(rowNum) || isNaN(filterNum)) return false;
|
|
456
|
-
|
|
457
|
-
switch (operator) {
|
|
458
|
-
case 'equals':
|
|
459
|
-
return rowNum === filterNum;
|
|
460
|
-
case 'greaterThan':
|
|
461
|
-
return rowNum > filterNum;
|
|
462
|
-
case 'lessThan':
|
|
463
|
-
return rowNum < filterNum;
|
|
464
|
-
case 'greaterThanOrEqual':
|
|
465
|
-
return rowNum >= filterNum;
|
|
466
|
-
case 'lessThanOrEqual':
|
|
467
|
-
return rowNum <= filterNum;
|
|
468
|
-
default:
|
|
469
|
-
return rowNum === filterNum;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
case 'date':
|
|
473
|
-
const rowDate = new Date(rowValue);
|
|
474
|
-
const filterDate = new Date(value);
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
isNaN(rowDate.getTime()) ||
|
|
478
|
-
isNaN(filterDate.getTime())
|
|
479
|
-
)
|
|
480
|
-
return false;
|
|
481
|
-
|
|
482
|
-
switch (operator) {
|
|
483
|
-
case 'equals':
|
|
484
|
-
return (
|
|
485
|
-
rowDate.toDateString() ===
|
|
486
|
-
filterDate.toDateString()
|
|
487
|
-
);
|
|
488
|
-
case 'after':
|
|
489
|
-
return rowDate > filterDate;
|
|
490
|
-
case 'before':
|
|
491
|
-
return rowDate < filterDate;
|
|
492
|
-
default:
|
|
493
|
-
return (
|
|
494
|
-
rowDate.toDateString() ===
|
|
495
|
-
filterDate.toDateString()
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
default:
|
|
500
|
-
return true;
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
});
|
|
504
|
-
},
|
|
505
|
-
[filterValues, gridHeaders]
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
// Handle filter change
|
|
509
|
-
const handleFilterChange = useCallback((key, value) => {
|
|
510
|
-
setFilterValues((prev) => ({
|
|
511
|
-
...prev,
|
|
512
|
-
[key]: value,
|
|
513
|
-
}));
|
|
514
|
-
}, []);
|
|
515
|
-
|
|
516
|
-
// Effect to update filteredData and sortedData when gridData, filterValues, or sortConfig changes
|
|
517
|
-
useEffect(() => {
|
|
518
|
-
if (gridData.length > 0) {
|
|
519
|
-
// First apply filters
|
|
520
|
-
const filtered = filterData(gridData);
|
|
521
|
-
setFilteredData(filtered);
|
|
522
|
-
|
|
523
|
-
// Then sort the filtered data
|
|
524
|
-
const sorted = sortData(
|
|
525
|
-
filtered,
|
|
526
|
-
sortConfig.key,
|
|
527
|
-
sortConfig.direction
|
|
528
|
-
);
|
|
529
|
-
setSortedData(sorted);
|
|
530
|
-
} else {
|
|
531
|
-
setFilteredData([]);
|
|
532
|
-
setSortedData([]);
|
|
533
|
-
}
|
|
534
|
-
}, [gridData, filterValues, sortConfig, sortData, filterData]);
|
|
535
|
-
|
|
536
|
-
// Load data from API if gridSetting.fetch is provided
|
|
537
|
-
useEffect(() => {
|
|
538
|
-
const fetchData = async () => {
|
|
539
|
-
// Only fetch if we have a URL
|
|
540
|
-
if (!settings?.fetch?.url) {
|
|
541
|
-
if (data && data.length > 0) {
|
|
542
|
-
// Use provided data if no fetch setting
|
|
543
|
-
setGridData(data);
|
|
544
|
-
}
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const url = settings.fetch.url;
|
|
549
|
-
const method = settings.fetch.method || 'POST';
|
|
550
|
-
|
|
551
|
-
// Combine base params with settings filter values
|
|
552
|
-
const params = {
|
|
553
|
-
...(settings.fetch.params || {}),
|
|
554
|
-
...settingsFilterValues,
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
// Create a string representation of the fetch parameters for comparison
|
|
558
|
-
const fetchParamsString = JSON.stringify({ url, method, params });
|
|
559
|
-
|
|
560
|
-
// Skip fetch if the parameters haven't changed
|
|
561
|
-
if (prevFetchParamsRef.current === fetchParamsString) {
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Update the ref with current parameters
|
|
566
|
-
prevFetchParamsRef.current = fetchParamsString;
|
|
567
|
-
|
|
568
|
-
setLoading(true);
|
|
569
|
-
try {
|
|
570
|
-
const response = await CustomFetch(url, method, params);
|
|
571
|
-
|
|
572
|
-
if (response.data.data) {
|
|
573
|
-
// Check if the response has the expected structure with header and rows
|
|
574
|
-
if (response.data.data.header && response.data.data.rows) {
|
|
575
|
-
// Set the headers from the response with the new structure
|
|
576
|
-
const headerEntries = Object.entries(
|
|
577
|
-
response.data.data.header
|
|
578
|
-
);
|
|
579
|
-
setGridHeaders(
|
|
580
|
-
headerEntries.map(([key, config]) => ({
|
|
581
|
-
key,
|
|
582
|
-
label: config.label || key,
|
|
583
|
-
sort: config.sort || key,
|
|
584
|
-
type: config.type || 'text',
|
|
585
|
-
filter: config.filter || null,
|
|
586
|
-
onClick: config.onClick || null,
|
|
587
|
-
}))
|
|
588
|
-
);
|
|
589
|
-
|
|
590
|
-
// Initialize filter values
|
|
591
|
-
const initialFilterValues = {};
|
|
592
|
-
headerEntries.forEach(([key, config]) => {
|
|
593
|
-
if (config.filter) {
|
|
594
|
-
initialFilterValues[key] = '';
|
|
595
|
-
}
|
|
596
|
-
});
|
|
597
|
-
setFilterValues(initialFilterValues);
|
|
598
|
-
|
|
599
|
-
// Set the rows data
|
|
600
|
-
setGridData(response.data.data.rows || []);
|
|
601
|
-
// Also set the initial sorted data
|
|
602
|
-
setSortedData(response.data.data.rows || []);
|
|
603
|
-
} else {
|
|
604
|
-
// Fallback to the previous behavior if the structure is different
|
|
605
|
-
setGridData(response.data.data.data || []);
|
|
606
|
-
setSortedData(response.data.data.data || []);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
} catch (error) {
|
|
610
|
-
console.error('Error fetching grid data:', error);
|
|
611
|
-
} finally {
|
|
612
|
-
setLoading(false);
|
|
613
|
-
}
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
fetchData();
|
|
617
|
-
}, [
|
|
618
|
-
// Only depend on the specific properties we need, not the entire objects
|
|
619
|
-
settings?.fetch?.url,
|
|
620
|
-
settings?.fetch?.method,
|
|
621
|
-
// Use JSON.stringify for the params to detect deep changes
|
|
622
|
-
JSON.stringify(settings?.fetch?.params),
|
|
623
|
-
// Only depend on data if we're not fetching
|
|
624
|
-
!settings?.fetch?.url ? JSON.stringify(data) : null,
|
|
625
|
-
]);
|
|
626
|
-
|
|
627
|
-
// Generate year options for dropdown
|
|
628
|
-
const generateYearOptions = useCallback((startYear) => {
|
|
629
|
-
const currentYear = new Date().getFullYear();
|
|
630
|
-
const years = [];
|
|
631
|
-
|
|
632
|
-
// Start from the specified year and go up to current year
|
|
633
|
-
for (let year = startYear; year <= currentYear + 1; year++) {
|
|
634
|
-
years.push({ value: year.toString(), label: year.toString() });
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return years;
|
|
638
|
-
}, []);
|
|
639
|
-
|
|
640
|
-
// Initialize settings filters
|
|
641
|
-
useEffect(() => {
|
|
642
|
-
if (settings?.filters && settings.filters.length > 0) {
|
|
643
|
-
const initialValues = {};
|
|
644
|
-
|
|
645
|
-
settings.filters.forEach((filter) => {
|
|
646
|
-
if (filter.type === 'dropdown' && filter.id === 'year') {
|
|
647
|
-
// For year dropdown with 'now' value, set to current year
|
|
648
|
-
if (filter.value === 'now') {
|
|
649
|
-
initialValues[filter.id] = new Date()
|
|
650
|
-
.getFullYear()
|
|
651
|
-
.toString();
|
|
652
|
-
} else {
|
|
653
|
-
initialValues[filter.id] = filter.value || '';
|
|
654
|
-
}
|
|
655
|
-
} else {
|
|
656
|
-
initialValues[filter.id] = filter.value || '';
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
setSettingsFilterValues(initialValues);
|
|
661
|
-
}
|
|
662
|
-
}, [settings?.filters]);
|
|
663
|
-
|
|
664
|
-
// Handle settings filter change
|
|
665
|
-
const handleSettingsFilterChange = useCallback(
|
|
666
|
-
(filterId, value) => {
|
|
667
|
-
setSettingsFilterValues((prev) => ({
|
|
668
|
-
...prev,
|
|
669
|
-
[filterId]: value,
|
|
670
|
-
}));
|
|
671
|
-
|
|
672
|
-
// Update fetch params with the new filter value
|
|
673
|
-
if (settings?.fetch?.params) {
|
|
674
|
-
const updatedParams = {
|
|
675
|
-
...settings.fetch.params,
|
|
676
|
-
[filterId]: value,
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
// Update the settings object
|
|
680
|
-
if (config.settings && config.settings.fetch) {
|
|
681
|
-
config.settings.fetch.params = updatedParams;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Trigger a re-fetch with the new params
|
|
685
|
-
prevFetchParamsRef.current = null;
|
|
686
|
-
}
|
|
687
|
-
},
|
|
688
|
-
[settings, config]
|
|
689
|
-
);
|
|
690
|
-
|
|
691
|
-
// Apply custom styling from userProfile if available
|
|
692
|
-
const getCustomStyles = () => {
|
|
693
|
-
const customStyles = {};
|
|
694
|
-
|
|
695
|
-
if (userProfile?.settings?.style) {
|
|
696
|
-
const { style } = userProfile.settings;
|
|
697
|
-
|
|
698
|
-
if (style.hoverColor) {
|
|
699
|
-
customStyles.hoverColor = {
|
|
700
|
-
'--hover-color': style.hoverColor,
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
return customStyles;
|
|
706
|
-
};
|
|
707
|
-
|
|
708
|
-
// CSS for sort indicators
|
|
709
|
-
const getSortIndicatorStyle = (key) => {
|
|
710
|
-
if (sortConfig.key !== key) {
|
|
711
|
-
return { opacity: 0.3 }; // Dim the icon when not sorting by this column
|
|
712
|
-
}
|
|
713
|
-
return {}; // Full opacity when sorting by this column
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
return (
|
|
717
|
-
<div className={styles.gridContainer}>
|
|
718
|
-
{/* Settings Filters */}
|
|
719
|
-
{settings?.filters && settings.filters.length > 0 && (
|
|
720
|
-
<div className={styles.settingsFiltersContainer}>
|
|
721
|
-
<div className={styles.settingsFiltersHeader}>
|
|
722
|
-
<div
|
|
723
|
-
className={styles.headerLeft}
|
|
724
|
-
onClick={() =>
|
|
725
|
-
setFiltersCollapsed(!filtersCollapsed)
|
|
726
|
-
}
|
|
727
|
-
>
|
|
728
|
-
<div className={styles.settingsFiltersTitle}>
|
|
729
|
-
<svg
|
|
730
|
-
width="16"
|
|
731
|
-
height="16"
|
|
732
|
-
viewBox="0 0 24 24"
|
|
733
|
-
fill="none"
|
|
734
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
735
|
-
>
|
|
736
|
-
<path
|
|
737
|
-
d="M4 6H20M7 12H17M9 18H15"
|
|
738
|
-
stroke="currentColor"
|
|
739
|
-
strokeWidth="2"
|
|
740
|
-
strokeLinecap="round"
|
|
741
|
-
strokeLinejoin="round"
|
|
742
|
-
/>
|
|
743
|
-
</svg>
|
|
744
|
-
Filters
|
|
745
|
-
{/* Show active filters count when collapsed */}
|
|
746
|
-
{filtersCollapsed && (
|
|
747
|
-
<div
|
|
748
|
-
className={
|
|
749
|
-
styles.activeFiltersIndicator
|
|
750
|
-
}
|
|
751
|
-
>
|
|
752
|
-
{Object.values(
|
|
753
|
-
settingsFilterValues
|
|
754
|
-
).some((value) => value !== '') && (
|
|
755
|
-
<>
|
|
756
|
-
<span>Active:</span>
|
|
757
|
-
<span
|
|
758
|
-
className={
|
|
759
|
-
styles.filterBadge
|
|
760
|
-
}
|
|
761
|
-
>
|
|
762
|
-
{
|
|
763
|
-
Object.values(
|
|
764
|
-
settingsFilterValues
|
|
765
|
-
).filter(
|
|
766
|
-
(value) =>
|
|
767
|
-
value !== ''
|
|
768
|
-
).length
|
|
769
|
-
}
|
|
770
|
-
</span>
|
|
771
|
-
</>
|
|
772
|
-
)}
|
|
773
|
-
</div>
|
|
774
|
-
)}
|
|
775
|
-
</div>
|
|
776
|
-
|
|
777
|
-
{/* Toggle icon */}
|
|
778
|
-
<svg
|
|
779
|
-
className={`${styles.toggleIcon} ${
|
|
780
|
-
filtersCollapsed ? styles.collapsed : ''
|
|
781
|
-
}`}
|
|
782
|
-
width="16"
|
|
783
|
-
height="16"
|
|
784
|
-
viewBox="0 0 24 24"
|
|
785
|
-
fill="none"
|
|
786
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
787
|
-
>
|
|
788
|
-
<path
|
|
789
|
-
d="M19 9l-7 7-7-7"
|
|
790
|
-
stroke="currentColor"
|
|
791
|
-
strokeWidth="2"
|
|
792
|
-
strokeLinecap="round"
|
|
793
|
-
strokeLinejoin="round"
|
|
794
|
-
/>
|
|
795
|
-
</svg>
|
|
796
|
-
</div>
|
|
797
|
-
|
|
798
|
-
{/* Clear filters button */}
|
|
799
|
-
<button
|
|
800
|
-
className={styles.clearFiltersButton}
|
|
801
|
-
onClick={(e) => {
|
|
802
|
-
e.stopPropagation(); // Prevent toggling when clicking the button
|
|
803
|
-
// Initialize with default values
|
|
804
|
-
const initialValues = {};
|
|
805
|
-
settings.filters.forEach((filter) => {
|
|
806
|
-
if (
|
|
807
|
-
filter.type === 'dropdown' &&
|
|
808
|
-
filter.id === 'year' &&
|
|
809
|
-
filter.value === 'now'
|
|
810
|
-
) {
|
|
811
|
-
initialValues[filter.id] = new Date()
|
|
812
|
-
.getFullYear()
|
|
813
|
-
.toString();
|
|
814
|
-
} else {
|
|
815
|
-
initialValues[filter.id] =
|
|
816
|
-
filter.value || '';
|
|
817
|
-
}
|
|
818
|
-
});
|
|
819
|
-
setSettingsFilterValues(initialValues);
|
|
820
|
-
|
|
821
|
-
// Reset fetch params
|
|
822
|
-
prevFetchParamsRef.current = null;
|
|
823
|
-
}}
|
|
824
|
-
>
|
|
825
|
-
<svg
|
|
826
|
-
viewBox="0 0 24 24"
|
|
827
|
-
fill="none"
|
|
828
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
829
|
-
>
|
|
830
|
-
<path
|
|
831
|
-
d="M19 7l-7 7-7-7"
|
|
832
|
-
stroke="currentColor"
|
|
833
|
-
strokeWidth="2"
|
|
834
|
-
strokeLinecap="round"
|
|
835
|
-
strokeLinejoin="round"
|
|
836
|
-
transform="rotate(90, 12, 12)"
|
|
837
|
-
/>
|
|
838
|
-
</svg>
|
|
839
|
-
Clear
|
|
840
|
-
</button>
|
|
841
|
-
</div>
|
|
842
|
-
|
|
843
|
-
<div
|
|
844
|
-
className={`${styles.settingsFiltersContent} ${
|
|
845
|
-
filtersCollapsed ? styles.collapsed : ''
|
|
846
|
-
}`}
|
|
847
|
-
>
|
|
848
|
-
{settings.filters.map((filter, index) => (
|
|
849
|
-
<div
|
|
850
|
-
key={`settings-filter-${index}`}
|
|
851
|
-
className={styles.settingsFilterItem}
|
|
852
|
-
>
|
|
853
|
-
<label className={styles.settingsFilterLabel}>
|
|
854
|
-
{filter.label}
|
|
855
|
-
</label>
|
|
856
|
-
{filter.type === 'dropdown' &&
|
|
857
|
-
filter.id === 'year' ? (
|
|
858
|
-
<select
|
|
859
|
-
className={
|
|
860
|
-
styles.settingsFilterDropdown
|
|
861
|
-
}
|
|
862
|
-
value={
|
|
863
|
-
settingsFilterValues[filter.id] ||
|
|
864
|
-
''
|
|
865
|
-
}
|
|
866
|
-
onChange={(e) =>
|
|
867
|
-
handleSettingsFilterChange(
|
|
868
|
-
filter.id,
|
|
869
|
-
e.target.value
|
|
870
|
-
)
|
|
871
|
-
}
|
|
872
|
-
>
|
|
873
|
-
{generateYearOptions(
|
|
874
|
-
filter.startYear || 2000
|
|
875
|
-
).map((option) => (
|
|
876
|
-
<option
|
|
877
|
-
key={option.value}
|
|
878
|
-
value={option.value}
|
|
879
|
-
>
|
|
880
|
-
{option.label}
|
|
881
|
-
</option>
|
|
882
|
-
))}
|
|
883
|
-
</select>
|
|
884
|
-
) : filter.type === 'dropdown' ? (
|
|
885
|
-
<select
|
|
886
|
-
className={
|
|
887
|
-
styles.settingsFilterDropdown
|
|
888
|
-
}
|
|
889
|
-
value={
|
|
890
|
-
settingsFilterValues[filter.id] ||
|
|
891
|
-
''
|
|
892
|
-
}
|
|
893
|
-
onChange={(e) =>
|
|
894
|
-
handleSettingsFilterChange(
|
|
895
|
-
filter.id,
|
|
896
|
-
e.target.value
|
|
897
|
-
)
|
|
898
|
-
}
|
|
899
|
-
>
|
|
900
|
-
{filter.options &&
|
|
901
|
-
filter.options.map((option) => (
|
|
902
|
-
<option
|
|
903
|
-
key={option.value}
|
|
904
|
-
value={option.value}
|
|
905
|
-
>
|
|
906
|
-
{option.label}
|
|
907
|
-
</option>
|
|
908
|
-
))}
|
|
909
|
-
</select>
|
|
910
|
-
) : filter.type === 'text' ? (
|
|
911
|
-
<input
|
|
912
|
-
type="text"
|
|
913
|
-
className={styles.settingsFilterInput}
|
|
914
|
-
value={
|
|
915
|
-
settingsFilterValues[filter.id] ||
|
|
916
|
-
''
|
|
917
|
-
}
|
|
918
|
-
onChange={(e) =>
|
|
919
|
-
handleSettingsFilterChange(
|
|
920
|
-
filter.id,
|
|
921
|
-
e.target.value
|
|
922
|
-
)
|
|
923
|
-
}
|
|
924
|
-
placeholder={filter.placeholder || ''}
|
|
925
|
-
/>
|
|
926
|
-
) : null}
|
|
927
|
-
</div>
|
|
928
|
-
))}
|
|
929
|
-
</div>
|
|
930
|
-
</div>
|
|
931
|
-
)}
|
|
932
|
-
|
|
933
|
-
{loading ? (
|
|
934
|
-
<div className={styles.loadingContainer}>
|
|
935
|
-
<div className={styles.loadingSpinner}></div>
|
|
936
|
-
<p>Loading data...</p>
|
|
937
|
-
</div>
|
|
938
|
-
) : (
|
|
939
|
-
<table className={styles.gridTable}>
|
|
940
|
-
<thead>
|
|
941
|
-
<tr>
|
|
942
|
-
{/* Use gridHeaders if available, otherwise fall back to headers from columns */}
|
|
943
|
-
{gridHeaders.length > 0
|
|
944
|
-
? gridHeaders.map((header, index) => (
|
|
945
|
-
<th
|
|
946
|
-
key={`header-${index}`}
|
|
947
|
-
className={`${styles.gridHeader} ${styles.sortableHeader}`}
|
|
948
|
-
onClick={() =>
|
|
949
|
-
requestSort(header.key)
|
|
950
|
-
}
|
|
951
|
-
style={{ cursor: 'pointer' }}
|
|
952
|
-
>
|
|
953
|
-
<div className={styles.headerContent}>
|
|
954
|
-
{header.label}
|
|
955
|
-
<span
|
|
956
|
-
className={
|
|
957
|
-
styles.sortIndicator
|
|
958
|
-
}
|
|
959
|
-
style={getSortIndicatorStyle(
|
|
960
|
-
header.key
|
|
961
|
-
)}
|
|
962
|
-
>
|
|
963
|
-
{sortConfig.key ===
|
|
964
|
-
header.key &&
|
|
965
|
-
sortConfig.direction === 'asc'
|
|
966
|
-
? ' ▲'
|
|
967
|
-
: ' ▼'}
|
|
968
|
-
</span>
|
|
969
|
-
</div>
|
|
970
|
-
</th>
|
|
971
|
-
))
|
|
972
|
-
: headers.map((header, index) => (
|
|
973
|
-
<th
|
|
974
|
-
key={`header-${index}`}
|
|
975
|
-
className={styles.gridHeader}
|
|
976
|
-
>
|
|
977
|
-
{header}
|
|
978
|
-
</th>
|
|
979
|
-
))}
|
|
980
|
-
</tr>
|
|
981
|
-
{/* Filter row */}
|
|
982
|
-
{gridHeaders.some((header) => header.filter) && (
|
|
983
|
-
<tr className={styles.filterRow}>
|
|
984
|
-
{gridHeaders.map((header, index) => (
|
|
985
|
-
<th
|
|
986
|
-
key={`filter-${index}`}
|
|
987
|
-
className={styles.filterCell}
|
|
988
|
-
>
|
|
989
|
-
{header.filter ? (
|
|
990
|
-
<input
|
|
991
|
-
type={
|
|
992
|
-
header.filter.type ===
|
|
993
|
-
'date'
|
|
994
|
-
? 'date'
|
|
995
|
-
: 'text'
|
|
996
|
-
}
|
|
997
|
-
className={styles.filterInput}
|
|
998
|
-
placeholder={`Filter ${header.label}...`}
|
|
999
|
-
value={
|
|
1000
|
-
filterValues[header.key] ||
|
|
1001
|
-
''
|
|
1002
|
-
}
|
|
1003
|
-
onChange={(e) =>
|
|
1004
|
-
handleFilterChange(
|
|
1005
|
-
header.key,
|
|
1006
|
-
e.target.value
|
|
1007
|
-
)
|
|
1008
|
-
}
|
|
1009
|
-
/>
|
|
1010
|
-
) : null}
|
|
1011
|
-
</th>
|
|
1012
|
-
))}
|
|
1013
|
-
</tr>
|
|
1014
|
-
)}
|
|
1015
|
-
</thead>
|
|
1016
|
-
<tbody>
|
|
1017
|
-
{sortedData && sortedData.length > 0 ? (
|
|
1018
|
-
sortedData.map((row, rowIndex) => (
|
|
1019
|
-
<tr
|
|
1020
|
-
key={`row-${rowIndex}`}
|
|
1021
|
-
className={styles.gridRow}
|
|
1022
|
-
style={getCustomStyles().hoverColor}
|
|
1023
|
-
>
|
|
1024
|
-
{/* Use gridHeaders if available, otherwise fall back to columns */}
|
|
1025
|
-
{gridHeaders.length > 0
|
|
1026
|
-
? gridHeaders.map(
|
|
1027
|
-
(header, colIndex) => (
|
|
1028
|
-
<td
|
|
1029
|
-
key={`cell-${rowIndex}-${colIndex}`}
|
|
1030
|
-
className={`${
|
|
1031
|
-
styles.gridCell
|
|
1032
|
-
} ${
|
|
1033
|
-
header.onClick &&
|
|
1034
|
-
!row[header.key]
|
|
1035
|
-
? styles.clickableCell
|
|
1036
|
-
: ''
|
|
1037
|
-
}`}
|
|
1038
|
-
onClick={
|
|
1039
|
-
header.onClick &&
|
|
1040
|
-
!row[header.key]
|
|
1041
|
-
? async () => {
|
|
1042
|
-
try {
|
|
1043
|
-
// Replace {id} in the URL with the row's ID
|
|
1044
|
-
const url =
|
|
1045
|
-
header.onClick.url.replace(
|
|
1046
|
-
'{id}',
|
|
1047
|
-
row.id
|
|
1048
|
-
);
|
|
1049
|
-
|
|
1050
|
-
// Show loading toast
|
|
1051
|
-
toast.info(
|
|
1052
|
-
'Updating...',
|
|
1053
|
-
{
|
|
1054
|
-
autoClose: 1000,
|
|
1055
|
-
}
|
|
1056
|
-
);
|
|
1057
|
-
|
|
1058
|
-
// Extract field and value from params
|
|
1059
|
-
const {
|
|
1060
|
-
field,
|
|
1061
|
-
value,
|
|
1062
|
-
...otherParams
|
|
1063
|
-
} =
|
|
1064
|
-
header
|
|
1065
|
-
.onClick
|
|
1066
|
-
.params;
|
|
1067
|
-
|
|
1068
|
-
// Create a new params object with the field as the key
|
|
1069
|
-
const params =
|
|
1070
|
-
{
|
|
1071
|
-
...otherParams,
|
|
1072
|
-
_method:
|
|
1073
|
-
header
|
|
1074
|
-
.onClick
|
|
1075
|
-
.method, // Add _method for PUT/DELETE requests
|
|
1076
|
-
};
|
|
1077
|
-
|
|
1078
|
-
// Set the field as the key with the value
|
|
1079
|
-
// If value is "now", replace it with current date in ISO format
|
|
1080
|
-
params[
|
|
1081
|
-
field
|
|
1082
|
-
] =
|
|
1083
|
-
value ===
|
|
1084
|
-
'now'
|
|
1085
|
-
? new Date().toISOString()
|
|
1086
|
-
: value;
|
|
1087
|
-
|
|
1088
|
-
// Make the API call
|
|
1089
|
-
const response =
|
|
1090
|
-
await CustomFetch(
|
|
1091
|
-
url,
|
|
1092
|
-
header
|
|
1093
|
-
.onClick
|
|
1094
|
-
.method,
|
|
1095
|
-
params
|
|
1096
|
-
);
|
|
1097
|
-
|
|
1098
|
-
if (
|
|
1099
|
-
response
|
|
1100
|
-
.data
|
|
1101
|
-
.error
|
|
1102
|
-
) {
|
|
1103
|
-
toast.error(
|
|
1104
|
-
response
|
|
1105
|
-
.data
|
|
1106
|
-
.error
|
|
1107
|
-
);
|
|
1108
|
-
} else {
|
|
1109
|
-
toast.success(
|
|
1110
|
-
'Updated successfully',
|
|
1111
|
-
{
|
|
1112
|
-
autoClose: 1000,
|
|
1113
|
-
}
|
|
1114
|
-
);
|
|
1115
|
-
// Refresh the data
|
|
1116
|
-
prevFetchParamsRef.current =
|
|
1117
|
-
null;
|
|
1118
|
-
// Force a re-fetch
|
|
1119
|
-
const fetchUrl =
|
|
1120
|
-
settings
|
|
1121
|
-
.fetch
|
|
1122
|
-
.url;
|
|
1123
|
-
const fetchMethod =
|
|
1124
|
-
settings
|
|
1125
|
-
.fetch
|
|
1126
|
-
.method ||
|
|
1127
|
-
'POST';
|
|
1128
|
-
const fetchParams =
|
|
1129
|
-
{
|
|
1130
|
-
...(settings
|
|
1131
|
-
.fetch
|
|
1132
|
-
.params ||
|
|
1133
|
-
{}),
|
|
1134
|
-
...settingsFilterValues,
|
|
1135
|
-
};
|
|
1136
|
-
|
|
1137
|
-
setLoading(
|
|
1138
|
-
true
|
|
1139
|
-
);
|
|
1140
|
-
try {
|
|
1141
|
-
const fetchResponse =
|
|
1142
|
-
await CustomFetch(
|
|
1143
|
-
fetchUrl,
|
|
1144
|
-
fetchMethod,
|
|
1145
|
-
fetchParams
|
|
1146
|
-
);
|
|
1147
|
-
if (
|
|
1148
|
-
fetchResponse
|
|
1149
|
-
.data
|
|
1150
|
-
.data
|
|
1151
|
-
) {
|
|
1152
|
-
if (
|
|
1153
|
-
fetchResponse
|
|
1154
|
-
.data
|
|
1155
|
-
.data
|
|
1156
|
-
.header &&
|
|
1157
|
-
fetchResponse
|
|
1158
|
-
.data
|
|
1159
|
-
.data
|
|
1160
|
-
.rows
|
|
1161
|
-
) {
|
|
1162
|
-
setGridData(
|
|
1163
|
-
fetchResponse
|
|
1164
|
-
.data
|
|
1165
|
-
.data
|
|
1166
|
-
.rows ||
|
|
1167
|
-
[]
|
|
1168
|
-
);
|
|
1169
|
-
setSortedData(
|
|
1170
|
-
fetchResponse
|
|
1171
|
-
.data
|
|
1172
|
-
.data
|
|
1173
|
-
.rows ||
|
|
1174
|
-
[]
|
|
1175
|
-
);
|
|
1176
|
-
} else {
|
|
1177
|
-
setGridData(
|
|
1178
|
-
fetchResponse
|
|
1179
|
-
.data
|
|
1180
|
-
.data
|
|
1181
|
-
.data ||
|
|
1182
|
-
[]
|
|
1183
|
-
);
|
|
1184
|
-
setSortedData(
|
|
1185
|
-
fetchResponse
|
|
1186
|
-
.data
|
|
1187
|
-
.data
|
|
1188
|
-
.data ||
|
|
1189
|
-
[]
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
console.error(
|
|
1195
|
-
'Error refreshing data:',
|
|
1196
|
-
error
|
|
1197
|
-
);
|
|
1198
|
-
} finally {
|
|
1199
|
-
setLoading(
|
|
1200
|
-
false
|
|
1201
|
-
);
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
} catch (error) {
|
|
1205
|
-
console.error(
|
|
1206
|
-
'Error updating cell:',
|
|
1207
|
-
error
|
|
1208
|
-
);
|
|
1209
|
-
toast.error(
|
|
1210
|
-
'An error occurred while updating'
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
: undefined
|
|
1215
|
-
}
|
|
1216
|
-
>
|
|
1217
|
-
{row[header.key] ? (
|
|
1218
|
-
header.onClick ? (
|
|
1219
|
-
<span
|
|
1220
|
-
className={
|
|
1221
|
-
styles.cellWithValue
|
|
1222
|
-
}
|
|
1223
|
-
>
|
|
1224
|
-
{formatCellContent(
|
|
1225
|
-
row[
|
|
1226
|
-
header
|
|
1227
|
-
.key
|
|
1228
|
-
],
|
|
1229
|
-
{
|
|
1230
|
-
type:
|
|
1231
|
-
header.type ||
|
|
1232
|
-
(typeof row[
|
|
1233
|
-
header
|
|
1234
|
-
.key
|
|
1235
|
-
] ===
|
|
1236
|
-
'number'
|
|
1237
|
-
? 'number'
|
|
1238
|
-
: ''),
|
|
1239
|
-
}
|
|
1240
|
-
)}
|
|
1241
|
-
</span>
|
|
1242
|
-
) : (
|
|
1243
|
-
<span
|
|
1244
|
-
className={
|
|
1245
|
-
styles.cellContent
|
|
1246
|
-
}
|
|
1247
|
-
data-type={
|
|
1248
|
-
header.type ||
|
|
1249
|
-
(typeof row[
|
|
1250
|
-
header
|
|
1251
|
-
.key
|
|
1252
|
-
] ===
|
|
1253
|
-
'number'
|
|
1254
|
-
? 'number'
|
|
1255
|
-
: '')
|
|
1256
|
-
}
|
|
1257
|
-
>
|
|
1258
|
-
{formatCellContent(
|
|
1259
|
-
row[
|
|
1260
|
-
header
|
|
1261
|
-
.key
|
|
1262
|
-
],
|
|
1263
|
-
{
|
|
1264
|
-
type:
|
|
1265
|
-
header.type ||
|
|
1266
|
-
(typeof row[
|
|
1267
|
-
header
|
|
1268
|
-
.key
|
|
1269
|
-
] ===
|
|
1270
|
-
'number'
|
|
1271
|
-
? 'number'
|
|
1272
|
-
: ''),
|
|
1273
|
-
}
|
|
1274
|
-
)}
|
|
1275
|
-
</span>
|
|
1276
|
-
)
|
|
1277
|
-
) : header.onClick ? (
|
|
1278
|
-
<span
|
|
1279
|
-
className={
|
|
1280
|
-
styles.placeholderText
|
|
1281
|
-
}
|
|
1282
|
-
>
|
|
1283
|
-
{header.onClick
|
|
1284
|
-
.placeholder ||
|
|
1285
|
-
'Click to set'}
|
|
1286
|
-
</span>
|
|
1287
|
-
) : (
|
|
1288
|
-
''
|
|
1289
|
-
)}
|
|
1290
|
-
</td>
|
|
1291
|
-
)
|
|
1292
|
-
)
|
|
1293
|
-
: columns.map((col, colIndex) => (
|
|
1294
|
-
<td
|
|
1295
|
-
key={`cell-${rowIndex}-${colIndex}`}
|
|
1296
|
-
className={styles.gridCell}
|
|
1297
|
-
>
|
|
1298
|
-
<span
|
|
1299
|
-
className={
|
|
1300
|
-
styles.cellContent
|
|
1301
|
-
}
|
|
1302
|
-
data-type={
|
|
1303
|
-
col.type ||
|
|
1304
|
-
(typeof row[
|
|
1305
|
-
col.name
|
|
1306
|
-
] === 'number'
|
|
1307
|
-
? 'number'
|
|
1308
|
-
: '')
|
|
1309
|
-
}
|
|
1310
|
-
>
|
|
1311
|
-
{formatCellContent(
|
|
1312
|
-
row[col.name],
|
|
1313
|
-
col
|
|
1314
|
-
)}
|
|
1315
|
-
</span>
|
|
1316
|
-
</td>
|
|
1317
|
-
))}
|
|
1318
|
-
</tr>
|
|
1319
|
-
))
|
|
1320
|
-
) : (
|
|
1321
|
-
<tr>
|
|
1322
|
-
<td
|
|
1323
|
-
colSpan={
|
|
1324
|
-
gridHeaders.length || headers.length
|
|
1325
|
-
}
|
|
1326
|
-
className={styles.noData}
|
|
1327
|
-
>
|
|
1328
|
-
No data available
|
|
1329
|
-
</td>
|
|
1330
|
-
</tr>
|
|
1331
|
-
)}
|
|
1332
|
-
</tbody>
|
|
1333
|
-
</table>
|
|
1334
|
-
)}
|
|
1335
|
-
</div>
|
|
1336
|
-
);
|
|
1337
|
-
};
|
|
1338
|
-
|
|
1339
|
-
const ReportForm = ({ config, userProfile }) => {
|
|
1340
|
-
const [formData, setFormData] = useState({});
|
|
1341
|
-
|
|
1342
|
-
const handleChange = (e) => {
|
|
1343
|
-
const {
|
|
1344
|
-
value,
|
|
1345
|
-
dataset: { name },
|
|
1346
|
-
} = e.target;
|
|
1347
|
-
|
|
1348
|
-
setFormData((prevData) => ({ ...prevData, [name]: value }));
|
|
1349
|
-
};
|
|
1350
|
-
|
|
1351
|
-
const handleChangeDate = (date, id) => {
|
|
1352
|
-
setFormData((prevData) => ({ ...prevData, [id]: date }));
|
|
1353
|
-
};
|
|
1354
|
-
|
|
1355
|
-
const handleChangeSelect = (inputValue, _, id) => {
|
|
1356
|
-
setFormData((prevData) => ({ ...prevData, [id]: inputValue }));
|
|
1357
|
-
};
|
|
1358
|
-
|
|
1359
|
-
const handleExport = async (e) => {
|
|
1360
|
-
try {
|
|
1361
|
-
if (e) {
|
|
1362
|
-
e.preventDefault();
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
const missingFields = config.form.filters
|
|
1366
|
-
.filter(
|
|
1367
|
-
(item) => item.required === true && formData[item.id] === ''
|
|
1368
|
-
)
|
|
1369
|
-
.map((item) => item.label);
|
|
1370
|
-
|
|
1371
|
-
if (missingFields.length > 0) {
|
|
1372
|
-
const fieldNames = missingFields.join(', ');
|
|
1373
|
-
const errorMessage = `Please fill in a value for the following field(s): ${fieldNames}.`;
|
|
1374
|
-
toast.error(errorMessage);
|
|
1375
|
-
} else {
|
|
1376
|
-
toast.info('Preparing your export...');
|
|
1377
|
-
|
|
1378
|
-
const res = await Download(
|
|
1379
|
-
config.form.url,
|
|
1380
|
-
config.form.method,
|
|
1381
|
-
formData
|
|
1382
|
-
);
|
|
1383
|
-
|
|
1384
|
-
if (res.data.error) {
|
|
1385
|
-
toast.error(res.data.error);
|
|
1386
|
-
} else {
|
|
1387
|
-
// Assuming the response contains a Blob or similar data
|
|
1388
|
-
if (res.data && res.data instanceof Blob) {
|
|
1389
|
-
const fileName = `${config.form.filename}`;
|
|
1390
|
-
saveAs(res.data, fileName);
|
|
1391
|
-
toast.success('Export completed successfully');
|
|
1392
|
-
} else {
|
|
1393
|
-
// Handle the case where the response is not a Blob
|
|
1394
|
-
toast.error('Invalid file format received.');
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
} catch (err) {
|
|
1399
|
-
console.error(err);
|
|
1400
|
-
toast.error('An error occurred during export');
|
|
1401
|
-
}
|
|
1402
|
-
};
|
|
1403
|
-
|
|
1404
|
-
useEffect(() => {
|
|
1405
|
-
const tmpForm = config.form?.filters.reduce((acc, item) => {
|
|
1406
|
-
acc[item.id] = '';
|
|
1407
|
-
return acc;
|
|
1408
|
-
}, {});
|
|
1409
|
-
setFormData(tmpForm);
|
|
1410
|
-
}, [config.form?.filters]);
|
|
1411
|
-
|
|
1412
|
-
// Determine if the form has a title
|
|
1413
|
-
const formTitle = config.form?.title || 'Export Report';
|
|
1414
|
-
const formDescription =
|
|
1415
|
-
config.form?.description || 'Select your export criteria below.';
|
|
1416
|
-
|
|
1417
|
-
return (
|
|
1418
|
-
<div className={reportStyles.reportFormContainer}>
|
|
1419
|
-
<form className={reportStyles.reportForm}>
|
|
1420
|
-
<h2 className={reportStyles.formTitle}>{formTitle}</h2>
|
|
1421
|
-
<p className={reportStyles.formDescription}>
|
|
1422
|
-
{formDescription}
|
|
1423
|
-
</p>
|
|
1424
|
-
|
|
1425
|
-
{config.form?.filters.map((item, key) => (
|
|
1426
|
-
<div
|
|
1427
|
-
key={`filter-container-${key}`}
|
|
1428
|
-
className={`${reportStyles.formField} ${
|
|
1429
|
-
item.fullWidth ? reportStyles.fullWidthField : ''
|
|
1430
|
-
}`}
|
|
1431
|
-
>
|
|
1432
|
-
<Field
|
|
1433
|
-
api={userProfile.settings.api}
|
|
1434
|
-
autocompleteCallback={() => {}}
|
|
1435
|
-
childDropdownCallback={() => {}}
|
|
1436
|
-
fetchData={() => {}}
|
|
1437
|
-
formData={formData}
|
|
1438
|
-
inputClass={''}
|
|
1439
|
-
inputValue={formData[item.id]}
|
|
1440
|
-
key={`filter-fields-${key}`}
|
|
1441
|
-
mapbox={{}}
|
|
1442
|
-
onChange={handleChange}
|
|
1443
|
-
onChangeCheckbox={() => {}}
|
|
1444
|
-
onChangeDate={handleChangeDate}
|
|
1445
|
-
onChangeNumberFormat={() => {}}
|
|
1446
|
-
onChangeRicheditor={() => {}}
|
|
1447
|
-
onChangeSelect={handleChangeSelect}
|
|
1448
|
-
onChangeToggle={() => {}}
|
|
1449
|
-
settings={item}
|
|
1450
|
-
setFormData={setFormData}
|
|
1451
|
-
style={userProfile.settings.style}
|
|
1452
|
-
uploadProgress={0}
|
|
1453
|
-
userProfile={userProfile}
|
|
1454
|
-
/>
|
|
1455
|
-
</div>
|
|
1456
|
-
))}
|
|
1457
|
-
|
|
1458
|
-
<div className={reportStyles.formActions}>
|
|
1459
|
-
<button
|
|
1460
|
-
className={reportStyles.exportButton}
|
|
1461
|
-
onClick={handleExport}
|
|
1462
|
-
type="button"
|
|
1463
|
-
>
|
|
1464
|
-
<CloudDownload
|
|
1465
|
-
strokeWidth={2}
|
|
1466
|
-
size={18}
|
|
1467
|
-
style={{ marginRight: '8px' }}
|
|
1468
|
-
/>{' '}
|
|
1469
|
-
Export
|
|
1470
|
-
</button>
|
|
1471
|
-
</div>
|
|
1472
|
-
</form>
|
|
1473
|
-
</div>
|
|
1474
|
-
);
|
|
1475
|
-
};
|
|
1476
226
|
|
|
1477
227
|
function GenericIndex({
|
|
1478
228
|
layout = 'full',
|
|
@@ -1856,15 +606,15 @@ function GenericIndex({
|
|
|
1856
606
|
const renderContentBasedOnType = useCallback(() => {
|
|
1857
607
|
switch (config.type) {
|
|
1858
608
|
case 'grid':
|
|
1859
|
-
return <
|
|
609
|
+
return <GenericGrid config={config} userProfile={userProfile} />;
|
|
1860
610
|
case 'table':
|
|
1861
611
|
return renderTable();
|
|
1862
612
|
case 'report':
|
|
1863
|
-
return <
|
|
613
|
+
return <GenericReportForm config={config} userProfile={userProfile} />;
|
|
1864
614
|
default:
|
|
1865
615
|
return renderTable(); // Fallback to table if no type is specified or recognized
|
|
1866
616
|
}
|
|
1867
|
-
}, [config, renderTable]);
|
|
617
|
+
}, [config, renderTable, userProfile]);
|
|
1868
618
|
|
|
1869
619
|
// Custom handler for filter changes
|
|
1870
620
|
const handleFilterChange = useCallback(
|