@visns-studio/visns-components 5.10.10 → 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/cms/DataGrid.jsx +83 -3
- package/src/components/crm/columns/ColumnRenderers.jsx +97 -2
- package/src/components/crm/generic/ContactSelectorModal.jsx +380 -0
- package/src/components/crm/generic/DatePickerDialog.jsx +302 -0
- package/src/components/crm/generic/GenericDetail.jsx +67 -2
- 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/GenericDetail.module.scss +77 -0
- package/src/components/crm/generic/styles/GenericIndex.module.scss +176 -0
- package/src/index.js +4 -0
|
@@ -0,0 +1,1534 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { toast } from 'react-toastify';
|
|
9
|
+
import parse from 'html-react-parser';
|
|
10
|
+
import moment from 'moment';
|
|
11
|
+
import numeral from 'numeral';
|
|
12
|
+
|
|
13
|
+
import CustomFetch from '../Fetch';
|
|
14
|
+
import MultiSelect from '../MultiSelect';
|
|
15
|
+
import { showDatePickerDialog } from './DatePickerDialog';
|
|
16
|
+
import { showContactSelectorModal } from './ContactSelectorModal';
|
|
17
|
+
import styles from './styles/GenericIndex.module.scss';
|
|
18
|
+
|
|
19
|
+
// ContactTooltip component for enhanced contact display
|
|
20
|
+
const ContactTooltip = ({ contacts, children }) => {
|
|
21
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
22
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
23
|
+
const tooltipRef = useRef(null);
|
|
24
|
+
const triggerRef = useRef(null);
|
|
25
|
+
|
|
26
|
+
const handleMouseEnter = (e) => {
|
|
27
|
+
if (!contacts || contacts.length === 0) return;
|
|
28
|
+
|
|
29
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
30
|
+
setPosition({
|
|
31
|
+
x: rect.left + rect.width / 2,
|
|
32
|
+
y: rect.top - 10
|
|
33
|
+
});
|
|
34
|
+
setIsVisible(true);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleMouseLeave = () => {
|
|
38
|
+
setIsVisible(false);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const formatContact = (contact) => {
|
|
42
|
+
const name = contact.name || 'Unknown Contact';
|
|
43
|
+
const email = contact.email || '';
|
|
44
|
+
const type = contact.type === 'contact' ? 'Existing Contact' : 'Manual Entry';
|
|
45
|
+
|
|
46
|
+
return { name, email, type };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<div
|
|
52
|
+
ref={triggerRef}
|
|
53
|
+
onMouseEnter={handleMouseEnter}
|
|
54
|
+
onMouseLeave={handleMouseLeave}
|
|
55
|
+
style={{ position: 'relative' }}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{isVisible && contacts && contacts.length > 0 && (
|
|
61
|
+
<div
|
|
62
|
+
ref={tooltipRef}
|
|
63
|
+
className={styles.contactTooltip}
|
|
64
|
+
style={{
|
|
65
|
+
position: 'fixed',
|
|
66
|
+
left: `${position.x}px`,
|
|
67
|
+
top: `${position.y}px`,
|
|
68
|
+
transform: 'translateX(-50%) translateY(-100%)',
|
|
69
|
+
zIndex: 9999,
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<div className={styles.contactTooltipHeader}>
|
|
73
|
+
<strong>{contacts.length} Contact{contacts.length !== 1 ? 's' : ''}</strong>
|
|
74
|
+
</div>
|
|
75
|
+
<div className={styles.contactTooltipBody}>
|
|
76
|
+
{contacts.map((contact, index) => {
|
|
77
|
+
const { name, email, type } = formatContact(contact);
|
|
78
|
+
return (
|
|
79
|
+
<div key={index} className={styles.contactTooltipItem}>
|
|
80
|
+
<div className={styles.contactName}>{name}</div>
|
|
81
|
+
{email && <div className={styles.contactEmail}>{email}</div>}
|
|
82
|
+
<div className={styles.contactType}>{type}</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
<div className={styles.contactTooltipArrow}></div>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) => {
|
|
95
|
+
const [gridData, setGridData] = useState([]);
|
|
96
|
+
const [sortedData, setSortedData] = useState([]);
|
|
97
|
+
const [, setFilteredData] = useState([]);
|
|
98
|
+
const [filterValues, setFilterValues] = useState({});
|
|
99
|
+
const [settingsFilterValues, setSettingsFilterValues] = useState({});
|
|
100
|
+
const [filterOptions, setFilterOptions] = useState({});
|
|
101
|
+
const [loading, setLoading] = useState(false);
|
|
102
|
+
const [filtersCollapsed, setFiltersCollapsed] = useState(false);
|
|
103
|
+
const [sortConfig, setSortConfig] = useState({
|
|
104
|
+
key: null,
|
|
105
|
+
direction: 'asc',
|
|
106
|
+
});
|
|
107
|
+
const prevFetchParamsRef = useRef(null);
|
|
108
|
+
|
|
109
|
+
// Extract data and settings from config using useMemo to prevent unnecessary re-renders
|
|
110
|
+
const {
|
|
111
|
+
settings,
|
|
112
|
+
columns = [],
|
|
113
|
+
data: configData = [],
|
|
114
|
+
} = useMemo(
|
|
115
|
+
() => ({
|
|
116
|
+
settings: config.settings || {},
|
|
117
|
+
columns: config.columns || [],
|
|
118
|
+
data: config.data || data || [],
|
|
119
|
+
}),
|
|
120
|
+
[config.settings, config.columns, config.data, data]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Create table headers from columns
|
|
124
|
+
const headers = useMemo(
|
|
125
|
+
() => columns.map((col) => col.header || col.name || ''),
|
|
126
|
+
[columns]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Format cell content based on column type or value type
|
|
130
|
+
const formatCellContent = (value, column, row = null) => {
|
|
131
|
+
if (value === null || value === undefined) return '';
|
|
132
|
+
if (value === 0) return '0'; // Explicitly handle zero values
|
|
133
|
+
if (value === '') return '';
|
|
134
|
+
|
|
135
|
+
// Check if this is a date field with contacts (survey workflow)
|
|
136
|
+
if (column.type === 'date' && column.onClick?.type === 'date_with_contacts' && row) {
|
|
137
|
+
return formatDateWithContacts(value, column, row);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Try to infer the type if not provided
|
|
141
|
+
const valueType = column.type || typeof value;
|
|
142
|
+
|
|
143
|
+
switch (valueType) {
|
|
144
|
+
case 'date':
|
|
145
|
+
return moment(value).isValid()
|
|
146
|
+
? moment(value).format(column.format || 'DD/MM/YYYY')
|
|
147
|
+
: value;
|
|
148
|
+
case 'datetime':
|
|
149
|
+
return moment(value).isValid()
|
|
150
|
+
? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
|
|
151
|
+
: value;
|
|
152
|
+
case 'currency':
|
|
153
|
+
return numeral(value).format(column.format || '$0,0.00');
|
|
154
|
+
case 'number':
|
|
155
|
+
// Check if it's actually a number before formatting
|
|
156
|
+
return !isNaN(parseFloat(value)) && isFinite(value)
|
|
157
|
+
? numeral(value).format(column.format || '0,0')
|
|
158
|
+
: value;
|
|
159
|
+
case 'boolean':
|
|
160
|
+
if (
|
|
161
|
+
value === 1 ||
|
|
162
|
+
value === true ||
|
|
163
|
+
value === 'true' ||
|
|
164
|
+
value === 'yes' ||
|
|
165
|
+
value === 'Yes'
|
|
166
|
+
) {
|
|
167
|
+
return 'Yes';
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
value === 0 ||
|
|
171
|
+
value === false ||
|
|
172
|
+
value === 'false' ||
|
|
173
|
+
value === 'no' ||
|
|
174
|
+
value === 'No'
|
|
175
|
+
) {
|
|
176
|
+
return 'No';
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
case 'year':
|
|
180
|
+
// Special handling for year values
|
|
181
|
+
return value.toString();
|
|
182
|
+
case 'quarter':
|
|
183
|
+
// Special handling for quarter values (Q1, Q2, etc.)
|
|
184
|
+
if (value && value.toString().startsWith('Q')) {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
case 'html':
|
|
189
|
+
// Explicitly parse HTML content
|
|
190
|
+
return parse(String(value));
|
|
191
|
+
case 'richtext':
|
|
192
|
+
// Parse rich text content
|
|
193
|
+
return parse(String(value));
|
|
194
|
+
default:
|
|
195
|
+
// For text values, check if it contains HTML tags
|
|
196
|
+
if (
|
|
197
|
+
typeof value === 'string' &&
|
|
198
|
+
(value.includes('<br') ||
|
|
199
|
+
value.includes('<p>') ||
|
|
200
|
+
value.includes('<div') ||
|
|
201
|
+
value.includes('<span'))
|
|
202
|
+
) {
|
|
203
|
+
// Ensure <br /> tags are properly handled
|
|
204
|
+
const processedValue = String(value)
|
|
205
|
+
.replace(/<br \/>/g, '<br>')
|
|
206
|
+
.replace(/<br\/>/g, '<br>');
|
|
207
|
+
return parse(processedValue);
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Format date field with associated contacts
|
|
214
|
+
const formatDateWithContacts = (dateValue, column, row) => {
|
|
215
|
+
const contactsField = column.key + '_contacts';
|
|
216
|
+
const contacts = row[contactsField];
|
|
217
|
+
|
|
218
|
+
// Format the date
|
|
219
|
+
const formattedDate = moment(dateValue).isValid()
|
|
220
|
+
? moment(dateValue).format(column.format || 'DD/MM/YYYY')
|
|
221
|
+
: dateValue;
|
|
222
|
+
|
|
223
|
+
// If no contacts, just return the date
|
|
224
|
+
if (!contacts || !Array.isArray(contacts) || contacts.length === 0) {
|
|
225
|
+
return formattedDate;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Create JSX element to display date + contacts with enhanced tooltip
|
|
229
|
+
return (
|
|
230
|
+
<div className={styles.dateWithContacts}>
|
|
231
|
+
<div className={styles.dateValue}>{formattedDate}</div>
|
|
232
|
+
<ContactTooltip contacts={contacts}>
|
|
233
|
+
<div className={styles.contactsSummary}>
|
|
234
|
+
📧 {contacts.length} contact{contacts.length !== 1 ? 's' : ''}
|
|
235
|
+
</div>
|
|
236
|
+
</ContactTooltip>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
// State for headers from API response
|
|
243
|
+
const [gridHeaders, setGridHeaders] = useState([]);
|
|
244
|
+
|
|
245
|
+
// Function to sort data
|
|
246
|
+
const sortData = useCallback(
|
|
247
|
+
(data, key, direction) => {
|
|
248
|
+
if (!key) return [...data];
|
|
249
|
+
|
|
250
|
+
return [...data].sort((a, b) => {
|
|
251
|
+
// Find the header config for this key to determine type
|
|
252
|
+
const headerConfig = gridHeaders.find((h) => h.key === key);
|
|
253
|
+
const type = headerConfig?.type || 'text';
|
|
254
|
+
|
|
255
|
+
// Get values, handling null/undefined
|
|
256
|
+
let aValue =
|
|
257
|
+
a[key] === null || a[key] === undefined ? '' : a[key];
|
|
258
|
+
let bValue =
|
|
259
|
+
b[key] === null || b[key] === undefined ? '' : b[key];
|
|
260
|
+
|
|
261
|
+
// Sort based on type
|
|
262
|
+
switch (type) {
|
|
263
|
+
case 'number':
|
|
264
|
+
case 'year':
|
|
265
|
+
aValue = parseFloat(aValue) || 0;
|
|
266
|
+
bValue = parseFloat(bValue) || 0;
|
|
267
|
+
break;
|
|
268
|
+
case 'date':
|
|
269
|
+
// Convert to timestamp for comparison
|
|
270
|
+
aValue = aValue ? new Date(aValue).getTime() : 0;
|
|
271
|
+
bValue = bValue ? new Date(bValue).getTime() : 0;
|
|
272
|
+
break;
|
|
273
|
+
default:
|
|
274
|
+
// For text, convert to lowercase strings
|
|
275
|
+
aValue = String(aValue).toLowerCase();
|
|
276
|
+
bValue = String(bValue).toLowerCase();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Compare values
|
|
280
|
+
if (aValue < bValue) {
|
|
281
|
+
return direction === 'asc' ? -1 : 1;
|
|
282
|
+
}
|
|
283
|
+
if (aValue > bValue) {
|
|
284
|
+
return direction === 'asc' ? 1 : -1;
|
|
285
|
+
}
|
|
286
|
+
return 0;
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
[gridHeaders]
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Handle sort request
|
|
293
|
+
const requestSort = useCallback((key) => {
|
|
294
|
+
// If clicking the same column, toggle direction
|
|
295
|
+
// Otherwise, start with ascending sort for the new column
|
|
296
|
+
setSortConfig((prevConfig) => {
|
|
297
|
+
const newDirection =
|
|
298
|
+
prevConfig.key === key && prevConfig.direction === 'asc'
|
|
299
|
+
? 'desc'
|
|
300
|
+
: 'asc';
|
|
301
|
+
|
|
302
|
+
return { key, direction: newDirection };
|
|
303
|
+
});
|
|
304
|
+
}, []);
|
|
305
|
+
|
|
306
|
+
// Function to filter data based on filter values
|
|
307
|
+
const filterData = useCallback(
|
|
308
|
+
(data) => {
|
|
309
|
+
// If no filter values are set, return the original data
|
|
310
|
+
if (Object.keys(filterValues).length === 0) {
|
|
311
|
+
return data;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Filter the data based on the filter values
|
|
315
|
+
return data.filter((row) => {
|
|
316
|
+
// Check each filter
|
|
317
|
+
return Object.entries(filterValues).every(([key, value]) => {
|
|
318
|
+
// Skip empty filter values
|
|
319
|
+
if (!value) return true;
|
|
320
|
+
|
|
321
|
+
// Get the row value
|
|
322
|
+
const rowValue = row[key];
|
|
323
|
+
if (rowValue === null || rowValue === undefined)
|
|
324
|
+
return false;
|
|
325
|
+
|
|
326
|
+
// Find the header config for this key
|
|
327
|
+
const headerConfig = gridHeaders.find((h) => h.key === key);
|
|
328
|
+
if (!headerConfig || !headerConfig.filter) return true;
|
|
329
|
+
|
|
330
|
+
const { type, operator } = headerConfig.filter;
|
|
331
|
+
|
|
332
|
+
// Apply filter based on type and operator
|
|
333
|
+
switch (type) {
|
|
334
|
+
case 'string':
|
|
335
|
+
const rowStr = String(rowValue).toLowerCase();
|
|
336
|
+
const filterStr = String(value).toLowerCase();
|
|
337
|
+
|
|
338
|
+
switch (operator) {
|
|
339
|
+
case 'equals':
|
|
340
|
+
return rowStr === filterStr;
|
|
341
|
+
case 'contains':
|
|
342
|
+
return rowStr.includes(filterStr);
|
|
343
|
+
case 'startsWith':
|
|
344
|
+
return rowStr.startsWith(filterStr);
|
|
345
|
+
case 'endsWith':
|
|
346
|
+
return rowStr.endsWith(filterStr);
|
|
347
|
+
default:
|
|
348
|
+
return rowStr.includes(filterStr);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
case 'number':
|
|
352
|
+
const rowNum = parseFloat(rowValue);
|
|
353
|
+
const filterNum = parseFloat(value);
|
|
354
|
+
|
|
355
|
+
if (isNaN(rowNum) || isNaN(filterNum)) return false;
|
|
356
|
+
|
|
357
|
+
switch (operator) {
|
|
358
|
+
case 'equals':
|
|
359
|
+
return rowNum === filterNum;
|
|
360
|
+
case 'greaterThan':
|
|
361
|
+
return rowNum > filterNum;
|
|
362
|
+
case 'lessThan':
|
|
363
|
+
return rowNum < filterNum;
|
|
364
|
+
case 'greaterThanOrEqual':
|
|
365
|
+
return rowNum >= filterNum;
|
|
366
|
+
case 'lessThanOrEqual':
|
|
367
|
+
return rowNum <= filterNum;
|
|
368
|
+
default:
|
|
369
|
+
return rowNum === filterNum;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
case 'date':
|
|
373
|
+
const rowDate = new Date(rowValue);
|
|
374
|
+
const filterDate = new Date(value);
|
|
375
|
+
|
|
376
|
+
if (
|
|
377
|
+
isNaN(rowDate.getTime()) ||
|
|
378
|
+
isNaN(filterDate.getTime())
|
|
379
|
+
)
|
|
380
|
+
return false;
|
|
381
|
+
|
|
382
|
+
switch (operator) {
|
|
383
|
+
case 'equals':
|
|
384
|
+
return (
|
|
385
|
+
rowDate.toDateString() ===
|
|
386
|
+
filterDate.toDateString()
|
|
387
|
+
);
|
|
388
|
+
case 'after':
|
|
389
|
+
return rowDate > filterDate;
|
|
390
|
+
case 'before':
|
|
391
|
+
return rowDate < filterDate;
|
|
392
|
+
default:
|
|
393
|
+
return (
|
|
394
|
+
rowDate.toDateString() ===
|
|
395
|
+
filterDate.toDateString()
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
default:
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
},
|
|
405
|
+
[filterValues, gridHeaders]
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Handle filter change
|
|
409
|
+
const handleFilterChange = useCallback((key, value) => {
|
|
410
|
+
setFilterValues((prev) => ({
|
|
411
|
+
...prev,
|
|
412
|
+
[key]: value,
|
|
413
|
+
}));
|
|
414
|
+
}, []);
|
|
415
|
+
|
|
416
|
+
// Effect to update filteredData and sortedData when gridData, filterValues, or sortConfig changes
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
if (gridData.length > 0) {
|
|
419
|
+
// First apply filters
|
|
420
|
+
const filtered = filterData(gridData);
|
|
421
|
+
setFilteredData(filtered);
|
|
422
|
+
|
|
423
|
+
// Then sort the filtered data
|
|
424
|
+
const sorted = sortData(
|
|
425
|
+
filtered,
|
|
426
|
+
sortConfig.key,
|
|
427
|
+
sortConfig.direction
|
|
428
|
+
);
|
|
429
|
+
setSortedData(sorted);
|
|
430
|
+
} else {
|
|
431
|
+
setFilteredData([]);
|
|
432
|
+
setSortedData([]);
|
|
433
|
+
}
|
|
434
|
+
}, [gridData, filterValues, sortConfig, sortData, filterData]);
|
|
435
|
+
|
|
436
|
+
// Load data from API if gridSetting.fetch is provided
|
|
437
|
+
useEffect(() => {
|
|
438
|
+
const fetchData = async () => {
|
|
439
|
+
// Check if we should use static data instead of fetching
|
|
440
|
+
if (settings?.useStaticData && settings?.staticData) {
|
|
441
|
+
const staticData = settings.staticData;
|
|
442
|
+
|
|
443
|
+
if (staticData.header && staticData.rows) {
|
|
444
|
+
// Set the headers from the static data
|
|
445
|
+
const headerEntries = Object.entries(staticData.header);
|
|
446
|
+
setGridHeaders(
|
|
447
|
+
headerEntries.map(([key, config]) => ({
|
|
448
|
+
key,
|
|
449
|
+
label: config.label || key,
|
|
450
|
+
sort: config.sort || key,
|
|
451
|
+
type: config.type || 'text',
|
|
452
|
+
filter: config.filter || null,
|
|
453
|
+
onClick: config.onClick || null,
|
|
454
|
+
}))
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
// Initialize filter values
|
|
458
|
+
const initialFilterValues = {};
|
|
459
|
+
headerEntries.forEach(([key, config]) => {
|
|
460
|
+
if (config.filter) {
|
|
461
|
+
initialFilterValues[key] = '';
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
setFilterValues(initialFilterValues);
|
|
465
|
+
|
|
466
|
+
// Set the rows data
|
|
467
|
+
const newData = staticData.rows || [];
|
|
468
|
+
setGridData(newData);
|
|
469
|
+
setSortedData(newData);
|
|
470
|
+
|
|
471
|
+
// Notify parent component of data change
|
|
472
|
+
if (onDataChange) {
|
|
473
|
+
onDataChange(newData);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Only fetch if we have a URL
|
|
480
|
+
if (!settings?.fetch?.url) {
|
|
481
|
+
if (configData && configData.length > 0) {
|
|
482
|
+
// Use provided data if no fetch setting
|
|
483
|
+
setGridData(configData);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const url = settings.fetch.url;
|
|
489
|
+
const method = settings.fetch.method || 'POST';
|
|
490
|
+
|
|
491
|
+
// Combine base params with settings filter values
|
|
492
|
+
const params = {
|
|
493
|
+
...(settings.fetch.params || {}),
|
|
494
|
+
...settingsFilterValues,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// Create a string representation of the fetch parameters for comparison
|
|
498
|
+
const fetchParamsString = JSON.stringify({ url, method, params });
|
|
499
|
+
|
|
500
|
+
// Skip fetch if the parameters haven't changed
|
|
501
|
+
if (prevFetchParamsRef.current === fetchParamsString) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Update the ref with current parameters
|
|
506
|
+
prevFetchParamsRef.current = fetchParamsString;
|
|
507
|
+
|
|
508
|
+
setLoading(true);
|
|
509
|
+
try {
|
|
510
|
+
const response = await CustomFetch(url, method, params);
|
|
511
|
+
|
|
512
|
+
if (response.data.data) {
|
|
513
|
+
// Check if the response has the expected structure with header and rows
|
|
514
|
+
if (response.data.data.header && response.data.data.rows) {
|
|
515
|
+
// Set the headers from the response with the new structure
|
|
516
|
+
const headerEntries = Object.entries(
|
|
517
|
+
response.data.data.header
|
|
518
|
+
);
|
|
519
|
+
setGridHeaders(
|
|
520
|
+
headerEntries.map(([key, config]) => ({
|
|
521
|
+
key,
|
|
522
|
+
label: config.label || key,
|
|
523
|
+
sort: config.sort || key,
|
|
524
|
+
type: config.type || 'text',
|
|
525
|
+
filter: config.filter || null,
|
|
526
|
+
onClick: config.onClick || null,
|
|
527
|
+
}))
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
// Initialize filter values
|
|
531
|
+
const initialFilterValues = {};
|
|
532
|
+
headerEntries.forEach(([key, config]) => {
|
|
533
|
+
if (config.filter) {
|
|
534
|
+
initialFilterValues[key] = '';
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
setFilterValues(initialFilterValues);
|
|
538
|
+
|
|
539
|
+
// Set the rows data
|
|
540
|
+
const newData = response.data.data.rows || [];
|
|
541
|
+
setGridData(newData);
|
|
542
|
+
setSortedData(newData);
|
|
543
|
+
|
|
544
|
+
// Notify parent component of data change
|
|
545
|
+
if (onDataChange) {
|
|
546
|
+
onDataChange(newData);
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
// Fallback to the previous behavior if the structure is different
|
|
550
|
+
const newData = response.data.data.data || [];
|
|
551
|
+
setGridData(newData);
|
|
552
|
+
setSortedData(newData);
|
|
553
|
+
|
|
554
|
+
// Notify parent component of data change
|
|
555
|
+
if (onDataChange) {
|
|
556
|
+
onDataChange(newData);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error('Error fetching grid data:', error);
|
|
562
|
+
} finally {
|
|
563
|
+
setLoading(false);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
fetchData();
|
|
568
|
+
}, [
|
|
569
|
+
// Only depend on the specific properties we need, not the entire objects
|
|
570
|
+
settings?.fetch?.url,
|
|
571
|
+
settings?.fetch?.method,
|
|
572
|
+
// Use JSON.stringify for the params to detect deep changes
|
|
573
|
+
JSON.stringify(settings?.fetch?.params),
|
|
574
|
+
JSON.stringify(settingsFilterValues),
|
|
575
|
+
// Only depend on data if we're not fetching
|
|
576
|
+
!settings?.fetch?.url ? JSON.stringify(configData) : null,
|
|
577
|
+
// Include static data dependencies
|
|
578
|
+
settings?.useStaticData,
|
|
579
|
+
JSON.stringify(settings?.staticData),
|
|
580
|
+
onDataChange,
|
|
581
|
+
]);
|
|
582
|
+
|
|
583
|
+
// Generate year options for dropdown
|
|
584
|
+
const generateYearOptions = useCallback((startYear) => {
|
|
585
|
+
const currentYear = new Date().getFullYear();
|
|
586
|
+
const years = [];
|
|
587
|
+
|
|
588
|
+
// Start from the specified year and go up to current year
|
|
589
|
+
for (let year = startYear; year <= currentYear + 1; year++) {
|
|
590
|
+
years.push({ value: year.toString(), label: year.toString() });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return years;
|
|
594
|
+
}, []);
|
|
595
|
+
|
|
596
|
+
// Fetch filter options from URL
|
|
597
|
+
const fetchFilterOptions = useCallback(async (filter) => {
|
|
598
|
+
if (!filter.url) return;
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const response = await CustomFetch(filter.url, 'POST', {});
|
|
602
|
+
if (response.data && response.data.data) {
|
|
603
|
+
setFilterOptions(prev => ({
|
|
604
|
+
...prev,
|
|
605
|
+
[filter.id]: response.data.data
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
} catch (error) {
|
|
609
|
+
console.error(`Error fetching options for ${filter.id}:`, error);
|
|
610
|
+
toast.error(`Failed to load ${filter.label} options`);
|
|
611
|
+
}
|
|
612
|
+
}, []);
|
|
613
|
+
|
|
614
|
+
// Initialize settings filters and fetch options
|
|
615
|
+
useEffect(() => {
|
|
616
|
+
if (settings?.filters && settings.filters.length > 0) {
|
|
617
|
+
const initialValues = {};
|
|
618
|
+
|
|
619
|
+
settings.filters.forEach((filter) => {
|
|
620
|
+
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && (filter.id === 'year' || filter.startYear)) {
|
|
621
|
+
// For year filters with 'now' value, set to current year
|
|
622
|
+
if (filter.value === 'now') {
|
|
623
|
+
const currentYear = new Date().getFullYear().toString();
|
|
624
|
+
// For multiselect, check isMulti to determine if it should be array or single value
|
|
625
|
+
if (filter.type === 'multiselect') {
|
|
626
|
+
const value = filter.isMulti === false ? currentYear : [currentYear];
|
|
627
|
+
initialValues[filter.id] = value;
|
|
628
|
+
} else {
|
|
629
|
+
initialValues[filter.id] = currentYear;
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
if (filter.type === 'multiselect') {
|
|
633
|
+
initialValues[filter.id] = filter.isMulti === false ? (filter.value || '') : (filter.value || []);
|
|
634
|
+
} else {
|
|
635
|
+
initialValues[filter.id] = filter.value || '';
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} else if (filter.type === 'multiselect') {
|
|
639
|
+
// For multiselect, handle 'now' value and initialize based on isMulti
|
|
640
|
+
if (filter.value === 'now') {
|
|
641
|
+
const currentYear = new Date().getFullYear().toString();
|
|
642
|
+
initialValues[filter.id] = filter.isMulti === false ? currentYear : [currentYear];
|
|
643
|
+
} else {
|
|
644
|
+
if (filter.isMulti === false) {
|
|
645
|
+
// Single select mode - use single value
|
|
646
|
+
initialValues[filter.id] = Array.isArray(filter.value) ? filter.value[0] || '' : (filter.value || '');
|
|
647
|
+
} else {
|
|
648
|
+
// Multi select mode - use array
|
|
649
|
+
initialValues[filter.id] = Array.isArray(filter.value) ? filter.value : (filter.value ? [filter.value] : []);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
// For other filter types
|
|
654
|
+
if (filter.value === 'now') {
|
|
655
|
+
initialValues[filter.id] = new Date().getFullYear().toString();
|
|
656
|
+
} else {
|
|
657
|
+
initialValues[filter.id] = filter.value || '';
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Fetch options for dropdowns with URL
|
|
662
|
+
if ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.url) {
|
|
663
|
+
fetchFilterOptions(filter);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
setSettingsFilterValues(initialValues);
|
|
668
|
+
}
|
|
669
|
+
}, [settings?.filters, fetchFilterOptions]);
|
|
670
|
+
|
|
671
|
+
// Handle settings filter change
|
|
672
|
+
const handleSettingsFilterChange = useCallback(
|
|
673
|
+
(filterId, value) => {
|
|
674
|
+
setSettingsFilterValues((prev) => ({
|
|
675
|
+
...prev,
|
|
676
|
+
[filterId]: value,
|
|
677
|
+
}));
|
|
678
|
+
|
|
679
|
+
// Update fetch params with the new filter value
|
|
680
|
+
if (settings?.fetch?.params) {
|
|
681
|
+
let processedValue = value;
|
|
682
|
+
|
|
683
|
+
// For multiselect, convert array to appropriate format for API
|
|
684
|
+
if (Array.isArray(value)) {
|
|
685
|
+
// Keep as array - most APIs expect this format for multiple values
|
|
686
|
+
processedValue = value;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const updatedParams = {
|
|
690
|
+
...settings.fetch.params,
|
|
691
|
+
[filterId]: processedValue,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// Update the settings object
|
|
695
|
+
if (config.settings && config.settings.fetch) {
|
|
696
|
+
config.settings.fetch.params = updatedParams;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Trigger a re-fetch with the new params
|
|
700
|
+
prevFetchParamsRef.current = null;
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
[settings, config]
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
// Apply custom styling from userProfile if available
|
|
707
|
+
const getCustomStyles = () => {
|
|
708
|
+
const customStyles = {};
|
|
709
|
+
|
|
710
|
+
if (userProfile?.settings?.style) {
|
|
711
|
+
const { style } = userProfile.settings;
|
|
712
|
+
|
|
713
|
+
if (style.hoverColor) {
|
|
714
|
+
customStyles.hoverColor = {
|
|
715
|
+
'--hover-color': style.hoverColor,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return customStyles;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// CSS for sort indicators
|
|
724
|
+
const getSortIndicatorStyle = (key) => {
|
|
725
|
+
if (sortConfig.key !== key) {
|
|
726
|
+
return { opacity: 0.3 }; // Dim the icon when not sorting by this column
|
|
727
|
+
}
|
|
728
|
+
return {}; // Full opacity when sorting by this column
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// Handle row click
|
|
732
|
+
const handleRowClick = useCallback((row, rowIndex) => {
|
|
733
|
+
if (onRowClick) {
|
|
734
|
+
onRowClick(row, rowIndex);
|
|
735
|
+
}
|
|
736
|
+
}, [onRowClick]);
|
|
737
|
+
|
|
738
|
+
// Handle cell click with support for date picker and contact selection
|
|
739
|
+
const handleCellClick = useCallback(async (e, header, row) => {
|
|
740
|
+
e.stopPropagation(); // Prevent row click
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
// Debug logging to check configuration
|
|
744
|
+
const dateOnlyConfig = header.onClick.dateOnly || userProfile?.settings?.dateOnly || false;
|
|
745
|
+
console.log('Cell click configuration:', {
|
|
746
|
+
column: header.label,
|
|
747
|
+
clickType: header.onClick.type || 'date',
|
|
748
|
+
hasContactSelection: !!header.onClick.contactSelection,
|
|
749
|
+
headerDateOnly: header.onClick.dateOnly,
|
|
750
|
+
userProfileDateOnly: userProfile?.settings?.dateOnly,
|
|
751
|
+
finalDateOnly: dateOnlyConfig
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// Determine the interaction type
|
|
755
|
+
const clickType = header.onClick.type || 'date';
|
|
756
|
+
|
|
757
|
+
if (clickType === 'date_with_contacts') {
|
|
758
|
+
// Multi-step process: Date selection first, then contact selection
|
|
759
|
+
await handleDateWithContactsFlow(header, row, dateOnlyConfig);
|
|
760
|
+
} else {
|
|
761
|
+
// Simple date selection (backward compatibility)
|
|
762
|
+
await handleSimpleDateSelection(header, row, dateOnlyConfig);
|
|
763
|
+
}
|
|
764
|
+
} catch (error) {
|
|
765
|
+
console.error('Error handling cell click:', error);
|
|
766
|
+
toast.error('An error occurred while opening the dialog');
|
|
767
|
+
}
|
|
768
|
+
}, [settings, settingsFilterValues, onDataChange, userProfile]);
|
|
769
|
+
|
|
770
|
+
// Handle simple date selection (existing functionality)
|
|
771
|
+
const handleSimpleDateSelection = useCallback(async (header, row, dateOnlyConfig) => {
|
|
772
|
+
await showDatePickerDialog({
|
|
773
|
+
title: header.onClick.title || 'Select Date',
|
|
774
|
+
message: header.onClick.message || header.onClick.placeholder || `Please select a date for ${header.label}:`,
|
|
775
|
+
confirmLabel: 'Set Date',
|
|
776
|
+
cancelLabel: 'Cancel',
|
|
777
|
+
defaultDate: header.onClick.params?.value === 'now' ? 'now' : (row[header.key] || null),
|
|
778
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
779
|
+
// Timezone configuration - can be set via userProfile or header config
|
|
780
|
+
timezone: header.onClick.timezone || userProfile?.settings?.timezone || 'auto',
|
|
781
|
+
forcePerth: header.onClick.forcePerth || userProfile?.settings?.forcePerth || false,
|
|
782
|
+
// Date field type - set to true for DATE fields, false for DATETIME fields
|
|
783
|
+
dateOnly: dateOnlyConfig,
|
|
784
|
+
onConfirm: async (selectedDateISO) => {
|
|
785
|
+
await submitDateUpdate(header, row, selectedDateISO);
|
|
786
|
+
},
|
|
787
|
+
onCancel: () => {
|
|
788
|
+
// Do nothing when cancelled
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
}, [settings, settingsFilterValues, onDataChange, userProfile]);
|
|
792
|
+
|
|
793
|
+
// Handle date selection with contact selection flow
|
|
794
|
+
const handleDateWithContactsFlow = useCallback(async (header, row, dateOnlyConfig) => {
|
|
795
|
+
// Step 1: Date selection
|
|
796
|
+
await showDatePickerDialog({
|
|
797
|
+
title: header.onClick.title || 'Select Date',
|
|
798
|
+
message: header.onClick.message || header.onClick.placeholder || `Please select a date for ${header.label}:`,
|
|
799
|
+
confirmLabel: 'Next: Select Contacts',
|
|
800
|
+
cancelLabel: 'Cancel',
|
|
801
|
+
defaultDate: header.onClick.params?.value === 'now' ? 'now' : (row[header.key] || null),
|
|
802
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
803
|
+
timezone: header.onClick.timezone || userProfile?.settings?.timezone || 'auto',
|
|
804
|
+
forcePerth: header.onClick.forcePerth || userProfile?.settings?.forcePerth || false,
|
|
805
|
+
dateOnly: dateOnlyConfig,
|
|
806
|
+
onConfirm: async (selectedDateISO) => {
|
|
807
|
+
// Step 2: Contact selection
|
|
808
|
+
const contactFieldKey = header.key + '_contacts';
|
|
809
|
+
const existingContacts = row[contactFieldKey] || [];
|
|
810
|
+
|
|
811
|
+
console.log('Opening contact selector with row context:', {
|
|
812
|
+
rowKeys: Object.keys(row),
|
|
813
|
+
rowClientId: row.client_id,
|
|
814
|
+
urlParams: header.onClick.contactSelection?.urlParams,
|
|
815
|
+
fullRow: row
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
await showContactSelectorModal({
|
|
819
|
+
title: header.onClick.contactSelection?.title || 'Select Contacts',
|
|
820
|
+
message: header.onClick.contactSelection?.message || `Add contacts for ${header.label}:`,
|
|
821
|
+
confirmLabel: 'Save',
|
|
822
|
+
cancelLabel: 'Cancel',
|
|
823
|
+
existingContacts: Array.isArray(existingContacts) ? existingContacts : [],
|
|
824
|
+
contactConfig: {
|
|
825
|
+
contactsUrl: header.onClick.contactSelection?.contactsUrl || '/ajax/contacts/dropdown',
|
|
826
|
+
urlParams: header.onClick.contactSelection?.urlParams || {},
|
|
827
|
+
allowManualEntry: header.onClick.contactSelection?.allowManualEntry !== false,
|
|
828
|
+
fields: header.onClick.contactSelection?.fields || ['name', 'email'],
|
|
829
|
+
multiple: header.onClick.contactSelection?.multiple !== false,
|
|
830
|
+
maxContacts: header.onClick.contactSelection?.maxContacts || 10
|
|
831
|
+
},
|
|
832
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
833
|
+
rowContext: row,
|
|
834
|
+
onConfirm: async (selectedContacts) => {
|
|
835
|
+
// Submit both date and contacts
|
|
836
|
+
await submitDateAndContactsUpdate(header, row, selectedDateISO, selectedContacts);
|
|
837
|
+
},
|
|
838
|
+
onCancel: () => {
|
|
839
|
+
// User cancelled contact selection - do nothing
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
},
|
|
843
|
+
onCancel: () => {
|
|
844
|
+
// User cancelled date selection - do nothing
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
}, [settings, settingsFilterValues, onDataChange, userProfile]);
|
|
848
|
+
|
|
849
|
+
// Submit date update only
|
|
850
|
+
const submitDateUpdate = useCallback(async (header, row, selectedDateISO) => {
|
|
851
|
+
try {
|
|
852
|
+
// Replace {id} in the URL with the row's ID
|
|
853
|
+
const url = header.onClick.url.replace('{id}', row.id);
|
|
854
|
+
|
|
855
|
+
// Show loading toast
|
|
856
|
+
toast.info('Updating...', { autoClose: 1000 });
|
|
857
|
+
|
|
858
|
+
// Extract field and value from params
|
|
859
|
+
const { field, ...otherParams } = header.onClick.params;
|
|
860
|
+
|
|
861
|
+
// Create a new params object with the field as the key
|
|
862
|
+
const params = {
|
|
863
|
+
...otherParams,
|
|
864
|
+
_method: header.onClick.method, // Add _method for PUT/DELETE requests
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
// Set the field as the key with the selected date value
|
|
868
|
+
params[field] = selectedDateISO;
|
|
869
|
+
|
|
870
|
+
// Make the API call
|
|
871
|
+
const response = await CustomFetch(url, header.onClick.method, params);
|
|
872
|
+
|
|
873
|
+
if (response.data.error) {
|
|
874
|
+
toast.error(response.data.error);
|
|
875
|
+
} else {
|
|
876
|
+
toast.success('Date updated successfully', { autoClose: 1000 });
|
|
877
|
+
await refreshGridData();
|
|
878
|
+
}
|
|
879
|
+
} catch (error) {
|
|
880
|
+
console.error('Error updating date:', error);
|
|
881
|
+
toast.error('An error occurred while updating');
|
|
882
|
+
}
|
|
883
|
+
}, []);
|
|
884
|
+
|
|
885
|
+
// Submit date and contacts update
|
|
886
|
+
const submitDateAndContactsUpdate = useCallback(async (header, row, selectedDateISO, selectedContacts) => {
|
|
887
|
+
try {
|
|
888
|
+
// Replace {id} in the URL with the row's ID
|
|
889
|
+
const url = header.onClick.url.replace('{id}', row.id);
|
|
890
|
+
|
|
891
|
+
// Show loading toast
|
|
892
|
+
toast.info('Saving date and contacts...', { autoClose: 1000 });
|
|
893
|
+
|
|
894
|
+
// Extract field and value from params
|
|
895
|
+
const { field, ...otherParams } = header.onClick.params;
|
|
896
|
+
|
|
897
|
+
// Create a new params object with both date and contacts
|
|
898
|
+
const params = {
|
|
899
|
+
...otherParams,
|
|
900
|
+
_method: header.onClick.method, // Add _method for PUT/DELETE requests
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
// Set the date field
|
|
904
|
+
params[field] = selectedDateISO;
|
|
905
|
+
|
|
906
|
+
// Set the contacts field (field name + '_contacts')
|
|
907
|
+
const contactsField = field + '_contacts';
|
|
908
|
+
params[contactsField] = selectedContacts;
|
|
909
|
+
|
|
910
|
+
console.log('Submitting date and contacts:', params);
|
|
911
|
+
|
|
912
|
+
// Make the API call
|
|
913
|
+
const response = await CustomFetch(url, header.onClick.method, params);
|
|
914
|
+
|
|
915
|
+
if (response.data.error) {
|
|
916
|
+
toast.error(response.data.error);
|
|
917
|
+
} else {
|
|
918
|
+
toast.success('Date and contacts saved successfully', { autoClose: 1500 });
|
|
919
|
+
await refreshGridData();
|
|
920
|
+
}
|
|
921
|
+
} catch (error) {
|
|
922
|
+
console.error('Error updating date and contacts:', error);
|
|
923
|
+
toast.error('An error occurred while saving');
|
|
924
|
+
}
|
|
925
|
+
}, []);
|
|
926
|
+
|
|
927
|
+
// Refresh grid data helper function
|
|
928
|
+
const refreshGridData = useCallback(async () => {
|
|
929
|
+
try {
|
|
930
|
+
// Refresh the data
|
|
931
|
+
prevFetchParamsRef.current = null;
|
|
932
|
+
|
|
933
|
+
// Force a re-fetch
|
|
934
|
+
const fetchUrl = settings.fetch.url;
|
|
935
|
+
const fetchMethod = settings.fetch.method || 'POST';
|
|
936
|
+
const fetchParams = {
|
|
937
|
+
...(settings.fetch.params || {}),
|
|
938
|
+
...settingsFilterValues,
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
setLoading(true);
|
|
942
|
+
const fetchResponse = await CustomFetch(fetchUrl, fetchMethod, fetchParams);
|
|
943
|
+
if (fetchResponse.data.data) {
|
|
944
|
+
if (fetchResponse.data.data.header && fetchResponse.data.data.rows) {
|
|
945
|
+
const newData = fetchResponse.data.data.rows || [];
|
|
946
|
+
setGridData(newData);
|
|
947
|
+
setSortedData(newData);
|
|
948
|
+
if (onDataChange) {
|
|
949
|
+
onDataChange(newData);
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
const newData = fetchResponse.data.data.data || [];
|
|
953
|
+
setGridData(newData);
|
|
954
|
+
setSortedData(newData);
|
|
955
|
+
if (onDataChange) {
|
|
956
|
+
onDataChange(newData);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} catch (error) {
|
|
961
|
+
console.error('Error refreshing data:', error);
|
|
962
|
+
toast.error('Error refreshing data');
|
|
963
|
+
} finally {
|
|
964
|
+
setLoading(false);
|
|
965
|
+
}
|
|
966
|
+
}, [settings, settingsFilterValues, onDataChange]);
|
|
967
|
+
|
|
968
|
+
return (
|
|
969
|
+
<div className={styles.gridContainer}>
|
|
970
|
+
{/* Settings Filters */}
|
|
971
|
+
{settings?.filters && settings.filters.length > 0 && (
|
|
972
|
+
<div className={styles.settingsFiltersContainer}>
|
|
973
|
+
<div className={styles.settingsFiltersHeader}>
|
|
974
|
+
<div
|
|
975
|
+
className={styles.headerLeft}
|
|
976
|
+
onClick={() =>
|
|
977
|
+
setFiltersCollapsed(!filtersCollapsed)
|
|
978
|
+
}
|
|
979
|
+
>
|
|
980
|
+
<div className={styles.settingsFiltersTitle}>
|
|
981
|
+
<svg
|
|
982
|
+
width="16"
|
|
983
|
+
height="16"
|
|
984
|
+
viewBox="0 0 24 24"
|
|
985
|
+
fill="none"
|
|
986
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
987
|
+
>
|
|
988
|
+
<path
|
|
989
|
+
d="M4 6H20M7 12H17M9 18H15"
|
|
990
|
+
stroke="currentColor"
|
|
991
|
+
strokeWidth="2"
|
|
992
|
+
strokeLinecap="round"
|
|
993
|
+
strokeLinejoin="round"
|
|
994
|
+
/>
|
|
995
|
+
</svg>
|
|
996
|
+
Filters
|
|
997
|
+
{/* Show active filters count when collapsed */}
|
|
998
|
+
{filtersCollapsed && (
|
|
999
|
+
<div
|
|
1000
|
+
className={
|
|
1001
|
+
styles.activeFiltersIndicator
|
|
1002
|
+
}
|
|
1003
|
+
>
|
|
1004
|
+
{Object.values(
|
|
1005
|
+
settingsFilterValues
|
|
1006
|
+
).some((value) => value !== '') && (
|
|
1007
|
+
<>
|
|
1008
|
+
<span>Active:</span>
|
|
1009
|
+
<span
|
|
1010
|
+
className={
|
|
1011
|
+
styles.filterBadge
|
|
1012
|
+
}
|
|
1013
|
+
>
|
|
1014
|
+
{
|
|
1015
|
+
Object.values(
|
|
1016
|
+
settingsFilterValues
|
|
1017
|
+
).filter(
|
|
1018
|
+
(value) =>
|
|
1019
|
+
value !== ''
|
|
1020
|
+
).length
|
|
1021
|
+
}
|
|
1022
|
+
</span>
|
|
1023
|
+
</>
|
|
1024
|
+
)}
|
|
1025
|
+
</div>
|
|
1026
|
+
)}
|
|
1027
|
+
</div>
|
|
1028
|
+
|
|
1029
|
+
{/* Toggle icon */}
|
|
1030
|
+
<svg
|
|
1031
|
+
className={`${styles.toggleIcon} ${
|
|
1032
|
+
filtersCollapsed ? styles.collapsed : ''
|
|
1033
|
+
}`}
|
|
1034
|
+
width="16"
|
|
1035
|
+
height="16"
|
|
1036
|
+
viewBox="0 0 24 24"
|
|
1037
|
+
fill="none"
|
|
1038
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1039
|
+
>
|
|
1040
|
+
<path
|
|
1041
|
+
d="M19 9l-7 7-7-7"
|
|
1042
|
+
stroke="currentColor"
|
|
1043
|
+
strokeWidth="2"
|
|
1044
|
+
strokeLinecap="round"
|
|
1045
|
+
strokeLinejoin="round"
|
|
1046
|
+
/>
|
|
1047
|
+
</svg>
|
|
1048
|
+
</div>
|
|
1049
|
+
|
|
1050
|
+
{/* Clear filters button */}
|
|
1051
|
+
<button
|
|
1052
|
+
className={styles.clearFiltersButton}
|
|
1053
|
+
onClick={(e) => {
|
|
1054
|
+
e.stopPropagation(); // Prevent toggling when clicking the button
|
|
1055
|
+
// Initialize with default values
|
|
1056
|
+
const initialValues = {};
|
|
1057
|
+
settings.filters.forEach((filter) => {
|
|
1058
|
+
if (
|
|
1059
|
+
(filter.type === 'dropdown' || filter.type === 'multiselect') &&
|
|
1060
|
+
(filter.id === 'year' || filter.startYear) &&
|
|
1061
|
+
filter.value === 'now'
|
|
1062
|
+
) {
|
|
1063
|
+
const currentYear = new Date()
|
|
1064
|
+
.getFullYear()
|
|
1065
|
+
.toString();
|
|
1066
|
+
if (filter.type === 'multiselect') {
|
|
1067
|
+
initialValues[filter.id] = filter.isMulti === false ? currentYear : [currentYear];
|
|
1068
|
+
} else {
|
|
1069
|
+
initialValues[filter.id] = currentYear;
|
|
1070
|
+
}
|
|
1071
|
+
} else if (filter.type === 'multiselect') {
|
|
1072
|
+
if (filter.value === 'now') {
|
|
1073
|
+
const currentYear = new Date().getFullYear().toString();
|
|
1074
|
+
initialValues[filter.id] = filter.isMulti === false ? currentYear : [currentYear];
|
|
1075
|
+
} else {
|
|
1076
|
+
if (filter.isMulti === false) {
|
|
1077
|
+
initialValues[filter.id] = Array.isArray(filter.value) ? filter.value[0] || '' : (filter.value || '');
|
|
1078
|
+
} else {
|
|
1079
|
+
initialValues[filter.id] = Array.isArray(filter.value) ? filter.value : (filter.value ? [filter.value] : []);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
} else {
|
|
1083
|
+
if (filter.value === 'now') {
|
|
1084
|
+
initialValues[filter.id] = new Date().getFullYear().toString();
|
|
1085
|
+
} else {
|
|
1086
|
+
initialValues[filter.id] = filter.value || '';
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
setSettingsFilterValues(initialValues);
|
|
1091
|
+
|
|
1092
|
+
// Reset fetch params
|
|
1093
|
+
prevFetchParamsRef.current = null;
|
|
1094
|
+
}}
|
|
1095
|
+
>
|
|
1096
|
+
<svg
|
|
1097
|
+
viewBox="0 0 24 24"
|
|
1098
|
+
fill="none"
|
|
1099
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
1100
|
+
>
|
|
1101
|
+
<path
|
|
1102
|
+
d="M19 7l-7 7-7-7"
|
|
1103
|
+
stroke="currentColor"
|
|
1104
|
+
strokeWidth="2"
|
|
1105
|
+
strokeLinecap="round"
|
|
1106
|
+
strokeLinejoin="round"
|
|
1107
|
+
transform="rotate(90, 12, 12)"
|
|
1108
|
+
/>
|
|
1109
|
+
</svg>
|
|
1110
|
+
Clear
|
|
1111
|
+
</button>
|
|
1112
|
+
</div>
|
|
1113
|
+
|
|
1114
|
+
<div
|
|
1115
|
+
className={`${styles.settingsFiltersContent} ${
|
|
1116
|
+
filtersCollapsed ? styles.collapsed : ''
|
|
1117
|
+
}`}
|
|
1118
|
+
>
|
|
1119
|
+
{settings.filters.map((filter, index) => (
|
|
1120
|
+
<div
|
|
1121
|
+
key={`settings-filter-${index}`}
|
|
1122
|
+
className={styles.settingsFilterItem}
|
|
1123
|
+
>
|
|
1124
|
+
<label className={styles.settingsFilterLabel}>
|
|
1125
|
+
{filter.label}
|
|
1126
|
+
</label>
|
|
1127
|
+
{filter.type === 'dropdown' &&
|
|
1128
|
+
filter.id === 'year' ? (
|
|
1129
|
+
<select
|
|
1130
|
+
className={
|
|
1131
|
+
styles.settingsFilterDropdown
|
|
1132
|
+
}
|
|
1133
|
+
value={
|
|
1134
|
+
settingsFilterValues[filter.id] ||
|
|
1135
|
+
''
|
|
1136
|
+
}
|
|
1137
|
+
onChange={(e) =>
|
|
1138
|
+
handleSettingsFilterChange(
|
|
1139
|
+
filter.id,
|
|
1140
|
+
e.target.value
|
|
1141
|
+
)
|
|
1142
|
+
}
|
|
1143
|
+
>
|
|
1144
|
+
{generateYearOptions(
|
|
1145
|
+
filter.startYear || 2000
|
|
1146
|
+
).map((option) => (
|
|
1147
|
+
<option
|
|
1148
|
+
key={option.value}
|
|
1149
|
+
value={option.value}
|
|
1150
|
+
>
|
|
1151
|
+
{option.label}
|
|
1152
|
+
</option>
|
|
1153
|
+
))}
|
|
1154
|
+
</select>
|
|
1155
|
+
) : filter.type === 'dropdown' && filter.url ? (
|
|
1156
|
+
<select
|
|
1157
|
+
className={
|
|
1158
|
+
styles.settingsFilterDropdown
|
|
1159
|
+
}
|
|
1160
|
+
value={
|
|
1161
|
+
settingsFilterValues[filter.id] ||
|
|
1162
|
+
''
|
|
1163
|
+
}
|
|
1164
|
+
onChange={(e) =>
|
|
1165
|
+
handleSettingsFilterChange(
|
|
1166
|
+
filter.id,
|
|
1167
|
+
e.target.value
|
|
1168
|
+
)
|
|
1169
|
+
}
|
|
1170
|
+
>
|
|
1171
|
+
<option value="">Select {filter.label}</option>
|
|
1172
|
+
{filterOptions[filter.id] &&
|
|
1173
|
+
filterOptions[filter.id].map((option) => (
|
|
1174
|
+
<option
|
|
1175
|
+
key={option.value || option.id}
|
|
1176
|
+
value={option.value || option.id}
|
|
1177
|
+
>
|
|
1178
|
+
{option.label || option.name || option.title}
|
|
1179
|
+
</option>
|
|
1180
|
+
))}
|
|
1181
|
+
</select>
|
|
1182
|
+
) : filter.type === 'dropdown' ? (
|
|
1183
|
+
<select
|
|
1184
|
+
className={
|
|
1185
|
+
styles.settingsFilterDropdown
|
|
1186
|
+
}
|
|
1187
|
+
value={
|
|
1188
|
+
settingsFilterValues[filter.id] ||
|
|
1189
|
+
''
|
|
1190
|
+
}
|
|
1191
|
+
onChange={(e) =>
|
|
1192
|
+
handleSettingsFilterChange(
|
|
1193
|
+
filter.id,
|
|
1194
|
+
e.target.value
|
|
1195
|
+
)
|
|
1196
|
+
}
|
|
1197
|
+
>
|
|
1198
|
+
<option value="">Select {filter.label}</option>
|
|
1199
|
+
{filter.options &&
|
|
1200
|
+
filter.options.map((option) => (
|
|
1201
|
+
<option
|
|
1202
|
+
key={option.value}
|
|
1203
|
+
value={option.value}
|
|
1204
|
+
>
|
|
1205
|
+
{option.label}
|
|
1206
|
+
</option>
|
|
1207
|
+
))}
|
|
1208
|
+
</select>
|
|
1209
|
+
) : filter.type === 'multiselect' ? (
|
|
1210
|
+
<div className={styles.multiselectContainer}>
|
|
1211
|
+
<MultiSelect
|
|
1212
|
+
options={
|
|
1213
|
+
(filter.id === 'year' || filter.startYear) ?
|
|
1214
|
+
generateYearOptions(filter.startYear || 2000) :
|
|
1215
|
+
filter.url ?
|
|
1216
|
+
(filterOptions[filter.id] || []).map(option => ({
|
|
1217
|
+
value: option.value || option.id,
|
|
1218
|
+
label: option.label || option.name || option.title
|
|
1219
|
+
})) :
|
|
1220
|
+
filter.options || []
|
|
1221
|
+
}
|
|
1222
|
+
inputValue={(() => {
|
|
1223
|
+
const currentValue = settingsFilterValues[filter.id];
|
|
1224
|
+
const availableOptions = (filter.id === 'year' || filter.startYear) ?
|
|
1225
|
+
generateYearOptions(filter.startYear || 2000) :
|
|
1226
|
+
filter.url ?
|
|
1227
|
+
(filterOptions[filter.id] || []).map(option => ({
|
|
1228
|
+
value: option.value || option.id,
|
|
1229
|
+
label: option.label || option.name || option.title
|
|
1230
|
+
})) :
|
|
1231
|
+
filter.options || [];
|
|
1232
|
+
|
|
1233
|
+
if (filter.isMulti === false) {
|
|
1234
|
+
// Single select mode - find the matching option object
|
|
1235
|
+
if (currentValue) {
|
|
1236
|
+
const matchingOption = availableOptions.find(opt => opt.value === currentValue || opt.value === currentValue.toString());
|
|
1237
|
+
return matchingOption || null;
|
|
1238
|
+
}
|
|
1239
|
+
return null;
|
|
1240
|
+
} else {
|
|
1241
|
+
// Multi select mode - return array of matching option objects
|
|
1242
|
+
if (Array.isArray(currentValue) && currentValue.length > 0) {
|
|
1243
|
+
return currentValue.map(val => {
|
|
1244
|
+
const matchingOption = availableOptions.find(opt => opt.value === val || opt.value === val.toString());
|
|
1245
|
+
return matchingOption || { value: val, label: val };
|
|
1246
|
+
}).filter(Boolean);
|
|
1247
|
+
}
|
|
1248
|
+
return [];
|
|
1249
|
+
}
|
|
1250
|
+
})()}
|
|
1251
|
+
onChange={(selectedOptions) => {
|
|
1252
|
+
if (filter.isMulti === false) {
|
|
1253
|
+
// Single select mode - extract value from the selected option object
|
|
1254
|
+
const value = selectedOptions ? selectedOptions.value : '';
|
|
1255
|
+
handleSettingsFilterChange(filter.id, value);
|
|
1256
|
+
} else {
|
|
1257
|
+
// Multi select mode - extract values from array of option objects
|
|
1258
|
+
const values = Array.isArray(selectedOptions)
|
|
1259
|
+
? selectedOptions.map(opt => opt.value)
|
|
1260
|
+
: [];
|
|
1261
|
+
handleSettingsFilterChange(filter.id, values);
|
|
1262
|
+
}
|
|
1263
|
+
}}
|
|
1264
|
+
placeholder={`Select ${filter.label}`}
|
|
1265
|
+
multi={filter.isMulti !== false} // Default to true, false only if explicitly set
|
|
1266
|
+
settings={{
|
|
1267
|
+
id: filter.id,
|
|
1268
|
+
limit: filter.limit || undefined
|
|
1269
|
+
}}
|
|
1270
|
+
/>
|
|
1271
|
+
</div>
|
|
1272
|
+
) : filter.type === 'text' ? (
|
|
1273
|
+
<input
|
|
1274
|
+
type="text"
|
|
1275
|
+
className={styles.settingsFilterInput}
|
|
1276
|
+
value={
|
|
1277
|
+
settingsFilterValues[filter.id] ||
|
|
1278
|
+
''
|
|
1279
|
+
}
|
|
1280
|
+
onChange={(e) =>
|
|
1281
|
+
handleSettingsFilterChange(
|
|
1282
|
+
filter.id,
|
|
1283
|
+
e.target.value
|
|
1284
|
+
)
|
|
1285
|
+
}
|
|
1286
|
+
placeholder={filter.placeholder || ''}
|
|
1287
|
+
/>
|
|
1288
|
+
) : null}
|
|
1289
|
+
</div>
|
|
1290
|
+
))}
|
|
1291
|
+
</div>
|
|
1292
|
+
</div>
|
|
1293
|
+
)}
|
|
1294
|
+
|
|
1295
|
+
{loading ? (
|
|
1296
|
+
<div className={styles.loadingContainer}>
|
|
1297
|
+
<div className={styles.loadingSpinner}></div>
|
|
1298
|
+
<p>Loading data...</p>
|
|
1299
|
+
</div>
|
|
1300
|
+
) : (
|
|
1301
|
+
<table className={styles.gridTable}>
|
|
1302
|
+
<thead>
|
|
1303
|
+
<tr>
|
|
1304
|
+
{/* Use gridHeaders if available, otherwise fall back to headers from columns */}
|
|
1305
|
+
{gridHeaders.length > 0
|
|
1306
|
+
? gridHeaders.map((header, index) => (
|
|
1307
|
+
<th
|
|
1308
|
+
key={`header-${index}`}
|
|
1309
|
+
className={`${styles.gridHeader} ${styles.sortableHeader}`}
|
|
1310
|
+
onClick={() =>
|
|
1311
|
+
requestSort(header.key)
|
|
1312
|
+
}
|
|
1313
|
+
style={{ cursor: 'pointer' }}
|
|
1314
|
+
>
|
|
1315
|
+
<div className={styles.headerContent}>
|
|
1316
|
+
{header.label}
|
|
1317
|
+
<span
|
|
1318
|
+
className={
|
|
1319
|
+
styles.sortIndicator
|
|
1320
|
+
}
|
|
1321
|
+
style={getSortIndicatorStyle(
|
|
1322
|
+
header.key
|
|
1323
|
+
)}
|
|
1324
|
+
>
|
|
1325
|
+
{sortConfig.key ===
|
|
1326
|
+
header.key &&
|
|
1327
|
+
sortConfig.direction === 'asc'
|
|
1328
|
+
? ' ▲'
|
|
1329
|
+
: ' ▼'}
|
|
1330
|
+
</span>
|
|
1331
|
+
</div>
|
|
1332
|
+
</th>
|
|
1333
|
+
))
|
|
1334
|
+
: headers.map((header, index) => (
|
|
1335
|
+
<th
|
|
1336
|
+
key={`header-${index}`}
|
|
1337
|
+
className={styles.gridHeader}
|
|
1338
|
+
>
|
|
1339
|
+
{header}
|
|
1340
|
+
</th>
|
|
1341
|
+
))}
|
|
1342
|
+
</tr>
|
|
1343
|
+
{/* Filter row */}
|
|
1344
|
+
{gridHeaders.some((header) => header.filter) && (
|
|
1345
|
+
<tr className={styles.filterRow}>
|
|
1346
|
+
{gridHeaders.map((header, index) => (
|
|
1347
|
+
<th
|
|
1348
|
+
key={`filter-${index}`}
|
|
1349
|
+
className={styles.filterCell}
|
|
1350
|
+
>
|
|
1351
|
+
{header.filter ? (
|
|
1352
|
+
<input
|
|
1353
|
+
type={
|
|
1354
|
+
header.filter.type ===
|
|
1355
|
+
'date'
|
|
1356
|
+
? 'date'
|
|
1357
|
+
: 'text'
|
|
1358
|
+
}
|
|
1359
|
+
className={styles.filterInput}
|
|
1360
|
+
placeholder={`Filter ${header.label}...`}
|
|
1361
|
+
value={
|
|
1362
|
+
filterValues[header.key] ||
|
|
1363
|
+
''
|
|
1364
|
+
}
|
|
1365
|
+
onChange={(e) =>
|
|
1366
|
+
handleFilterChange(
|
|
1367
|
+
header.key,
|
|
1368
|
+
e.target.value
|
|
1369
|
+
)
|
|
1370
|
+
}
|
|
1371
|
+
/>
|
|
1372
|
+
) : null}
|
|
1373
|
+
</th>
|
|
1374
|
+
))}
|
|
1375
|
+
</tr>
|
|
1376
|
+
)}
|
|
1377
|
+
</thead>
|
|
1378
|
+
<tbody>
|
|
1379
|
+
{sortedData && sortedData.length > 0 ? (
|
|
1380
|
+
sortedData.map((row, rowIndex) => (
|
|
1381
|
+
<tr
|
|
1382
|
+
key={`row-${rowIndex}`}
|
|
1383
|
+
className={`${styles.gridRow} ${onRowClick ? styles.clickableRow : ''}`}
|
|
1384
|
+
style={getCustomStyles().hoverColor}
|
|
1385
|
+
onClick={() => handleRowClick(row, rowIndex)}
|
|
1386
|
+
>
|
|
1387
|
+
{/* Use gridHeaders if available, otherwise fall back to columns */}
|
|
1388
|
+
{gridHeaders.length > 0
|
|
1389
|
+
? gridHeaders.map(
|
|
1390
|
+
(header, colIndex) => (
|
|
1391
|
+
<td
|
|
1392
|
+
key={`cell-${rowIndex}-${colIndex}`}
|
|
1393
|
+
className={`${
|
|
1394
|
+
styles.gridCell
|
|
1395
|
+
} ${
|
|
1396
|
+
header.onClick &&
|
|
1397
|
+
!row[header.key]
|
|
1398
|
+
? styles.clickableCell
|
|
1399
|
+
: ''
|
|
1400
|
+
}`}
|
|
1401
|
+
onClick={
|
|
1402
|
+
header.onClick &&
|
|
1403
|
+
!row[header.key]
|
|
1404
|
+
? (e) => handleCellClick(e, header, row)
|
|
1405
|
+
: undefined
|
|
1406
|
+
}
|
|
1407
|
+
>
|
|
1408
|
+
{row[header.key] ? (
|
|
1409
|
+
header.onClick ? (
|
|
1410
|
+
<span
|
|
1411
|
+
className={
|
|
1412
|
+
styles.cellWithValue
|
|
1413
|
+
}
|
|
1414
|
+
>
|
|
1415
|
+
{formatCellContent(
|
|
1416
|
+
row[
|
|
1417
|
+
header
|
|
1418
|
+
.key
|
|
1419
|
+
],
|
|
1420
|
+
{
|
|
1421
|
+
...header,
|
|
1422
|
+
type:
|
|
1423
|
+
header.type ||
|
|
1424
|
+
(typeof row[
|
|
1425
|
+
header
|
|
1426
|
+
.key
|
|
1427
|
+
] ===
|
|
1428
|
+
'number'
|
|
1429
|
+
? 'number'
|
|
1430
|
+
: ''),
|
|
1431
|
+
},
|
|
1432
|
+
row
|
|
1433
|
+
)}
|
|
1434
|
+
</span>
|
|
1435
|
+
) : (
|
|
1436
|
+
<span
|
|
1437
|
+
className={
|
|
1438
|
+
styles.cellContent
|
|
1439
|
+
}
|
|
1440
|
+
data-type={
|
|
1441
|
+
header.type ||
|
|
1442
|
+
(typeof row[
|
|
1443
|
+
header
|
|
1444
|
+
.key
|
|
1445
|
+
] ===
|
|
1446
|
+
'number'
|
|
1447
|
+
? 'number'
|
|
1448
|
+
: '')
|
|
1449
|
+
}
|
|
1450
|
+
>
|
|
1451
|
+
{formatCellContent(
|
|
1452
|
+
row[
|
|
1453
|
+
header
|
|
1454
|
+
.key
|
|
1455
|
+
],
|
|
1456
|
+
{
|
|
1457
|
+
...header,
|
|
1458
|
+
type:
|
|
1459
|
+
header.type ||
|
|
1460
|
+
(typeof row[
|
|
1461
|
+
header
|
|
1462
|
+
.key
|
|
1463
|
+
] ===
|
|
1464
|
+
'number'
|
|
1465
|
+
? 'number'
|
|
1466
|
+
: ''),
|
|
1467
|
+
},
|
|
1468
|
+
row
|
|
1469
|
+
)}
|
|
1470
|
+
</span>
|
|
1471
|
+
)
|
|
1472
|
+
) : header.onClick ? (
|
|
1473
|
+
<span
|
|
1474
|
+
className={
|
|
1475
|
+
styles.placeholderText
|
|
1476
|
+
}
|
|
1477
|
+
>
|
|
1478
|
+
{header.onClick
|
|
1479
|
+
.placeholder ||
|
|
1480
|
+
'Click to set'}
|
|
1481
|
+
</span>
|
|
1482
|
+
) : (
|
|
1483
|
+
''
|
|
1484
|
+
)}
|
|
1485
|
+
</td>
|
|
1486
|
+
)
|
|
1487
|
+
)
|
|
1488
|
+
: columns.map((col, colIndex) => (
|
|
1489
|
+
<td
|
|
1490
|
+
key={`cell-${rowIndex}-${colIndex}`}
|
|
1491
|
+
className={styles.gridCell}
|
|
1492
|
+
>
|
|
1493
|
+
<span
|
|
1494
|
+
className={
|
|
1495
|
+
styles.cellContent
|
|
1496
|
+
}
|
|
1497
|
+
data-type={
|
|
1498
|
+
col.type ||
|
|
1499
|
+
(typeof row[
|
|
1500
|
+
col.name
|
|
1501
|
+
] === 'number'
|
|
1502
|
+
? 'number'
|
|
1503
|
+
: '')
|
|
1504
|
+
}
|
|
1505
|
+
>
|
|
1506
|
+
{formatCellContent(
|
|
1507
|
+
row[col.name],
|
|
1508
|
+
col
|
|
1509
|
+
)}
|
|
1510
|
+
</span>
|
|
1511
|
+
</td>
|
|
1512
|
+
))}
|
|
1513
|
+
</tr>
|
|
1514
|
+
))
|
|
1515
|
+
) : (
|
|
1516
|
+
<tr>
|
|
1517
|
+
<td
|
|
1518
|
+
colSpan={
|
|
1519
|
+
gridHeaders.length || headers.length
|
|
1520
|
+
}
|
|
1521
|
+
className={styles.noData}
|
|
1522
|
+
>
|
|
1523
|
+
No data available
|
|
1524
|
+
</td>
|
|
1525
|
+
</tr>
|
|
1526
|
+
)}
|
|
1527
|
+
</tbody>
|
|
1528
|
+
</table>
|
|
1529
|
+
)}
|
|
1530
|
+
</div>
|
|
1531
|
+
);
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
export default GenericGrid;
|