@visns-studio/visns-components 5.10.11 → 5.11.2
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 +7 -7
- package/src/components/cms/DataGrid.jsx +8 -8
- package/src/components/cms/DropZone.jsx +1 -1
- package/src/components/cms/Form.jsx +1 -1
- package/src/components/cms/Gallery.jsx +1 -1
- package/src/components/cms/sorting/Item.jsx +1 -1
- package/src/components/crm/Autocomplete.jsx +3 -3
- package/src/components/crm/Breadcrumb.jsx +4 -4
- package/src/components/crm/BusinessCardOcr.jsx +681 -95
- package/src/components/crm/DataGrid.jsx +34 -32
- package/src/components/crm/Field.jsx +12 -12
- package/src/components/crm/Form.jsx +2 -2
- package/src/components/crm/Navigation.jsx +11 -11
- package/src/components/crm/Notification.jsx +2 -2
- package/src/components/crm/QuickAction.jsx +3 -3
- package/src/components/crm/SectionHeader.jsx +1 -1
- package/src/components/crm/SwitchAccount.jsx +4 -4
- package/src/components/crm/auth/ClientLogin.jsx +2 -2
- package/src/components/crm/auth/ClientOTPVerify.jsx +2 -2
- package/src/components/crm/auth/Login.jsx +3 -3
- package/src/components/crm/auth/Reset.jsx +2 -2
- package/src/components/crm/auth/TwoFactorAuth.jsx +2 -2
- package/src/components/crm/columns/ColumnRenderers.jsx +320 -259
- package/src/components/crm/controls/DataGridSearch.jsx +5 -5
- package/src/components/crm/generic/AlternativeActionModal.jsx +100 -0
- package/src/components/crm/generic/ContactSelectorModal.jsx +423 -0
- package/src/components/crm/generic/DatePickerDialog.jsx +302 -0
- package/src/components/crm/generic/DateRangeSelectorModal.jsx +181 -0
- package/src/components/crm/generic/GenericClientPortal.jsx +1 -1
- package/src/components/crm/generic/GenericDashboard.jsx +4 -4
- package/src/components/crm/generic/GenericDetail.jsx +6 -6
- package/src/components/crm/generic/GenericDynamic.jsx +3 -3
- package/src/components/crm/generic/GenericEditableTable.jsx +4 -4
- package/src/components/crm/generic/GenericFormBuilder.jsx +6 -6
- package/src/components/crm/generic/GenericGrid.jsx +2888 -0
- package/src/components/crm/generic/GenericIndex.jsx +5 -1255
- package/src/components/crm/generic/GenericReport.jsx +966 -646
- package/src/components/crm/generic/GenericReportForm.jsx +194 -0
- package/src/components/crm/generic/NotificationList.jsx +1 -1
- package/src/components/crm/generic/ReasonCollectorModal.jsx +194 -0
- package/src/components/crm/generic/SortableQuoteItems.jsx +3 -3
- package/src/components/crm/generic/shared/formatters.js +231 -0
- package/src/components/crm/generic/shared/gridUtils.js +194 -0
- package/src/components/crm/generic/styles/AlternativeActionModal.css +127 -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/DateRangeSelectorModal.css +115 -0
- package/src/components/crm/generic/styles/GenericIndex.module.scss +382 -0
- package/src/components/crm/generic/styles/GenericReport.module.scss +96 -0
- package/src/components/crm/generic/styles/ReasonCollectorModal.css +217 -0
- package/src/components/crm/modals/GalleryModal.jsx +2 -2
- package/src/components/crm/sorting/Item.jsx +3 -3
- package/src/components/crm/styles/BusinessCardOcr.module.scss +197 -4
- package/src/index.js +4 -0
|
@@ -0,0 +1,2888 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { toast } from 'react-toastify';
|
|
3
|
+
import parse from 'html-react-parser';
|
|
4
|
+
import moment from 'moment';
|
|
5
|
+
import numeral from 'numeral';
|
|
6
|
+
import { Calendar, X, Mail, FileText, Users } from 'lucide-react';
|
|
7
|
+
import { Tooltip } from 'react-tooltip';
|
|
8
|
+
|
|
9
|
+
import CustomFetch from '../Fetch';
|
|
10
|
+
import MultiSelect from '../MultiSelect';
|
|
11
|
+
import { showDatePickerDialog } from './DatePickerDialog';
|
|
12
|
+
import { showContactSelectorModal } from './ContactSelectorModal';
|
|
13
|
+
import { showDateRangeSelectorModal } from './DateRangeSelectorModal';
|
|
14
|
+
import { showAlternativeActionModal } from './AlternativeActionModal';
|
|
15
|
+
import { showReasonCollectorModal } from './ReasonCollectorModal';
|
|
16
|
+
import styles from './styles/GenericIndex.module.scss';
|
|
17
|
+
|
|
18
|
+
// ContactTooltip component for enhanced contact display
|
|
19
|
+
const ContactTooltip = ({ contacts, children }) => {
|
|
20
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
21
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
22
|
+
const tooltipRef = useRef(null);
|
|
23
|
+
const triggerRef = useRef(null);
|
|
24
|
+
|
|
25
|
+
const handleMouseEnter = (e) => {
|
|
26
|
+
if (!contacts || contacts.length === 0) return;
|
|
27
|
+
|
|
28
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
29
|
+
setPosition({
|
|
30
|
+
x: rect.left + rect.width / 2,
|
|
31
|
+
y: rect.top - 10,
|
|
32
|
+
});
|
|
33
|
+
setIsVisible(true);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleMouseLeave = () => {
|
|
37
|
+
setIsVisible(false);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const formatContact = (contact) => {
|
|
41
|
+
const name = contact.name || 'Unknown Contact';
|
|
42
|
+
const email = contact.email || '';
|
|
43
|
+
const type =
|
|
44
|
+
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>
|
|
74
|
+
{contacts.length} Contact
|
|
75
|
+
{contacts.length !== 1 ? 's' : ''}
|
|
76
|
+
</strong>
|
|
77
|
+
</div>
|
|
78
|
+
<div className={styles.contactTooltipBody}>
|
|
79
|
+
{contacts.map((contact, index) => {
|
|
80
|
+
const { name, email, type } =
|
|
81
|
+
formatContact(contact);
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
key={index}
|
|
85
|
+
className={styles.contactTooltipItem}
|
|
86
|
+
>
|
|
87
|
+
<div className={styles.contactName}>
|
|
88
|
+
{name}
|
|
89
|
+
</div>
|
|
90
|
+
{email && (
|
|
91
|
+
<div className={styles.contactEmail}>
|
|
92
|
+
{email}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
<div className={styles.contactType}>
|
|
96
|
+
{type}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</div>
|
|
102
|
+
<div className={styles.contactTooltipArrow}></div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const GenericGrid = ({
|
|
110
|
+
config,
|
|
111
|
+
userProfile,
|
|
112
|
+
data,
|
|
113
|
+
onDataChange,
|
|
114
|
+
onRowClick,
|
|
115
|
+
}) => {
|
|
116
|
+
const [gridData, setGridData] = useState([]);
|
|
117
|
+
const [sortedData, setSortedData] = useState([]);
|
|
118
|
+
const [, setFilteredData] = useState([]);
|
|
119
|
+
const [filterValues, setFilterValues] = useState({});
|
|
120
|
+
const [settingsFilterValues, setSettingsFilterValues] = useState({});
|
|
121
|
+
const [filterOptions, setFilterOptions] = useState({});
|
|
122
|
+
const [loading, setLoading] = useState(false);
|
|
123
|
+
const [filtersCollapsed, setFiltersCollapsed] = useState(false);
|
|
124
|
+
const [sortConfig, setSortConfig] = useState({
|
|
125
|
+
key: null,
|
|
126
|
+
direction: 'asc',
|
|
127
|
+
});
|
|
128
|
+
const prevFetchParamsRef = useRef(null);
|
|
129
|
+
|
|
130
|
+
// Extract data and settings from config using useMemo to prevent unnecessary re-renders
|
|
131
|
+
const {
|
|
132
|
+
settings,
|
|
133
|
+
columns = [],
|
|
134
|
+
data: configData = [],
|
|
135
|
+
} = useMemo(
|
|
136
|
+
() => ({
|
|
137
|
+
settings: config.settings || {},
|
|
138
|
+
columns: config.columns || [],
|
|
139
|
+
data: config.data || data || [],
|
|
140
|
+
}),
|
|
141
|
+
[config.settings, config.columns, config.data, data]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Create table headers from columns
|
|
145
|
+
const headers = useMemo(
|
|
146
|
+
() => columns.map((col) => col.header || col.name || ''),
|
|
147
|
+
[columns]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Format cell content based on column type or value type
|
|
151
|
+
const formatCellContent = (value, column, row = null) => {
|
|
152
|
+
// Check for quarter columns with status FIRST (before null checks)
|
|
153
|
+
if (
|
|
154
|
+
column.key &&
|
|
155
|
+
(column.key.startsWith('quarter_') ||
|
|
156
|
+
column.key === 'anniversary') &&
|
|
157
|
+
column.onClick?.type === 'date_with_alternatives' &&
|
|
158
|
+
row
|
|
159
|
+
) {
|
|
160
|
+
return formatQuarterWithStatus(value, column, row);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (value === null || value === undefined) return '';
|
|
164
|
+
if (value === 0) return '0'; // Explicitly handle zero values
|
|
165
|
+
if (value === '') return '';
|
|
166
|
+
|
|
167
|
+
// Check if this is a date field with contacts (survey workflow)
|
|
168
|
+
if (
|
|
169
|
+
column.type === 'date' &&
|
|
170
|
+
column.onClick?.type === 'date_with_contacts' &&
|
|
171
|
+
row
|
|
172
|
+
) {
|
|
173
|
+
return formatDateWithContacts(value, column, row);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if this is a date range field (agreement period)
|
|
177
|
+
if (column.type === 'daterange' && row) {
|
|
178
|
+
return formatDateRange(value, column, row);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check if this is a status field with reasons
|
|
182
|
+
if (column.type === 'status' && row) {
|
|
183
|
+
return formatStatusWithReason(value, column, row);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Try to infer the type if not provided
|
|
187
|
+
const valueType = column.type || typeof value;
|
|
188
|
+
|
|
189
|
+
switch (valueType) {
|
|
190
|
+
case 'date':
|
|
191
|
+
return moment(value).isValid()
|
|
192
|
+
? moment(value).format(column.format || 'DD/MM/YYYY')
|
|
193
|
+
: value;
|
|
194
|
+
case 'datetime':
|
|
195
|
+
return moment(value).isValid()
|
|
196
|
+
? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
|
|
197
|
+
: value;
|
|
198
|
+
case 'currency':
|
|
199
|
+
return numeral(value).format(column.format || '$0,0.00');
|
|
200
|
+
case 'number':
|
|
201
|
+
// Check if it's actually a number before formatting
|
|
202
|
+
return !isNaN(parseFloat(value)) && isFinite(value)
|
|
203
|
+
? numeral(value).format(column.format || '0,0')
|
|
204
|
+
: value;
|
|
205
|
+
case 'boolean':
|
|
206
|
+
if (
|
|
207
|
+
value === 1 ||
|
|
208
|
+
value === true ||
|
|
209
|
+
value === 'true' ||
|
|
210
|
+
value === 'yes' ||
|
|
211
|
+
value === 'Yes'
|
|
212
|
+
) {
|
|
213
|
+
return 'Yes';
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
value === 0 ||
|
|
217
|
+
value === false ||
|
|
218
|
+
value === 'false' ||
|
|
219
|
+
value === 'no' ||
|
|
220
|
+
value === 'No'
|
|
221
|
+
) {
|
|
222
|
+
return 'No';
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
case 'year':
|
|
226
|
+
// Special handling for year values
|
|
227
|
+
return value.toString();
|
|
228
|
+
case 'quarter':
|
|
229
|
+
// Special handling for quarter values (Q1, Q2, etc.)
|
|
230
|
+
if (value && value.toString().startsWith('Q')) {
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
case 'html':
|
|
235
|
+
// Explicitly parse HTML content
|
|
236
|
+
return parse(String(value));
|
|
237
|
+
case 'richtext':
|
|
238
|
+
// Parse rich text content
|
|
239
|
+
return parse(String(value));
|
|
240
|
+
default:
|
|
241
|
+
// For text values, check if it contains HTML tags
|
|
242
|
+
if (
|
|
243
|
+
typeof value === 'string' &&
|
|
244
|
+
(value.includes('<br') ||
|
|
245
|
+
value.includes('<p>') ||
|
|
246
|
+
value.includes('<div') ||
|
|
247
|
+
value.includes('<span'))
|
|
248
|
+
) {
|
|
249
|
+
// Ensure <br /> tags are properly handled
|
|
250
|
+
const processedValue = String(value)
|
|
251
|
+
.replace(/<br \/>/g, '<br>')
|
|
252
|
+
.replace(/<br\/>/g, '<br>');
|
|
253
|
+
return parse(processedValue);
|
|
254
|
+
}
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Format date field with associated contacts
|
|
260
|
+
const formatDateWithContacts = (dateValue, column, row) => {
|
|
261
|
+
const contactsField = column.key + '_contacts';
|
|
262
|
+
const contacts = row[contactsField];
|
|
263
|
+
|
|
264
|
+
// Format the date
|
|
265
|
+
const formattedDate = moment(dateValue).isValid()
|
|
266
|
+
? moment(dateValue).format(column.format || 'DD/MM/YYYY')
|
|
267
|
+
: dateValue;
|
|
268
|
+
|
|
269
|
+
// If no contacts, just return the date
|
|
270
|
+
if (!contacts || !Array.isArray(contacts) || contacts.length === 0) {
|
|
271
|
+
return formattedDate;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Create JSX element to display date + contacts with enhanced tooltip
|
|
275
|
+
return (
|
|
276
|
+
<div className={styles.dateWithContacts}>
|
|
277
|
+
<div className={styles.dateValue}>{formattedDate}</div>
|
|
278
|
+
<ContactTooltip contacts={contacts}>
|
|
279
|
+
<div className={styles.contactsSummary}>
|
|
280
|
+
📧 {contacts.length} contact
|
|
281
|
+
{contacts.length !== 1 ? 's' : ''}
|
|
282
|
+
</div>
|
|
283
|
+
</ContactTooltip>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Format date range field (start and end dates)
|
|
289
|
+
const formatDateRange = (_, column, row) => {
|
|
290
|
+
// For agreement period, use the fetched agreement dates
|
|
291
|
+
const startField =
|
|
292
|
+
column.key === 'agreement_period'
|
|
293
|
+
? 'agreement_start_date'
|
|
294
|
+
: column.onClick?.startField || 'start_date';
|
|
295
|
+
const endField =
|
|
296
|
+
column.key === 'agreement_period'
|
|
297
|
+
? 'agreement_end_date'
|
|
298
|
+
: column.onClick?.endField || 'end_date';
|
|
299
|
+
const contactsField = column.key + '_contacts';
|
|
300
|
+
|
|
301
|
+
const startDate = row[startField];
|
|
302
|
+
const endDate = row[endField];
|
|
303
|
+
const contacts = row[contactsField];
|
|
304
|
+
|
|
305
|
+
// Format dates
|
|
306
|
+
const formattedStart =
|
|
307
|
+
startDate && moment(startDate).isValid()
|
|
308
|
+
? moment(startDate).format(column.format || 'DD/MM/YYYY')
|
|
309
|
+
: '';
|
|
310
|
+
const formattedEnd =
|
|
311
|
+
endDate && moment(endDate).isValid()
|
|
312
|
+
? moment(endDate).format(column.format || 'DD/MM/YYYY')
|
|
313
|
+
: '';
|
|
314
|
+
|
|
315
|
+
let dateDisplay = '';
|
|
316
|
+
if (formattedStart && formattedEnd) {
|
|
317
|
+
dateDisplay = `${formattedStart} - ${formattedEnd}`;
|
|
318
|
+
} else if (formattedStart) {
|
|
319
|
+
dateDisplay = `From ${formattedStart}`;
|
|
320
|
+
} else if (formattedEnd) {
|
|
321
|
+
dateDisplay = `Until ${formattedEnd}`;
|
|
322
|
+
} else {
|
|
323
|
+
dateDisplay = 'Not set';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// If no contacts, just return the date range
|
|
327
|
+
if (!contacts || !Array.isArray(contacts) || contacts.length === 0) {
|
|
328
|
+
return dateDisplay;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Create JSX element to display date range + contacts
|
|
332
|
+
return (
|
|
333
|
+
<div className={styles.dateWithContacts}>
|
|
334
|
+
<div className={styles.dateValue}>{dateDisplay}</div>
|
|
335
|
+
<ContactTooltip contacts={contacts}>
|
|
336
|
+
<div className={styles.contactsSummary}>
|
|
337
|
+
📧 {contacts.length} contact
|
|
338
|
+
{contacts.length !== 1 ? 's' : ''}
|
|
339
|
+
</div>
|
|
340
|
+
</ContactTooltip>
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Format status field with reason
|
|
346
|
+
const formatStatusWithReason = (statusValue, column, row) => {
|
|
347
|
+
const reasonField = column.key + '_reason';
|
|
348
|
+
const reason = row[reasonField];
|
|
349
|
+
|
|
350
|
+
// Status display mapping
|
|
351
|
+
const statusMap = {
|
|
352
|
+
sent: { label: 'Sent', icon: '✅', color: '#10b981' },
|
|
353
|
+
not_sent: { label: 'Not Sent', icon: '❌', color: '#ef4444' },
|
|
354
|
+
pending: { label: 'Pending', icon: '⏳', color: '#f59e0b' },
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const status = statusMap[statusValue] || {
|
|
358
|
+
label: statusValue || 'Unknown',
|
|
359
|
+
icon: '❓',
|
|
360
|
+
color: '#6b7280',
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div className={styles.statusWithReason}>
|
|
365
|
+
<div
|
|
366
|
+
className={styles.statusValue}
|
|
367
|
+
style={{ color: status.color }}
|
|
368
|
+
>
|
|
369
|
+
{status.icon} {status.label}
|
|
370
|
+
</div>
|
|
371
|
+
{reason && (
|
|
372
|
+
<div className={styles.statusReason} title={reason}>
|
|
373
|
+
📝{' '}
|
|
374
|
+
{reason.length > 30
|
|
375
|
+
? reason.substring(0, 30) + '...'
|
|
376
|
+
: reason}
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Helper function to check if a quarter cell should be clickable
|
|
384
|
+
const isQuarterCellClickable = (column, row) => {
|
|
385
|
+
// Check if this is a quarter column with alternatives
|
|
386
|
+
if (
|
|
387
|
+
!column.key ||
|
|
388
|
+
(!column.key.startsWith('quarter_') &&
|
|
389
|
+
column.key !== 'anniversary') ||
|
|
390
|
+
column.onClick?.type !== 'date_with_alternatives'
|
|
391
|
+
) {
|
|
392
|
+
return true; // Not a quarter column, use default logic
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const statusField = column.key + '_status';
|
|
396
|
+
const status = row[statusField];
|
|
397
|
+
|
|
398
|
+
// Not clickable if status is 'not_sent'
|
|
399
|
+
return status !== 'not_sent';
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Helper function to check if a quarter cell has content (for display purposes)
|
|
403
|
+
const quarterCellHasContent = (column, row) => {
|
|
404
|
+
if (
|
|
405
|
+
!column.key ||
|
|
406
|
+
(!column.key.startsWith('quarter_') &&
|
|
407
|
+
column.key !== 'anniversary') ||
|
|
408
|
+
column.onClick?.type !== 'date_with_alternatives'
|
|
409
|
+
) {
|
|
410
|
+
return !!row[column.key]; // Not a quarter column, use default logic
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const statusField = column.key + '_status';
|
|
414
|
+
const status = row[statusField];
|
|
415
|
+
|
|
416
|
+
// Has content if there's a date value OR if status is not_sent
|
|
417
|
+
return !!row[column.key] || status === 'not_sent';
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Format quarter columns with status, contacts, and reasons with react-tooltip
|
|
421
|
+
const formatQuarterWithStatus = (value, column, row) => {
|
|
422
|
+
const statusField = column.key + '_status';
|
|
423
|
+
const contactsField = column.key + '_contacts';
|
|
424
|
+
const reasonField = column.key + '_reason';
|
|
425
|
+
|
|
426
|
+
const status = row[statusField] || 'pending';
|
|
427
|
+
const contacts = row[contactsField] || [];
|
|
428
|
+
const reason = row[reasonField];
|
|
429
|
+
|
|
430
|
+
console.log('Quarter formatting called for:', column.key, {
|
|
431
|
+
status,
|
|
432
|
+
hasValue: !!value,
|
|
433
|
+
hasReason: !!reason,
|
|
434
|
+
contactCount: contacts.length,
|
|
435
|
+
'will show not_sent': status === 'not_sent',
|
|
436
|
+
'will show sent': status === 'sent' && value,
|
|
437
|
+
'will show legacy':
|
|
438
|
+
value && status !== 'sent' && status !== 'not_sent',
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Generate unique ID for tooltip
|
|
442
|
+
const tooltipId = `quarter-tooltip-${column.key}-${row.id}`;
|
|
443
|
+
|
|
444
|
+
// If status is 'sent' and we have a date
|
|
445
|
+
if (status === 'sent' && value) {
|
|
446
|
+
const formattedDate = moment(value).isValid()
|
|
447
|
+
? moment(value).format('DD/MM/YYYY')
|
|
448
|
+
: value;
|
|
449
|
+
|
|
450
|
+
const contactCount = Array.isArray(contacts) ? contacts.length : 0;
|
|
451
|
+
|
|
452
|
+
// Build rich tooltip content
|
|
453
|
+
const tooltipContent = (
|
|
454
|
+
<div style={{ padding: '8px', maxWidth: '300px' }}>
|
|
455
|
+
<div
|
|
456
|
+
style={{
|
|
457
|
+
display: 'flex',
|
|
458
|
+
alignItems: 'center',
|
|
459
|
+
marginBottom: '8px',
|
|
460
|
+
color: '#059669',
|
|
461
|
+
fontWeight: 'bold',
|
|
462
|
+
}}
|
|
463
|
+
>
|
|
464
|
+
<Calendar size={16} style={{ marginRight: '6px' }} />
|
|
465
|
+
Sent on {formattedDate}
|
|
466
|
+
</div>
|
|
467
|
+
{contactCount > 0 && (
|
|
468
|
+
<div>
|
|
469
|
+
<div
|
|
470
|
+
style={{
|
|
471
|
+
display: 'flex',
|
|
472
|
+
alignItems: 'center',
|
|
473
|
+
marginBottom: '6px',
|
|
474
|
+
color: '#1d4ed8',
|
|
475
|
+
fontWeight: '500',
|
|
476
|
+
}}
|
|
477
|
+
>
|
|
478
|
+
<Users
|
|
479
|
+
size={14}
|
|
480
|
+
style={{ marginRight: '6px' }}
|
|
481
|
+
/>
|
|
482
|
+
{contactCount} Contact
|
|
483
|
+
{contactCount !== 1 ? 's' : ''}
|
|
484
|
+
</div>
|
|
485
|
+
<div style={{ fontSize: '12px', color: '#6b7280' }}>
|
|
486
|
+
{contacts.map((contact, index) => (
|
|
487
|
+
<div
|
|
488
|
+
key={index}
|
|
489
|
+
style={{
|
|
490
|
+
display: 'flex',
|
|
491
|
+
alignItems: 'center',
|
|
492
|
+
marginBottom: '4px',
|
|
493
|
+
}}
|
|
494
|
+
>
|
|
495
|
+
<Mail
|
|
496
|
+
size={12}
|
|
497
|
+
style={{
|
|
498
|
+
marginRight: '6px',
|
|
499
|
+
color: '#9ca3af',
|
|
500
|
+
}}
|
|
501
|
+
/>
|
|
502
|
+
<span>
|
|
503
|
+
{contact.name || 'Unknown'}
|
|
504
|
+
{contact.email && (
|
|
505
|
+
<span
|
|
506
|
+
style={{ color: '#9ca3af' }}
|
|
507
|
+
>
|
|
508
|
+
{' '}
|
|
509
|
+
({contact.email})
|
|
510
|
+
</span>
|
|
511
|
+
)}
|
|
512
|
+
{contact.notes && (
|
|
513
|
+
<div
|
|
514
|
+
style={{
|
|
515
|
+
fontSize: '11px',
|
|
516
|
+
fontStyle: 'italic',
|
|
517
|
+
marginTop: '2px',
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
{contact.notes}
|
|
521
|
+
</div>
|
|
522
|
+
)}
|
|
523
|
+
</span>
|
|
524
|
+
</div>
|
|
525
|
+
))}
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
)}
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
<>
|
|
534
|
+
<span
|
|
535
|
+
data-tooltip-id={tooltipId}
|
|
536
|
+
style={{
|
|
537
|
+
color: '#059669',
|
|
538
|
+
fontWeight: '500',
|
|
539
|
+
cursor: 'help',
|
|
540
|
+
display: 'inline-flex',
|
|
541
|
+
alignItems: 'center',
|
|
542
|
+
gap: '4px',
|
|
543
|
+
}}
|
|
544
|
+
className="quarter-cell-with-tooltip"
|
|
545
|
+
>
|
|
546
|
+
<Calendar size={14} />
|
|
547
|
+
{formattedDate}
|
|
548
|
+
{contactCount > 0 && (
|
|
549
|
+
<span
|
|
550
|
+
style={{ color: '#1d4ed8', marginLeft: '4px' }}
|
|
551
|
+
>
|
|
552
|
+
({contactCount})
|
|
553
|
+
</span>
|
|
554
|
+
)}
|
|
555
|
+
</span>
|
|
556
|
+
<Tooltip
|
|
557
|
+
id={tooltipId}
|
|
558
|
+
place="top"
|
|
559
|
+
style={{
|
|
560
|
+
backgroundColor: '#fff',
|
|
561
|
+
color: '#000',
|
|
562
|
+
border: '1px solid #e5e7eb',
|
|
563
|
+
borderRadius: '8px',
|
|
564
|
+
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
|
565
|
+
}}
|
|
566
|
+
>
|
|
567
|
+
{tooltipContent}
|
|
568
|
+
</Tooltip>
|
|
569
|
+
</>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// If status is 'not_sent' (with or without reason)
|
|
574
|
+
if (status === 'not_sent') {
|
|
575
|
+
const tooltipContent = (
|
|
576
|
+
<div style={{ padding: '8px', maxWidth: '250px' }}>
|
|
577
|
+
<div
|
|
578
|
+
style={{
|
|
579
|
+
display: 'flex',
|
|
580
|
+
alignItems: 'center',
|
|
581
|
+
marginBottom: '8px',
|
|
582
|
+
color: '#dc2626',
|
|
583
|
+
fontWeight: 'bold',
|
|
584
|
+
}}
|
|
585
|
+
>
|
|
586
|
+
<X size={16} style={{ marginRight: '6px' }} />
|
|
587
|
+
Not Sent
|
|
588
|
+
</div>
|
|
589
|
+
{reason && (
|
|
590
|
+
<div
|
|
591
|
+
style={{
|
|
592
|
+
display: 'flex',
|
|
593
|
+
alignItems: 'flex-start',
|
|
594
|
+
color: '#6b7280',
|
|
595
|
+
}}
|
|
596
|
+
>
|
|
597
|
+
<FileText
|
|
598
|
+
size={14}
|
|
599
|
+
style={{
|
|
600
|
+
marginRight: '6px',
|
|
601
|
+
marginTop: '2px',
|
|
602
|
+
flexShrink: 0,
|
|
603
|
+
}}
|
|
604
|
+
/>
|
|
605
|
+
<span style={{ fontSize: '13px' }}>{reason}</span>
|
|
606
|
+
</div>
|
|
607
|
+
)}
|
|
608
|
+
</div>
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<>
|
|
613
|
+
<span
|
|
614
|
+
data-tooltip-id={tooltipId}
|
|
615
|
+
style={{
|
|
616
|
+
color: '#dc2626',
|
|
617
|
+
fontWeight: '500',
|
|
618
|
+
cursor: 'help',
|
|
619
|
+
display: 'inline-flex',
|
|
620
|
+
alignItems: 'center',
|
|
621
|
+
gap: '4px',
|
|
622
|
+
}}
|
|
623
|
+
className="quarter-cell-with-tooltip"
|
|
624
|
+
>
|
|
625
|
+
<X size={14} />
|
|
626
|
+
Not Sent
|
|
627
|
+
</span>
|
|
628
|
+
<Tooltip
|
|
629
|
+
id={tooltipId}
|
|
630
|
+
place="top"
|
|
631
|
+
style={{
|
|
632
|
+
backgroundColor: '#fff',
|
|
633
|
+
color: '#000',
|
|
634
|
+
border: '1px solid #e5e7eb',
|
|
635
|
+
borderRadius: '8px',
|
|
636
|
+
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
|
637
|
+
}}
|
|
638
|
+
>
|
|
639
|
+
{tooltipContent}
|
|
640
|
+
</Tooltip>
|
|
641
|
+
</>
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// If we have a date but no clear status (legacy data)
|
|
646
|
+
if (value) {
|
|
647
|
+
const formattedDate = moment(value).isValid()
|
|
648
|
+
? moment(value).format('DD/MM/YYYY')
|
|
649
|
+
: value;
|
|
650
|
+
|
|
651
|
+
const contactCount = Array.isArray(contacts) ? contacts.length : 0;
|
|
652
|
+
|
|
653
|
+
const tooltipContent = (
|
|
654
|
+
<div style={{ padding: '8px', maxWidth: '300px' }}>
|
|
655
|
+
<div
|
|
656
|
+
style={{
|
|
657
|
+
display: 'flex',
|
|
658
|
+
alignItems: 'center',
|
|
659
|
+
marginBottom: '8px',
|
|
660
|
+
color: '#374151',
|
|
661
|
+
fontWeight: 'bold',
|
|
662
|
+
}}
|
|
663
|
+
>
|
|
664
|
+
<Calendar size={16} style={{ marginRight: '6px' }} />
|
|
665
|
+
{formattedDate}
|
|
666
|
+
</div>
|
|
667
|
+
{contactCount > 0 && (
|
|
668
|
+
<div>
|
|
669
|
+
<div
|
|
670
|
+
style={{
|
|
671
|
+
display: 'flex',
|
|
672
|
+
alignItems: 'center',
|
|
673
|
+
marginBottom: '6px',
|
|
674
|
+
color: '#1d4ed8',
|
|
675
|
+
fontWeight: '500',
|
|
676
|
+
}}
|
|
677
|
+
>
|
|
678
|
+
<Users
|
|
679
|
+
size={14}
|
|
680
|
+
style={{ marginRight: '6px' }}
|
|
681
|
+
/>
|
|
682
|
+
{contactCount} Contact
|
|
683
|
+
{contactCount !== 1 ? 's' : ''}
|
|
684
|
+
</div>
|
|
685
|
+
<div style={{ fontSize: '12px', color: '#6b7280' }}>
|
|
686
|
+
{contacts.map((contact, index) => (
|
|
687
|
+
<div
|
|
688
|
+
key={index}
|
|
689
|
+
style={{
|
|
690
|
+
display: 'flex',
|
|
691
|
+
alignItems: 'center',
|
|
692
|
+
marginBottom: '4px',
|
|
693
|
+
}}
|
|
694
|
+
>
|
|
695
|
+
<Mail
|
|
696
|
+
size={12}
|
|
697
|
+
style={{
|
|
698
|
+
marginRight: '6px',
|
|
699
|
+
color: '#9ca3af',
|
|
700
|
+
}}
|
|
701
|
+
/>
|
|
702
|
+
<span>
|
|
703
|
+
{contact.name || 'Unknown'}
|
|
704
|
+
{contact.email && (
|
|
705
|
+
<span
|
|
706
|
+
style={{ color: '#9ca3af' }}
|
|
707
|
+
>
|
|
708
|
+
{' '}
|
|
709
|
+
({contact.email})
|
|
710
|
+
</span>
|
|
711
|
+
)}
|
|
712
|
+
{contact.notes && (
|
|
713
|
+
<div
|
|
714
|
+
style={{
|
|
715
|
+
fontSize: '11px',
|
|
716
|
+
fontStyle: 'italic',
|
|
717
|
+
marginTop: '2px',
|
|
718
|
+
}}
|
|
719
|
+
>
|
|
720
|
+
{contact.notes}
|
|
721
|
+
</div>
|
|
722
|
+
)}
|
|
723
|
+
</span>
|
|
724
|
+
</div>
|
|
725
|
+
))}
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
)}
|
|
729
|
+
</div>
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
<>
|
|
734
|
+
<span
|
|
735
|
+
data-tooltip-id={tooltipId}
|
|
736
|
+
style={{
|
|
737
|
+
color: '#374151',
|
|
738
|
+
cursor: 'help',
|
|
739
|
+
display: 'inline-flex',
|
|
740
|
+
alignItems: 'center',
|
|
741
|
+
gap: '4px',
|
|
742
|
+
}}
|
|
743
|
+
className="quarter-cell-with-tooltip"
|
|
744
|
+
>
|
|
745
|
+
<Calendar size={14} />
|
|
746
|
+
{formattedDate}
|
|
747
|
+
{contactCount > 0 && (
|
|
748
|
+
<span
|
|
749
|
+
style={{ color: '#1d4ed8', marginLeft: '4px' }}
|
|
750
|
+
>
|
|
751
|
+
({contactCount})
|
|
752
|
+
</span>
|
|
753
|
+
)}
|
|
754
|
+
</span>
|
|
755
|
+
<Tooltip
|
|
756
|
+
id={tooltipId}
|
|
757
|
+
place="top"
|
|
758
|
+
style={{
|
|
759
|
+
backgroundColor: '#fff',
|
|
760
|
+
color: '#000',
|
|
761
|
+
border: '1px solid #e5e7eb',
|
|
762
|
+
borderRadius: '8px',
|
|
763
|
+
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
|
764
|
+
}}
|
|
765
|
+
>
|
|
766
|
+
{tooltipContent}
|
|
767
|
+
</Tooltip>
|
|
768
|
+
</>
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Default case - return empty or pending
|
|
773
|
+
return '';
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// State for headers from API response
|
|
777
|
+
const [gridHeaders, setGridHeaders] = useState([]);
|
|
778
|
+
|
|
779
|
+
// Function to sort data
|
|
780
|
+
const sortData = useCallback(
|
|
781
|
+
(data, key, direction) => {
|
|
782
|
+
if (!key) return [...data];
|
|
783
|
+
|
|
784
|
+
return [...data].sort((a, b) => {
|
|
785
|
+
// Find the header config for this key to determine type
|
|
786
|
+
const headerConfig = gridHeaders.find((h) => h.key === key);
|
|
787
|
+
const type = headerConfig?.type || 'text';
|
|
788
|
+
|
|
789
|
+
// Get values, handling null/undefined
|
|
790
|
+
let aValue =
|
|
791
|
+
a[key] === null || a[key] === undefined ? '' : a[key];
|
|
792
|
+
let bValue =
|
|
793
|
+
b[key] === null || b[key] === undefined ? '' : b[key];
|
|
794
|
+
|
|
795
|
+
// Sort based on type
|
|
796
|
+
switch (type) {
|
|
797
|
+
case 'number':
|
|
798
|
+
case 'year':
|
|
799
|
+
aValue = parseFloat(aValue) || 0;
|
|
800
|
+
bValue = parseFloat(bValue) || 0;
|
|
801
|
+
break;
|
|
802
|
+
case 'date':
|
|
803
|
+
// Convert to timestamp for comparison
|
|
804
|
+
aValue = aValue ? new Date(aValue).getTime() : 0;
|
|
805
|
+
bValue = bValue ? new Date(bValue).getTime() : 0;
|
|
806
|
+
break;
|
|
807
|
+
default:
|
|
808
|
+
// For text, convert to lowercase strings
|
|
809
|
+
aValue = String(aValue).toLowerCase();
|
|
810
|
+
bValue = String(bValue).toLowerCase();
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Compare values
|
|
814
|
+
if (aValue < bValue) {
|
|
815
|
+
return direction === 'asc' ? -1 : 1;
|
|
816
|
+
}
|
|
817
|
+
if (aValue > bValue) {
|
|
818
|
+
return direction === 'asc' ? 1 : -1;
|
|
819
|
+
}
|
|
820
|
+
return 0;
|
|
821
|
+
});
|
|
822
|
+
},
|
|
823
|
+
[gridHeaders]
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
// Handle sort request
|
|
827
|
+
const requestSort = useCallback((key) => {
|
|
828
|
+
// If clicking the same column, toggle direction
|
|
829
|
+
// Otherwise, start with ascending sort for the new column
|
|
830
|
+
setSortConfig((prevConfig) => {
|
|
831
|
+
const newDirection =
|
|
832
|
+
prevConfig.key === key && prevConfig.direction === 'asc'
|
|
833
|
+
? 'desc'
|
|
834
|
+
: 'asc';
|
|
835
|
+
|
|
836
|
+
return { key, direction: newDirection };
|
|
837
|
+
});
|
|
838
|
+
}, []);
|
|
839
|
+
|
|
840
|
+
// Function to filter data based on filter values
|
|
841
|
+
const filterData = useCallback(
|
|
842
|
+
(data) => {
|
|
843
|
+
// If no filter values are set, return the original data
|
|
844
|
+
if (Object.keys(filterValues).length === 0) {
|
|
845
|
+
return data;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Filter the data based on the filter values
|
|
849
|
+
return data.filter((row) => {
|
|
850
|
+
// Check each filter
|
|
851
|
+
return Object.entries(filterValues).every(([key, value]) => {
|
|
852
|
+
// Skip empty filter values
|
|
853
|
+
if (!value) return true;
|
|
854
|
+
|
|
855
|
+
// Get the row value
|
|
856
|
+
const rowValue = row[key];
|
|
857
|
+
if (rowValue === null || rowValue === undefined)
|
|
858
|
+
return false;
|
|
859
|
+
|
|
860
|
+
// Find the header config for this key
|
|
861
|
+
const headerConfig = gridHeaders.find((h) => h.key === key);
|
|
862
|
+
if (!headerConfig || !headerConfig.filter) return true;
|
|
863
|
+
|
|
864
|
+
const { type, operator } = headerConfig.filter;
|
|
865
|
+
|
|
866
|
+
// Apply filter based on type and operator
|
|
867
|
+
switch (type) {
|
|
868
|
+
case 'string':
|
|
869
|
+
const rowStr = String(rowValue).toLowerCase();
|
|
870
|
+
const filterStr = String(value).toLowerCase();
|
|
871
|
+
|
|
872
|
+
switch (operator) {
|
|
873
|
+
case 'equals':
|
|
874
|
+
return rowStr === filterStr;
|
|
875
|
+
case 'contains':
|
|
876
|
+
return rowStr.includes(filterStr);
|
|
877
|
+
case 'startsWith':
|
|
878
|
+
return rowStr.startsWith(filterStr);
|
|
879
|
+
case 'endsWith':
|
|
880
|
+
return rowStr.endsWith(filterStr);
|
|
881
|
+
default:
|
|
882
|
+
return rowStr.includes(filterStr);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
case 'number':
|
|
886
|
+
const rowNum = parseFloat(rowValue);
|
|
887
|
+
const filterNum = parseFloat(value);
|
|
888
|
+
|
|
889
|
+
if (isNaN(rowNum) || isNaN(filterNum)) return false;
|
|
890
|
+
|
|
891
|
+
switch (operator) {
|
|
892
|
+
case 'equals':
|
|
893
|
+
return rowNum === filterNum;
|
|
894
|
+
case 'greaterThan':
|
|
895
|
+
return rowNum > filterNum;
|
|
896
|
+
case 'lessThan':
|
|
897
|
+
return rowNum < filterNum;
|
|
898
|
+
case 'greaterThanOrEqual':
|
|
899
|
+
return rowNum >= filterNum;
|
|
900
|
+
case 'lessThanOrEqual':
|
|
901
|
+
return rowNum <= filterNum;
|
|
902
|
+
default:
|
|
903
|
+
return rowNum === filterNum;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
case 'date':
|
|
907
|
+
const rowDate = new Date(rowValue);
|
|
908
|
+
const filterDate = new Date(value);
|
|
909
|
+
|
|
910
|
+
if (
|
|
911
|
+
isNaN(rowDate.getTime()) ||
|
|
912
|
+
isNaN(filterDate.getTime())
|
|
913
|
+
)
|
|
914
|
+
return false;
|
|
915
|
+
|
|
916
|
+
switch (operator) {
|
|
917
|
+
case 'equals':
|
|
918
|
+
return (
|
|
919
|
+
rowDate.toDateString() ===
|
|
920
|
+
filterDate.toDateString()
|
|
921
|
+
);
|
|
922
|
+
case 'after':
|
|
923
|
+
return rowDate > filterDate;
|
|
924
|
+
case 'before':
|
|
925
|
+
return rowDate < filterDate;
|
|
926
|
+
default:
|
|
927
|
+
return (
|
|
928
|
+
rowDate.toDateString() ===
|
|
929
|
+
filterDate.toDateString()
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
default:
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
},
|
|
939
|
+
[filterValues, gridHeaders]
|
|
940
|
+
);
|
|
941
|
+
|
|
942
|
+
// Handle filter change
|
|
943
|
+
const handleFilterChange = useCallback((key, value) => {
|
|
944
|
+
setFilterValues((prev) => ({
|
|
945
|
+
...prev,
|
|
946
|
+
[key]: value,
|
|
947
|
+
}));
|
|
948
|
+
}, []);
|
|
949
|
+
|
|
950
|
+
// Effect to update filteredData and sortedData when gridData, filterValues, or sortConfig changes
|
|
951
|
+
useEffect(() => {
|
|
952
|
+
if (gridData.length > 0) {
|
|
953
|
+
// First apply filters
|
|
954
|
+
const filtered = filterData(gridData);
|
|
955
|
+
setFilteredData(filtered);
|
|
956
|
+
|
|
957
|
+
// Then sort the filtered data
|
|
958
|
+
const sorted = sortData(
|
|
959
|
+
filtered,
|
|
960
|
+
sortConfig.key,
|
|
961
|
+
sortConfig.direction
|
|
962
|
+
);
|
|
963
|
+
setSortedData(sorted);
|
|
964
|
+
} else {
|
|
965
|
+
setFilteredData([]);
|
|
966
|
+
setSortedData([]);
|
|
967
|
+
}
|
|
968
|
+
}, [gridData, filterValues, sortConfig, sortData, filterData]);
|
|
969
|
+
|
|
970
|
+
// Load data from API if gridSetting.fetch is provided
|
|
971
|
+
useEffect(() => {
|
|
972
|
+
const fetchData = async () => {
|
|
973
|
+
// Check if we should use static data instead of fetching
|
|
974
|
+
if (settings?.useStaticData && settings?.staticData) {
|
|
975
|
+
const staticData = settings.staticData;
|
|
976
|
+
|
|
977
|
+
if (staticData.header && staticData.rows) {
|
|
978
|
+
// Set the headers from the static data
|
|
979
|
+
const headerEntries = Object.entries(staticData.header);
|
|
980
|
+
setGridHeaders(
|
|
981
|
+
headerEntries.map(([key, config]) => ({
|
|
982
|
+
key,
|
|
983
|
+
label: config.label || key,
|
|
984
|
+
sort: config.sort || key,
|
|
985
|
+
type: config.type || 'text',
|
|
986
|
+
filter: config.filter || null,
|
|
987
|
+
onClick: config.onClick || null,
|
|
988
|
+
}))
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
// Initialize filter values
|
|
992
|
+
const initialFilterValues = {};
|
|
993
|
+
headerEntries.forEach(([key, config]) => {
|
|
994
|
+
if (config.filter) {
|
|
995
|
+
initialFilterValues[key] = '';
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
setFilterValues(initialFilterValues);
|
|
999
|
+
|
|
1000
|
+
// Set the rows data
|
|
1001
|
+
const newData = staticData.rows || [];
|
|
1002
|
+
setGridData(newData);
|
|
1003
|
+
setSortedData(newData);
|
|
1004
|
+
|
|
1005
|
+
// Notify parent component of data change
|
|
1006
|
+
if (onDataChange) {
|
|
1007
|
+
onDataChange(newData);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Only fetch if we have a URL
|
|
1014
|
+
if (!settings?.fetch?.url) {
|
|
1015
|
+
if (configData && configData.length > 0) {
|
|
1016
|
+
// Use provided data if no fetch setting
|
|
1017
|
+
setGridData(configData);
|
|
1018
|
+
}
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const url = settings.fetch.url;
|
|
1023
|
+
const method = settings.fetch.method || 'POST';
|
|
1024
|
+
|
|
1025
|
+
// Combine base params with settings filter values
|
|
1026
|
+
const params = {
|
|
1027
|
+
...(settings.fetch.params || {}),
|
|
1028
|
+
...settingsFilterValues,
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
// Create a string representation of the fetch parameters for comparison
|
|
1032
|
+
const fetchParamsString = JSON.stringify({ url, method, params });
|
|
1033
|
+
|
|
1034
|
+
// Skip fetch if the parameters haven't changed
|
|
1035
|
+
if (prevFetchParamsRef.current === fetchParamsString) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Update the ref with current parameters
|
|
1040
|
+
prevFetchParamsRef.current = fetchParamsString;
|
|
1041
|
+
|
|
1042
|
+
setLoading(true);
|
|
1043
|
+
try {
|
|
1044
|
+
const response = await CustomFetch(url, method, params);
|
|
1045
|
+
|
|
1046
|
+
if (response.data.data) {
|
|
1047
|
+
// Check if the response has the expected structure with header and rows
|
|
1048
|
+
if (response.data.data.header && response.data.data.rows) {
|
|
1049
|
+
// Set the headers from the response with the new structure
|
|
1050
|
+
const headerEntries = Object.entries(
|
|
1051
|
+
response.data.data.header
|
|
1052
|
+
);
|
|
1053
|
+
setGridHeaders(
|
|
1054
|
+
headerEntries.map(([key, config]) => ({
|
|
1055
|
+
key,
|
|
1056
|
+
label: config.label || key,
|
|
1057
|
+
sort: config.sort || key,
|
|
1058
|
+
type: config.type || 'text',
|
|
1059
|
+
filter: config.filter || null,
|
|
1060
|
+
onClick: config.onClick || null,
|
|
1061
|
+
}))
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
// Initialize filter values
|
|
1065
|
+
const initialFilterValues = {};
|
|
1066
|
+
headerEntries.forEach(([key, config]) => {
|
|
1067
|
+
if (config.filter) {
|
|
1068
|
+
initialFilterValues[key] = '';
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
setFilterValues(initialFilterValues);
|
|
1072
|
+
|
|
1073
|
+
// Set the rows data
|
|
1074
|
+
const newData = response.data.data.rows || [];
|
|
1075
|
+
setGridData(newData);
|
|
1076
|
+
setSortedData(newData);
|
|
1077
|
+
|
|
1078
|
+
// Notify parent component of data change
|
|
1079
|
+
if (onDataChange) {
|
|
1080
|
+
onDataChange(newData);
|
|
1081
|
+
}
|
|
1082
|
+
} else {
|
|
1083
|
+
// Fallback to the previous behavior if the structure is different
|
|
1084
|
+
const newData = response.data.data.data || [];
|
|
1085
|
+
setGridData(newData);
|
|
1086
|
+
setSortedData(newData);
|
|
1087
|
+
|
|
1088
|
+
// Notify parent component of data change
|
|
1089
|
+
if (onDataChange) {
|
|
1090
|
+
onDataChange(newData);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
console.error('Error fetching grid data:', error);
|
|
1096
|
+
} finally {
|
|
1097
|
+
setLoading(false);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
fetchData();
|
|
1102
|
+
}, [
|
|
1103
|
+
// Only depend on the specific properties we need, not the entire objects
|
|
1104
|
+
settings?.fetch?.url,
|
|
1105
|
+
settings?.fetch?.method,
|
|
1106
|
+
// Use JSON.stringify for the params to detect deep changes
|
|
1107
|
+
JSON.stringify(settings?.fetch?.params),
|
|
1108
|
+
JSON.stringify(settingsFilterValues),
|
|
1109
|
+
// Only depend on data if we're not fetching
|
|
1110
|
+
!settings?.fetch?.url ? JSON.stringify(configData) : null,
|
|
1111
|
+
// Include static data dependencies
|
|
1112
|
+
settings?.useStaticData,
|
|
1113
|
+
JSON.stringify(settings?.staticData),
|
|
1114
|
+
onDataChange,
|
|
1115
|
+
]);
|
|
1116
|
+
|
|
1117
|
+
// Generate year options for dropdown
|
|
1118
|
+
const generateYearOptions = useCallback((startYear) => {
|
|
1119
|
+
const currentYear = new Date().getFullYear();
|
|
1120
|
+
const years = [];
|
|
1121
|
+
|
|
1122
|
+
// Start from the specified year and go up to current year
|
|
1123
|
+
for (let year = startYear; year <= currentYear + 1; year++) {
|
|
1124
|
+
years.push({ value: year.toString(), label: year.toString() });
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return years;
|
|
1128
|
+
}, []);
|
|
1129
|
+
|
|
1130
|
+
// Fetch filter options from URL
|
|
1131
|
+
const fetchFilterOptions = useCallback(async (filter) => {
|
|
1132
|
+
if (!filter.url) return;
|
|
1133
|
+
|
|
1134
|
+
try {
|
|
1135
|
+
const response = await CustomFetch(filter.url, 'POST', {});
|
|
1136
|
+
if (response.data && response.data.data) {
|
|
1137
|
+
setFilterOptions((prev) => ({
|
|
1138
|
+
...prev,
|
|
1139
|
+
[filter.id]: response.data.data,
|
|
1140
|
+
}));
|
|
1141
|
+
}
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
console.error(`Error fetching options for ${filter.id}:`, error);
|
|
1144
|
+
toast.error(`Failed to load ${filter.label} options`);
|
|
1145
|
+
}
|
|
1146
|
+
}, []);
|
|
1147
|
+
|
|
1148
|
+
// Initialize settings filters and fetch options
|
|
1149
|
+
useEffect(() => {
|
|
1150
|
+
if (settings?.filters && settings.filters.length > 0) {
|
|
1151
|
+
const initialValues = {};
|
|
1152
|
+
|
|
1153
|
+
settings.filters.forEach((filter) => {
|
|
1154
|
+
if (
|
|
1155
|
+
(filter.type === 'dropdown' ||
|
|
1156
|
+
filter.type === 'multiselect') &&
|
|
1157
|
+
(filter.id === 'year' || filter.startYear)
|
|
1158
|
+
) {
|
|
1159
|
+
// For year filters with 'now' value, set to current year
|
|
1160
|
+
if (filter.value === 'now') {
|
|
1161
|
+
const currentYear = new Date().getFullYear().toString();
|
|
1162
|
+
// For multiselect, check isMulti to determine if it should be array or single value
|
|
1163
|
+
if (filter.type === 'multiselect') {
|
|
1164
|
+
const value =
|
|
1165
|
+
filter.isMulti === false
|
|
1166
|
+
? currentYear
|
|
1167
|
+
: [currentYear];
|
|
1168
|
+
initialValues[filter.id] = value;
|
|
1169
|
+
} else {
|
|
1170
|
+
initialValues[filter.id] = currentYear;
|
|
1171
|
+
}
|
|
1172
|
+
} else {
|
|
1173
|
+
if (filter.type === 'multiselect') {
|
|
1174
|
+
initialValues[filter.id] =
|
|
1175
|
+
filter.isMulti === false
|
|
1176
|
+
? filter.value || ''
|
|
1177
|
+
: filter.value || [];
|
|
1178
|
+
} else {
|
|
1179
|
+
initialValues[filter.id] = filter.value || '';
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
} else if (filter.type === 'multiselect') {
|
|
1183
|
+
// For multiselect, handle 'now' value and initialize based on isMulti
|
|
1184
|
+
if (filter.value === 'now') {
|
|
1185
|
+
const currentYear = new Date().getFullYear().toString();
|
|
1186
|
+
initialValues[filter.id] =
|
|
1187
|
+
filter.isMulti === false
|
|
1188
|
+
? currentYear
|
|
1189
|
+
: [currentYear];
|
|
1190
|
+
} else {
|
|
1191
|
+
if (filter.isMulti === false) {
|
|
1192
|
+
// Single select mode - use single value
|
|
1193
|
+
initialValues[filter.id] = Array.isArray(
|
|
1194
|
+
filter.value
|
|
1195
|
+
)
|
|
1196
|
+
? filter.value[0] || ''
|
|
1197
|
+
: filter.value || '';
|
|
1198
|
+
} else {
|
|
1199
|
+
// Multi select mode - use array
|
|
1200
|
+
initialValues[filter.id] = Array.isArray(
|
|
1201
|
+
filter.value
|
|
1202
|
+
)
|
|
1203
|
+
? filter.value
|
|
1204
|
+
: filter.value
|
|
1205
|
+
? [filter.value]
|
|
1206
|
+
: [];
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
} else {
|
|
1210
|
+
// For other filter types
|
|
1211
|
+
if (filter.value === 'now') {
|
|
1212
|
+
initialValues[filter.id] = new Date()
|
|
1213
|
+
.getFullYear()
|
|
1214
|
+
.toString();
|
|
1215
|
+
} else {
|
|
1216
|
+
initialValues[filter.id] = filter.value || '';
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Fetch options for dropdowns with URL
|
|
1221
|
+
if (
|
|
1222
|
+
(filter.type === 'dropdown' ||
|
|
1223
|
+
filter.type === 'multiselect') &&
|
|
1224
|
+
filter.url
|
|
1225
|
+
) {
|
|
1226
|
+
fetchFilterOptions(filter);
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
setSettingsFilterValues(initialValues);
|
|
1231
|
+
}
|
|
1232
|
+
}, [settings?.filters, fetchFilterOptions]);
|
|
1233
|
+
|
|
1234
|
+
// Handle settings filter change
|
|
1235
|
+
const handleSettingsFilterChange = useCallback(
|
|
1236
|
+
(filterId, value) => {
|
|
1237
|
+
setSettingsFilterValues((prev) => ({
|
|
1238
|
+
...prev,
|
|
1239
|
+
[filterId]: value,
|
|
1240
|
+
}));
|
|
1241
|
+
|
|
1242
|
+
// Update fetch params with the new filter value
|
|
1243
|
+
if (settings?.fetch?.params) {
|
|
1244
|
+
let processedValue = value;
|
|
1245
|
+
|
|
1246
|
+
// For multiselect, convert array to appropriate format for API
|
|
1247
|
+
if (Array.isArray(value)) {
|
|
1248
|
+
// Keep as array - most APIs expect this format for multiple values
|
|
1249
|
+
processedValue = value;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const updatedParams = {
|
|
1253
|
+
...settings.fetch.params,
|
|
1254
|
+
[filterId]: processedValue,
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// Update the settings object
|
|
1258
|
+
if (config.settings && config.settings.fetch) {
|
|
1259
|
+
config.settings.fetch.params = updatedParams;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Trigger a re-fetch with the new params
|
|
1263
|
+
prevFetchParamsRef.current = null;
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
[settings, config]
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
// Apply custom styling from userProfile if available
|
|
1270
|
+
const getCustomStyles = () => {
|
|
1271
|
+
const customStyles = {};
|
|
1272
|
+
|
|
1273
|
+
if (userProfile?.settings?.style) {
|
|
1274
|
+
const { style } = userProfile.settings;
|
|
1275
|
+
|
|
1276
|
+
if (style.hoverColor) {
|
|
1277
|
+
customStyles.hoverColor = {
|
|
1278
|
+
'--hover-color': style.hoverColor,
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
return customStyles;
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
// CSS for sort indicators
|
|
1287
|
+
const getSortIndicatorStyle = (key) => {
|
|
1288
|
+
if (sortConfig.key !== key) {
|
|
1289
|
+
return { opacity: 0.3 }; // Dim the icon when not sorting by this column
|
|
1290
|
+
}
|
|
1291
|
+
return {}; // Full opacity when sorting by this column
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
// Handle row click
|
|
1295
|
+
const handleRowClick = useCallback(
|
|
1296
|
+
(row, rowIndex) => {
|
|
1297
|
+
if (onRowClick) {
|
|
1298
|
+
onRowClick(row, rowIndex);
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
[onRowClick]
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
// Handle cell click with support for date picker and contact selection
|
|
1305
|
+
const handleCellClick = useCallback(
|
|
1306
|
+
async (e, header, row) => {
|
|
1307
|
+
e.stopPropagation(); // Prevent row click
|
|
1308
|
+
|
|
1309
|
+
try {
|
|
1310
|
+
// Debug logging to check configuration
|
|
1311
|
+
const dateOnlyConfig =
|
|
1312
|
+
header.onClick.dateOnly ||
|
|
1313
|
+
userProfile?.settings?.dateOnly ||
|
|
1314
|
+
false;
|
|
1315
|
+
console.log('=== CELL CLICK DEBUG ===');
|
|
1316
|
+
console.log('Column:', header.label);
|
|
1317
|
+
console.log('Column Key:', header.key);
|
|
1318
|
+
console.log('Full Header:', header);
|
|
1319
|
+
console.log('onClick Config:', header.onClick);
|
|
1320
|
+
console.log('onClick Type:', header.onClick?.type);
|
|
1321
|
+
console.log(
|
|
1322
|
+
'Has Alternatives:',
|
|
1323
|
+
!!header.onClick?.alternatives
|
|
1324
|
+
);
|
|
1325
|
+
console.log('Alternatives:', header.onClick?.alternatives);
|
|
1326
|
+
console.log('========================');
|
|
1327
|
+
|
|
1328
|
+
// Determine the interaction type
|
|
1329
|
+
const clickType = header.onClick.type || 'date';
|
|
1330
|
+
|
|
1331
|
+
switch (clickType) {
|
|
1332
|
+
case 'date_with_contacts':
|
|
1333
|
+
// Multi-step process: Date selection first, then contact selection
|
|
1334
|
+
await handleDateWithContactsFlow(
|
|
1335
|
+
header,
|
|
1336
|
+
row,
|
|
1337
|
+
dateOnlyConfig
|
|
1338
|
+
);
|
|
1339
|
+
break;
|
|
1340
|
+
case 'date_range_with_contacts':
|
|
1341
|
+
// Date range selection with contacts
|
|
1342
|
+
await handleDateRangeWithContactsFlow(
|
|
1343
|
+
header,
|
|
1344
|
+
row,
|
|
1345
|
+
dateOnlyConfig
|
|
1346
|
+
);
|
|
1347
|
+
break;
|
|
1348
|
+
case 'date_with_alternatives':
|
|
1349
|
+
// Choice between different workflows (send survey vs not sent)
|
|
1350
|
+
await handleDateWithAlternativesFlow(
|
|
1351
|
+
header,
|
|
1352
|
+
row,
|
|
1353
|
+
dateOnlyConfig
|
|
1354
|
+
);
|
|
1355
|
+
break;
|
|
1356
|
+
case 'contacts_only':
|
|
1357
|
+
// Contact selection only (no date selection)
|
|
1358
|
+
await handleContactsOnlyFlow(header, row);
|
|
1359
|
+
break;
|
|
1360
|
+
default:
|
|
1361
|
+
// Simple date selection (backward compatibility)
|
|
1362
|
+
await handleSimpleDateSelection(
|
|
1363
|
+
header,
|
|
1364
|
+
row,
|
|
1365
|
+
dateOnlyConfig
|
|
1366
|
+
);
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
console.error('Error handling cell click:', error);
|
|
1371
|
+
toast.error('An error occurred while opening the dialog');
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
[settings, settingsFilterValues, onDataChange, userProfile]
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
// Handle simple date selection (existing functionality)
|
|
1378
|
+
const handleSimpleDateSelection = useCallback(
|
|
1379
|
+
async (header, row, dateOnlyConfig) => {
|
|
1380
|
+
await showDatePickerDialog({
|
|
1381
|
+
title: header.onClick.title || 'Select Date',
|
|
1382
|
+
message:
|
|
1383
|
+
header.onClick.message ||
|
|
1384
|
+
header.onClick.placeholder ||
|
|
1385
|
+
`Please select a date for ${header.label}:`,
|
|
1386
|
+
confirmLabel: 'Set Date',
|
|
1387
|
+
cancelLabel: 'Cancel',
|
|
1388
|
+
defaultDate:
|
|
1389
|
+
header.onClick.params?.value === 'now'
|
|
1390
|
+
? 'now'
|
|
1391
|
+
: row[header.key] || null,
|
|
1392
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
1393
|
+
// Timezone configuration - can be set via userProfile or header config
|
|
1394
|
+
timezone:
|
|
1395
|
+
header.onClick.timezone ||
|
|
1396
|
+
userProfile?.settings?.timezone ||
|
|
1397
|
+
'auto',
|
|
1398
|
+
forcePerth:
|
|
1399
|
+
header.onClick.forcePerth ||
|
|
1400
|
+
userProfile?.settings?.forcePerth ||
|
|
1401
|
+
false,
|
|
1402
|
+
// Date field type - set to true for DATE fields, false for DATETIME fields
|
|
1403
|
+
dateOnly: dateOnlyConfig,
|
|
1404
|
+
onConfirm: async (selectedDateISO) => {
|
|
1405
|
+
await submitDateUpdate(header, row, selectedDateISO);
|
|
1406
|
+
},
|
|
1407
|
+
onCancel: () => {
|
|
1408
|
+
// Do nothing when cancelled
|
|
1409
|
+
},
|
|
1410
|
+
});
|
|
1411
|
+
},
|
|
1412
|
+
[settings, settingsFilterValues, onDataChange, userProfile]
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1415
|
+
// Handle date selection with contact selection flow
|
|
1416
|
+
const handleDateWithContactsFlow = useCallback(
|
|
1417
|
+
async (header, row, dateOnlyConfig) => {
|
|
1418
|
+
// Step 1: Date selection
|
|
1419
|
+
await showDatePickerDialog({
|
|
1420
|
+
title: header.onClick.title || 'Select Date',
|
|
1421
|
+
message:
|
|
1422
|
+
header.onClick.message ||
|
|
1423
|
+
header.onClick.placeholder ||
|
|
1424
|
+
`Please select a date for ${header.label}:`,
|
|
1425
|
+
confirmLabel: 'Next: Select Contacts',
|
|
1426
|
+
cancelLabel: 'Cancel',
|
|
1427
|
+
defaultDate:
|
|
1428
|
+
header.onClick.params?.value === 'now'
|
|
1429
|
+
? 'now'
|
|
1430
|
+
: row[header.key] || null,
|
|
1431
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
1432
|
+
timezone:
|
|
1433
|
+
header.onClick.timezone ||
|
|
1434
|
+
userProfile?.settings?.timezone ||
|
|
1435
|
+
'auto',
|
|
1436
|
+
forcePerth:
|
|
1437
|
+
header.onClick.forcePerth ||
|
|
1438
|
+
userProfile?.settings?.forcePerth ||
|
|
1439
|
+
false,
|
|
1440
|
+
dateOnly: dateOnlyConfig,
|
|
1441
|
+
onConfirm: async (selectedDateISO) => {
|
|
1442
|
+
// Step 2: Contact selection
|
|
1443
|
+
const contactFieldKey = header.key + '_contacts';
|
|
1444
|
+
const existingContacts = row[contactFieldKey] || [];
|
|
1445
|
+
|
|
1446
|
+
console.log('Opening contact selector with row context:', {
|
|
1447
|
+
rowKeys: Object.keys(row),
|
|
1448
|
+
rowClientId: row.client_id,
|
|
1449
|
+
urlParams: header.onClick.contactSelection?.urlParams,
|
|
1450
|
+
fullRow: row,
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
await showContactSelectorModal({
|
|
1454
|
+
title:
|
|
1455
|
+
header.onClick.contactSelection?.title ||
|
|
1456
|
+
'Select Contacts',
|
|
1457
|
+
message:
|
|
1458
|
+
header.onClick.contactSelection?.message ||
|
|
1459
|
+
`Add contacts for ${header.label}:`,
|
|
1460
|
+
confirmLabel: 'Save',
|
|
1461
|
+
cancelLabel: 'Cancel',
|
|
1462
|
+
existingContacts: Array.isArray(existingContacts)
|
|
1463
|
+
? existingContacts
|
|
1464
|
+
: [],
|
|
1465
|
+
contactConfig: {
|
|
1466
|
+
contactsUrl:
|
|
1467
|
+
header.onClick.contactSelection?.contactsUrl ||
|
|
1468
|
+
'/ajax/contacts/dropdown',
|
|
1469
|
+
urlParams:
|
|
1470
|
+
header.onClick.contactSelection?.urlParams ||
|
|
1471
|
+
{},
|
|
1472
|
+
allowManualEntry:
|
|
1473
|
+
header.onClick.contactSelection
|
|
1474
|
+
?.allowManualEntry !== false,
|
|
1475
|
+
fields: header.onClick.contactSelection?.fields || [
|
|
1476
|
+
'name',
|
|
1477
|
+
'email',
|
|
1478
|
+
],
|
|
1479
|
+
multiple:
|
|
1480
|
+
header.onClick.contactSelection?.multiple !==
|
|
1481
|
+
false,
|
|
1482
|
+
maxContacts:
|
|
1483
|
+
header.onClick.contactSelection?.maxContacts ||
|
|
1484
|
+
10,
|
|
1485
|
+
},
|
|
1486
|
+
data: {
|
|
1487
|
+
name: header.key,
|
|
1488
|
+
label: header.label,
|
|
1489
|
+
id: row.id,
|
|
1490
|
+
},
|
|
1491
|
+
rowContext: row,
|
|
1492
|
+
onConfirm: async (selectedContacts) => {
|
|
1493
|
+
// Submit both date and contacts
|
|
1494
|
+
await submitDateAndContactsUpdate(
|
|
1495
|
+
header,
|
|
1496
|
+
row,
|
|
1497
|
+
selectedDateISO,
|
|
1498
|
+
selectedContacts
|
|
1499
|
+
);
|
|
1500
|
+
},
|
|
1501
|
+
onCancel: () => {
|
|
1502
|
+
// User cancelled contact selection - do nothing
|
|
1503
|
+
},
|
|
1504
|
+
});
|
|
1505
|
+
},
|
|
1506
|
+
onCancel: () => {
|
|
1507
|
+
// User cancelled date selection - do nothing
|
|
1508
|
+
},
|
|
1509
|
+
});
|
|
1510
|
+
},
|
|
1511
|
+
[settings, settingsFilterValues, onDataChange, userProfile]
|
|
1512
|
+
);
|
|
1513
|
+
|
|
1514
|
+
// Submit date update only
|
|
1515
|
+
const submitDateUpdate = useCallback(
|
|
1516
|
+
async (header, row, selectedDateISO) => {
|
|
1517
|
+
try {
|
|
1518
|
+
// Replace {id} in the URL with the row's ID
|
|
1519
|
+
const url = header.onClick.url.replace('{id}', row.id);
|
|
1520
|
+
|
|
1521
|
+
// Show loading toast
|
|
1522
|
+
toast.info('Updating...', { autoClose: 1000 });
|
|
1523
|
+
|
|
1524
|
+
// Extract field and value from params
|
|
1525
|
+
const { field, ...otherParams } = header.onClick.params;
|
|
1526
|
+
|
|
1527
|
+
// Create a new params object with the field as the key
|
|
1528
|
+
const params = {
|
|
1529
|
+
...otherParams,
|
|
1530
|
+
_method: header.onClick.method, // Add _method for PUT/DELETE requests
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
// Set the field as the key with the selected date value
|
|
1534
|
+
params[field] = selectedDateISO;
|
|
1535
|
+
|
|
1536
|
+
// Make the API call
|
|
1537
|
+
const response = await CustomFetch(
|
|
1538
|
+
url,
|
|
1539
|
+
header.onClick.method,
|
|
1540
|
+
params
|
|
1541
|
+
);
|
|
1542
|
+
|
|
1543
|
+
if (response.data.error) {
|
|
1544
|
+
toast.error(response.data.error);
|
|
1545
|
+
} else {
|
|
1546
|
+
toast.success('Date updated successfully', {
|
|
1547
|
+
autoClose: 1000,
|
|
1548
|
+
});
|
|
1549
|
+
await refreshGridData();
|
|
1550
|
+
}
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
console.error('Error updating date:', error);
|
|
1553
|
+
toast.error('An error occurred while updating');
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1556
|
+
[]
|
|
1557
|
+
);
|
|
1558
|
+
|
|
1559
|
+
// Submit date and contacts update
|
|
1560
|
+
const submitDateAndContactsUpdate = useCallback(
|
|
1561
|
+
async (header, row, selectedDateISO, selectedContacts) => {
|
|
1562
|
+
try {
|
|
1563
|
+
// Replace {id} in the URL with the row's ID
|
|
1564
|
+
const url = header.onClick.url.replace('{id}', row.id);
|
|
1565
|
+
|
|
1566
|
+
// Show loading toast
|
|
1567
|
+
toast.info('Saving date and contacts...', { autoClose: 1000 });
|
|
1568
|
+
|
|
1569
|
+
// Extract field and value from params
|
|
1570
|
+
const { field, ...otherParams } = header.onClick.params;
|
|
1571
|
+
|
|
1572
|
+
// Create a new params object with both date and contacts
|
|
1573
|
+
const params = {
|
|
1574
|
+
...otherParams,
|
|
1575
|
+
_method: header.onClick.method, // Add _method for PUT/DELETE requests
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
// Set the date field
|
|
1579
|
+
params[field] = selectedDateISO;
|
|
1580
|
+
|
|
1581
|
+
// Set the contacts field (field name + '_contacts')
|
|
1582
|
+
const contactsField = field + '_contacts';
|
|
1583
|
+
params[contactsField] = selectedContacts;
|
|
1584
|
+
|
|
1585
|
+
console.log('Submitting date and contacts:', params);
|
|
1586
|
+
|
|
1587
|
+
// Make the API call
|
|
1588
|
+
const response = await CustomFetch(
|
|
1589
|
+
url,
|
|
1590
|
+
header.onClick.method,
|
|
1591
|
+
params
|
|
1592
|
+
);
|
|
1593
|
+
|
|
1594
|
+
if (response.data.error) {
|
|
1595
|
+
toast.error(response.data.error);
|
|
1596
|
+
} else {
|
|
1597
|
+
toast.success('Date and contacts saved successfully', {
|
|
1598
|
+
autoClose: 1500,
|
|
1599
|
+
});
|
|
1600
|
+
await refreshGridData();
|
|
1601
|
+
}
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
console.error('Error updating date and contacts:', error);
|
|
1604
|
+
toast.error('An error occurred while saving');
|
|
1605
|
+
}
|
|
1606
|
+
},
|
|
1607
|
+
[]
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
// Handle date range selection with contact selection flow
|
|
1611
|
+
const handleDateRangeWithContactsFlow = useCallback(
|
|
1612
|
+
async (header, row, dateOnlyConfig) => {
|
|
1613
|
+
const startField = header.onClick?.startField || 'start_date';
|
|
1614
|
+
const endField = header.onClick?.endField || 'end_date';
|
|
1615
|
+
|
|
1616
|
+
// Step 1: Date range selection
|
|
1617
|
+
await showDateRangeSelectorModal({
|
|
1618
|
+
title: header.onClick.title || 'Select Agreement Period',
|
|
1619
|
+
message:
|
|
1620
|
+
header.onClick.message ||
|
|
1621
|
+
`Please select the ${header.label} period:`,
|
|
1622
|
+
confirmLabel: 'Next: Select Contacts',
|
|
1623
|
+
cancelLabel: 'Cancel',
|
|
1624
|
+
defaultStartDate: row[startField] || null,
|
|
1625
|
+
defaultEndDate: row[endField] || null,
|
|
1626
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
1627
|
+
timezone:
|
|
1628
|
+
header.onClick.timezone ||
|
|
1629
|
+
userProfile?.settings?.timezone ||
|
|
1630
|
+
'auto',
|
|
1631
|
+
forcePerth:
|
|
1632
|
+
header.onClick.forcePerth ||
|
|
1633
|
+
userProfile?.settings?.forcePerth ||
|
|
1634
|
+
false,
|
|
1635
|
+
dateOnly: dateOnlyConfig,
|
|
1636
|
+
onConfirm: async (selectedStartDateISO, selectedEndDateISO) => {
|
|
1637
|
+
// Step 2: Contact selection
|
|
1638
|
+
const contactsField = header.key + '_contacts';
|
|
1639
|
+
const existingContacts = row[contactsField] || [];
|
|
1640
|
+
|
|
1641
|
+
await showContactSelectorModal({
|
|
1642
|
+
title:
|
|
1643
|
+
header.onClick.contactSelection?.title ||
|
|
1644
|
+
'Select Contacts for Agreement',
|
|
1645
|
+
message:
|
|
1646
|
+
header.onClick.contactSelection?.message ||
|
|
1647
|
+
`Add contacts for ${header.label}:`,
|
|
1648
|
+
confirmLabel: 'Save',
|
|
1649
|
+
cancelLabel: 'Cancel',
|
|
1650
|
+
existingContacts: Array.isArray(existingContacts)
|
|
1651
|
+
? existingContacts
|
|
1652
|
+
: [],
|
|
1653
|
+
contactConfig: {
|
|
1654
|
+
contactsUrl:
|
|
1655
|
+
header.onClick.contactSelection?.contactsUrl ||
|
|
1656
|
+
'/ajax/contacts/dropdown',
|
|
1657
|
+
urlParams:
|
|
1658
|
+
header.onClick.contactSelection?.urlParams ||
|
|
1659
|
+
{},
|
|
1660
|
+
allowManualEntry:
|
|
1661
|
+
header.onClick.contactSelection
|
|
1662
|
+
?.allowManualEntry !== false,
|
|
1663
|
+
fields: header.onClick.contactSelection?.fields || [
|
|
1664
|
+
'name',
|
|
1665
|
+
'email',
|
|
1666
|
+
],
|
|
1667
|
+
fieldTypes:
|
|
1668
|
+
header.onClick.contactSelection?.fieldTypes ||
|
|
1669
|
+
{},
|
|
1670
|
+
fieldLabels:
|
|
1671
|
+
header.onClick.contactSelection?.fieldLabels ||
|
|
1672
|
+
{},
|
|
1673
|
+
validation:
|
|
1674
|
+
header.onClick.contactSelection?.validation ||
|
|
1675
|
+
{},
|
|
1676
|
+
multiple:
|
|
1677
|
+
header.onClick.contactSelection?.multiple !==
|
|
1678
|
+
false,
|
|
1679
|
+
maxContacts:
|
|
1680
|
+
header.onClick.contactSelection?.maxContacts ||
|
|
1681
|
+
10,
|
|
1682
|
+
},
|
|
1683
|
+
data: {
|
|
1684
|
+
name: header.key,
|
|
1685
|
+
label: header.label,
|
|
1686
|
+
id: row.id,
|
|
1687
|
+
},
|
|
1688
|
+
rowContext: row,
|
|
1689
|
+
onConfirm: async (selectedContacts) => {
|
|
1690
|
+
// Submit date range and contacts
|
|
1691
|
+
await submitDateRangeAndContactsUpdate(
|
|
1692
|
+
header,
|
|
1693
|
+
row,
|
|
1694
|
+
selectedStartDateISO,
|
|
1695
|
+
selectedEndDateISO,
|
|
1696
|
+
selectedContacts
|
|
1697
|
+
);
|
|
1698
|
+
},
|
|
1699
|
+
onCancel: () => {
|
|
1700
|
+
// User cancelled contact selection - do nothing
|
|
1701
|
+
},
|
|
1702
|
+
});
|
|
1703
|
+
},
|
|
1704
|
+
onCancel: () => {
|
|
1705
|
+
// User cancelled date range selection - do nothing
|
|
1706
|
+
},
|
|
1707
|
+
});
|
|
1708
|
+
},
|
|
1709
|
+
[settings, settingsFilterValues, onDataChange, userProfile]
|
|
1710
|
+
);
|
|
1711
|
+
|
|
1712
|
+
// Handle alternatives workflow (send vs not sent)
|
|
1713
|
+
const handleDateWithAlternativesFlow = useCallback(
|
|
1714
|
+
async (header, row, dateOnlyConfig) => {
|
|
1715
|
+
console.log('=== ALTERNATIVES FLOW CALLED ===');
|
|
1716
|
+
console.log('Header alternatives:', header.onClick.alternatives);
|
|
1717
|
+
|
|
1718
|
+
const alternatives = header.onClick.alternatives || [
|
|
1719
|
+
{
|
|
1720
|
+
id: 'date_contacts',
|
|
1721
|
+
label: 'Send Survey',
|
|
1722
|
+
icon: '📅',
|
|
1723
|
+
workflow: 'date_with_contacts',
|
|
1724
|
+
description: 'Mark date and add contacts',
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
id: 'not_sent',
|
|
1728
|
+
label: 'Mark as Not Sent',
|
|
1729
|
+
icon: '❌',
|
|
1730
|
+
workflow: 'reason_only',
|
|
1731
|
+
description: 'Record reason for not sending',
|
|
1732
|
+
},
|
|
1733
|
+
];
|
|
1734
|
+
|
|
1735
|
+
await showAlternativeActionModal({
|
|
1736
|
+
title: header.onClick.title || 'Choose Action',
|
|
1737
|
+
message:
|
|
1738
|
+
header.onClick.message ||
|
|
1739
|
+
`What would you like to do for ${header.label}?`,
|
|
1740
|
+
alternatives: alternatives,
|
|
1741
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
1742
|
+
onSelect: async (selectedAlternative) => {
|
|
1743
|
+
if (selectedAlternative.workflow === 'date_with_contacts') {
|
|
1744
|
+
// Handle date + contacts workflow
|
|
1745
|
+
await handleDateWithContactsWorkflow(
|
|
1746
|
+
header,
|
|
1747
|
+
row,
|
|
1748
|
+
dateOnlyConfig,
|
|
1749
|
+
selectedAlternative
|
|
1750
|
+
);
|
|
1751
|
+
} else if (selectedAlternative.workflow === 'reason_only') {
|
|
1752
|
+
// Handle not sent reason workflow
|
|
1753
|
+
await handleNotSentReasonWorkflow(
|
|
1754
|
+
header,
|
|
1755
|
+
row,
|
|
1756
|
+
selectedAlternative
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
},
|
|
1760
|
+
onCancel: () => {
|
|
1761
|
+
// User cancelled - do nothing
|
|
1762
|
+
},
|
|
1763
|
+
});
|
|
1764
|
+
},
|
|
1765
|
+
[settings, settingsFilterValues, onDataChange, userProfile]
|
|
1766
|
+
);
|
|
1767
|
+
|
|
1768
|
+
// Handle date with contacts workflow (from alternatives)
|
|
1769
|
+
const handleDateWithContactsWorkflow = useCallback(
|
|
1770
|
+
async (header, row, dateOnlyConfig, alternative) => {
|
|
1771
|
+
// Step 1: Date selection
|
|
1772
|
+
await showDatePickerDialog({
|
|
1773
|
+
title:
|
|
1774
|
+
alternative.title || header.onClick.title || 'Select Date',
|
|
1775
|
+
message:
|
|
1776
|
+
alternative.message ||
|
|
1777
|
+
header.onClick.message ||
|
|
1778
|
+
`Please select a date for ${header.label}:`,
|
|
1779
|
+
confirmLabel: 'Next: Select Contacts',
|
|
1780
|
+
cancelLabel: 'Cancel',
|
|
1781
|
+
defaultDate:
|
|
1782
|
+
header.onClick.params?.value === 'now'
|
|
1783
|
+
? 'now'
|
|
1784
|
+
: row[header.key] || null,
|
|
1785
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
1786
|
+
timezone:
|
|
1787
|
+
header.onClick.timezone ||
|
|
1788
|
+
userProfile?.settings?.timezone ||
|
|
1789
|
+
'auto',
|
|
1790
|
+
forcePerth:
|
|
1791
|
+
header.onClick.forcePerth ||
|
|
1792
|
+
userProfile?.settings?.forcePerth ||
|
|
1793
|
+
false,
|
|
1794
|
+
dateOnly: dateOnlyConfig,
|
|
1795
|
+
onConfirm: async (selectedDateISO) => {
|
|
1796
|
+
// Step 2: Contact selection
|
|
1797
|
+
const contactsField = header.key + '_contacts';
|
|
1798
|
+
const existingContacts = row[contactsField] || [];
|
|
1799
|
+
|
|
1800
|
+
const contactSelection =
|
|
1801
|
+
alternative.contactSelection ||
|
|
1802
|
+
header.onClick.contactSelection ||
|
|
1803
|
+
{};
|
|
1804
|
+
|
|
1805
|
+
await showContactSelectorModal({
|
|
1806
|
+
title: contactSelection.title || 'Select Contacts',
|
|
1807
|
+
message:
|
|
1808
|
+
contactSelection.message ||
|
|
1809
|
+
`Add contacts for ${header.label}:`,
|
|
1810
|
+
confirmLabel: 'Save',
|
|
1811
|
+
cancelLabel: 'Cancel',
|
|
1812
|
+
existingContacts: Array.isArray(existingContacts)
|
|
1813
|
+
? existingContacts
|
|
1814
|
+
: [],
|
|
1815
|
+
contactConfig: {
|
|
1816
|
+
contactsUrl:
|
|
1817
|
+
contactSelection.contactsUrl ||
|
|
1818
|
+
'/ajax/contacts/dropdown',
|
|
1819
|
+
urlParams: contactSelection.urlParams || {},
|
|
1820
|
+
allowManualEntry:
|
|
1821
|
+
contactSelection.allowManualEntry !== false,
|
|
1822
|
+
fields: contactSelection.fields || [
|
|
1823
|
+
'name',
|
|
1824
|
+
'email',
|
|
1825
|
+
],
|
|
1826
|
+
fieldTypes: contactSelection.fieldTypes || {},
|
|
1827
|
+
fieldLabels: contactSelection.fieldLabels || {},
|
|
1828
|
+
validation: contactSelection.validation || {},
|
|
1829
|
+
multiple: contactSelection.multiple !== false,
|
|
1830
|
+
maxContacts: contactSelection.maxContacts || 10,
|
|
1831
|
+
},
|
|
1832
|
+
data: {
|
|
1833
|
+
name: header.key,
|
|
1834
|
+
label: header.label,
|
|
1835
|
+
id: row.id,
|
|
1836
|
+
},
|
|
1837
|
+
rowContext: row,
|
|
1838
|
+
onConfirm: async (selectedContacts) => {
|
|
1839
|
+
// Submit both date and contacts with status
|
|
1840
|
+
await submitDateContactsAndStatusUpdate(
|
|
1841
|
+
header,
|
|
1842
|
+
row,
|
|
1843
|
+
selectedDateISO,
|
|
1844
|
+
selectedContacts,
|
|
1845
|
+
'sent'
|
|
1846
|
+
);
|
|
1847
|
+
},
|
|
1848
|
+
onCancel: () => {
|
|
1849
|
+
// User cancelled contact selection - do nothing
|
|
1850
|
+
},
|
|
1851
|
+
});
|
|
1852
|
+
},
|
|
1853
|
+
onCancel: () => {
|
|
1854
|
+
// User cancelled date selection - do nothing
|
|
1855
|
+
},
|
|
1856
|
+
});
|
|
1857
|
+
},
|
|
1858
|
+
[settings, settingsFilterValues, onDataChange, userProfile]
|
|
1859
|
+
);
|
|
1860
|
+
|
|
1861
|
+
// Handle not sent reason workflow
|
|
1862
|
+
const handleNotSentReasonWorkflow = useCallback(
|
|
1863
|
+
async (header, row, alternative) => {
|
|
1864
|
+
const reasonField =
|
|
1865
|
+
alternative.reasonField || header.key + '_reason';
|
|
1866
|
+
const statusField =
|
|
1867
|
+
alternative.statusField || header.key + '_status';
|
|
1868
|
+
|
|
1869
|
+
await showReasonCollectorModal({
|
|
1870
|
+
title: alternative.title || 'Provide Reason',
|
|
1871
|
+
message:
|
|
1872
|
+
alternative.message ||
|
|
1873
|
+
`Why wasn't the ${header.label} sent?`,
|
|
1874
|
+
confirmLabel: 'Save Reason',
|
|
1875
|
+
cancelLabel: 'Cancel',
|
|
1876
|
+
reasonOptions: alternative.reasonOptions || [
|
|
1877
|
+
'Survey not applicable',
|
|
1878
|
+
'Client does not want survey',
|
|
1879
|
+
'Other',
|
|
1880
|
+
],
|
|
1881
|
+
allowCustomReason: alternative.allowCustomReason !== false,
|
|
1882
|
+
reasonRequired: alternative.reasonRequired !== false,
|
|
1883
|
+
placeholder:
|
|
1884
|
+
alternative.placeholder || 'Enter custom reason...',
|
|
1885
|
+
defaultReason: row[reasonField] || '',
|
|
1886
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
1887
|
+
onConfirm: async (reason) => {
|
|
1888
|
+
// Submit reason and status
|
|
1889
|
+
await submitReasonAndStatusUpdate(
|
|
1890
|
+
header,
|
|
1891
|
+
row,
|
|
1892
|
+
reason,
|
|
1893
|
+
'not_sent',
|
|
1894
|
+
reasonField,
|
|
1895
|
+
statusField
|
|
1896
|
+
);
|
|
1897
|
+
},
|
|
1898
|
+
onCancel: () => {
|
|
1899
|
+
// User cancelled - do nothing
|
|
1900
|
+
},
|
|
1901
|
+
});
|
|
1902
|
+
},
|
|
1903
|
+
[settings, settingsFilterValues, onDataChange, userProfile]
|
|
1904
|
+
);
|
|
1905
|
+
|
|
1906
|
+
// Submit date range and contacts update
|
|
1907
|
+
const submitDateRangeAndContactsUpdate = useCallback(
|
|
1908
|
+
async (header, row, startDateISO, endDateISO, selectedContacts) => {
|
|
1909
|
+
try {
|
|
1910
|
+
const url = header.onClick.url.replace('{id}', row.id);
|
|
1911
|
+
const startField = header.onClick?.startField || 'start_date';
|
|
1912
|
+
const endField = header.onClick?.endField || 'end_date';
|
|
1913
|
+
const contactsField = header.key + '_contacts';
|
|
1914
|
+
|
|
1915
|
+
toast.info('Saving date range and contacts...', {
|
|
1916
|
+
autoClose: 1000,
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
const params = {
|
|
1920
|
+
...header.onClick.params,
|
|
1921
|
+
_method: header.onClick.method,
|
|
1922
|
+
[startField]: startDateISO,
|
|
1923
|
+
[endField]: endDateISO,
|
|
1924
|
+
[contactsField]: selectedContacts,
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
const response = await CustomFetch(
|
|
1928
|
+
url,
|
|
1929
|
+
header.onClick.method,
|
|
1930
|
+
params
|
|
1931
|
+
);
|
|
1932
|
+
|
|
1933
|
+
if (response.data.error) {
|
|
1934
|
+
toast.error(response.data.error);
|
|
1935
|
+
} else {
|
|
1936
|
+
toast.success(
|
|
1937
|
+
'Date range and contacts saved successfully',
|
|
1938
|
+
{ autoClose: 1500 }
|
|
1939
|
+
);
|
|
1940
|
+
await refreshGridData();
|
|
1941
|
+
}
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
console.error('Error updating date range and contacts:', error);
|
|
1944
|
+
toast.error('An error occurred while saving');
|
|
1945
|
+
}
|
|
1946
|
+
},
|
|
1947
|
+
[]
|
|
1948
|
+
);
|
|
1949
|
+
|
|
1950
|
+
// Submit date, contacts, and status update
|
|
1951
|
+
const submitDateContactsAndStatusUpdate = useCallback(
|
|
1952
|
+
async (header, row, selectedDateISO, selectedContacts, status) => {
|
|
1953
|
+
try {
|
|
1954
|
+
const url = header.onClick.url.replace('{id}', row.id);
|
|
1955
|
+
const { field, ...otherParams } = header.onClick.params;
|
|
1956
|
+
const contactsField = field + '_contacts';
|
|
1957
|
+
const statusField = field + '_status';
|
|
1958
|
+
|
|
1959
|
+
toast.info('Saving date, contacts, and status...', {
|
|
1960
|
+
autoClose: 1000,
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
const params = {
|
|
1964
|
+
...otherParams,
|
|
1965
|
+
_method: header.onClick.method,
|
|
1966
|
+
[field]: selectedDateISO,
|
|
1967
|
+
[contactsField]: selectedContacts,
|
|
1968
|
+
[statusField]: status,
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
const response = await CustomFetch(
|
|
1972
|
+
url,
|
|
1973
|
+
header.onClick.method,
|
|
1974
|
+
params
|
|
1975
|
+
);
|
|
1976
|
+
|
|
1977
|
+
if (response.data.error) {
|
|
1978
|
+
toast.error(response.data.error);
|
|
1979
|
+
} else {
|
|
1980
|
+
toast.success('Survey data saved successfully', {
|
|
1981
|
+
autoClose: 1500,
|
|
1982
|
+
});
|
|
1983
|
+
await refreshGridData();
|
|
1984
|
+
}
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
console.error('Error updating survey data:', error);
|
|
1987
|
+
toast.error('An error occurred while saving');
|
|
1988
|
+
}
|
|
1989
|
+
},
|
|
1990
|
+
[]
|
|
1991
|
+
);
|
|
1992
|
+
|
|
1993
|
+
// Submit reason and status update
|
|
1994
|
+
const submitReasonAndStatusUpdate = useCallback(
|
|
1995
|
+
async (header, row, reason, status, reasonField, statusField) => {
|
|
1996
|
+
try {
|
|
1997
|
+
const url = header.onClick.url.replace('{id}', row.id);
|
|
1998
|
+
|
|
1999
|
+
toast.info('Saving reason...', { autoClose: 1000 });
|
|
2000
|
+
|
|
2001
|
+
const params = {
|
|
2002
|
+
...header.onClick.params,
|
|
2003
|
+
_method: header.onClick.method,
|
|
2004
|
+
[reasonField]: reason,
|
|
2005
|
+
[statusField]: status,
|
|
2006
|
+
};
|
|
2007
|
+
|
|
2008
|
+
const response = await CustomFetch(
|
|
2009
|
+
url,
|
|
2010
|
+
header.onClick.method,
|
|
2011
|
+
params
|
|
2012
|
+
);
|
|
2013
|
+
|
|
2014
|
+
if (response.data.error) {
|
|
2015
|
+
toast.error(response.data.error);
|
|
2016
|
+
} else {
|
|
2017
|
+
toast.success('Reason saved successfully', {
|
|
2018
|
+
autoClose: 1500,
|
|
2019
|
+
});
|
|
2020
|
+
await refreshGridData();
|
|
2021
|
+
}
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
console.error('Error saving reason:', error);
|
|
2024
|
+
toast.error('An error occurred while saving');
|
|
2025
|
+
}
|
|
2026
|
+
},
|
|
2027
|
+
[]
|
|
2028
|
+
);
|
|
2029
|
+
|
|
2030
|
+
// Handle contacts only flow (no date selection)
|
|
2031
|
+
const handleContactsOnlyFlow = useCallback(
|
|
2032
|
+
async (header, row) => {
|
|
2033
|
+
const contactsField = header.key + '_contacts';
|
|
2034
|
+
const existingContacts = row[contactsField] || [];
|
|
2035
|
+
|
|
2036
|
+
await showContactSelectorModal({
|
|
2037
|
+
title:
|
|
2038
|
+
header.onClick.contactSelection?.title ||
|
|
2039
|
+
header.onClick.title ||
|
|
2040
|
+
'Select Contacts',
|
|
2041
|
+
message:
|
|
2042
|
+
header.onClick.contactSelection?.message ||
|
|
2043
|
+
header.onClick.message ||
|
|
2044
|
+
`Add contacts for ${header.label}:`,
|
|
2045
|
+
confirmLabel: 'Save',
|
|
2046
|
+
cancelLabel: 'Cancel',
|
|
2047
|
+
existingContacts: Array.isArray(existingContacts)
|
|
2048
|
+
? existingContacts
|
|
2049
|
+
: [],
|
|
2050
|
+
contactConfig: {
|
|
2051
|
+
contactsUrl:
|
|
2052
|
+
header.onClick.contactSelection?.contactsUrl ||
|
|
2053
|
+
'/ajax/contacts/dropdown',
|
|
2054
|
+
urlParams: header.onClick.contactSelection?.urlParams || {},
|
|
2055
|
+
allowManualEntry:
|
|
2056
|
+
header.onClick.contactSelection?.allowManualEntry !==
|
|
2057
|
+
false,
|
|
2058
|
+
fields: header.onClick.contactSelection?.fields || [
|
|
2059
|
+
'name',
|
|
2060
|
+
'email',
|
|
2061
|
+
],
|
|
2062
|
+
fieldTypes:
|
|
2063
|
+
header.onClick.contactSelection?.fieldTypes || {},
|
|
2064
|
+
fieldLabels:
|
|
2065
|
+
header.onClick.contactSelection?.fieldLabels || {},
|
|
2066
|
+
validation:
|
|
2067
|
+
header.onClick.contactSelection?.validation || {},
|
|
2068
|
+
multiple:
|
|
2069
|
+
header.onClick.contactSelection?.multiple !== false,
|
|
2070
|
+
maxContacts:
|
|
2071
|
+
header.onClick.contactSelection?.maxContacts || 10,
|
|
2072
|
+
},
|
|
2073
|
+
data: { name: header.key, label: header.label, id: row.id },
|
|
2074
|
+
rowContext: row,
|
|
2075
|
+
onConfirm: async (selectedContacts) => {
|
|
2076
|
+
// Submit only contacts (no date)
|
|
2077
|
+
await submitContactsOnlyUpdate(
|
|
2078
|
+
header,
|
|
2079
|
+
row,
|
|
2080
|
+
selectedContacts
|
|
2081
|
+
);
|
|
2082
|
+
},
|
|
2083
|
+
onCancel: () => {
|
|
2084
|
+
// User cancelled contact selection - do nothing
|
|
2085
|
+
},
|
|
2086
|
+
});
|
|
2087
|
+
},
|
|
2088
|
+
[settings, settingsFilterValues, onDataChange, userProfile]
|
|
2089
|
+
);
|
|
2090
|
+
|
|
2091
|
+
// Submit contacts only update
|
|
2092
|
+
const submitContactsOnlyUpdate = useCallback(
|
|
2093
|
+
async (header, row, selectedContacts) => {
|
|
2094
|
+
try {
|
|
2095
|
+
const url = header.onClick.url.replace('{id}', row.id);
|
|
2096
|
+
const contactsField = header.key + '_contacts';
|
|
2097
|
+
|
|
2098
|
+
toast.info('Saving contacts...', { autoClose: 1000 });
|
|
2099
|
+
|
|
2100
|
+
const params = {
|
|
2101
|
+
...header.onClick.params,
|
|
2102
|
+
_method: header.onClick.method,
|
|
2103
|
+
[contactsField]: selectedContacts,
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2106
|
+
console.log('Submitting contacts only:', params);
|
|
2107
|
+
|
|
2108
|
+
const response = await CustomFetch(
|
|
2109
|
+
url,
|
|
2110
|
+
header.onClick.method,
|
|
2111
|
+
params
|
|
2112
|
+
);
|
|
2113
|
+
|
|
2114
|
+
if (response.data.error) {
|
|
2115
|
+
toast.error(response.data.error);
|
|
2116
|
+
} else {
|
|
2117
|
+
toast.success('Contacts saved successfully', {
|
|
2118
|
+
autoClose: 1500,
|
|
2119
|
+
});
|
|
2120
|
+
await refreshGridData();
|
|
2121
|
+
}
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
console.error('Error updating contacts:', error);
|
|
2124
|
+
toast.error('An error occurred while saving');
|
|
2125
|
+
}
|
|
2126
|
+
},
|
|
2127
|
+
[]
|
|
2128
|
+
);
|
|
2129
|
+
|
|
2130
|
+
// Refresh grid data helper function
|
|
2131
|
+
const refreshGridData = useCallback(async () => {
|
|
2132
|
+
try {
|
|
2133
|
+
// Refresh the data
|
|
2134
|
+
prevFetchParamsRef.current = null;
|
|
2135
|
+
|
|
2136
|
+
// Force a re-fetch
|
|
2137
|
+
const fetchUrl = settings.fetch.url;
|
|
2138
|
+
const fetchMethod = settings.fetch.method || 'POST';
|
|
2139
|
+
const fetchParams = {
|
|
2140
|
+
...(settings.fetch.params || {}),
|
|
2141
|
+
...settingsFilterValues,
|
|
2142
|
+
};
|
|
2143
|
+
|
|
2144
|
+
setLoading(true);
|
|
2145
|
+
const fetchResponse = await CustomFetch(
|
|
2146
|
+
fetchUrl,
|
|
2147
|
+
fetchMethod,
|
|
2148
|
+
fetchParams
|
|
2149
|
+
);
|
|
2150
|
+
if (fetchResponse.data.data) {
|
|
2151
|
+
if (
|
|
2152
|
+
fetchResponse.data.data.header &&
|
|
2153
|
+
fetchResponse.data.data.rows
|
|
2154
|
+
) {
|
|
2155
|
+
const newData = fetchResponse.data.data.rows || [];
|
|
2156
|
+
setGridData(newData);
|
|
2157
|
+
setSortedData(newData);
|
|
2158
|
+
if (onDataChange) {
|
|
2159
|
+
onDataChange(newData);
|
|
2160
|
+
}
|
|
2161
|
+
} else {
|
|
2162
|
+
const newData = fetchResponse.data.data.data || [];
|
|
2163
|
+
setGridData(newData);
|
|
2164
|
+
setSortedData(newData);
|
|
2165
|
+
if (onDataChange) {
|
|
2166
|
+
onDataChange(newData);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
console.error('Error refreshing data:', error);
|
|
2172
|
+
toast.error('Error refreshing data');
|
|
2173
|
+
} finally {
|
|
2174
|
+
setLoading(false);
|
|
2175
|
+
}
|
|
2176
|
+
}, [settings, settingsFilterValues, onDataChange]);
|
|
2177
|
+
|
|
2178
|
+
return (
|
|
2179
|
+
<div className={styles.gridContainer}>
|
|
2180
|
+
{/* Settings Filters */}
|
|
2181
|
+
{settings?.filters && settings.filters.length > 0 && (
|
|
2182
|
+
<div className={styles.settingsFiltersContainer}>
|
|
2183
|
+
<div className={styles.settingsFiltersHeader}>
|
|
2184
|
+
<div
|
|
2185
|
+
className={styles.headerLeft}
|
|
2186
|
+
onClick={() =>
|
|
2187
|
+
setFiltersCollapsed(!filtersCollapsed)
|
|
2188
|
+
}
|
|
2189
|
+
>
|
|
2190
|
+
<div className={styles.settingsFiltersTitle}>
|
|
2191
|
+
<svg
|
|
2192
|
+
width="16"
|
|
2193
|
+
height="16"
|
|
2194
|
+
viewBox="0 0 24 24"
|
|
2195
|
+
fill="none"
|
|
2196
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2197
|
+
>
|
|
2198
|
+
<path
|
|
2199
|
+
d="M4 6H20M7 12H17M9 18H15"
|
|
2200
|
+
stroke="currentColor"
|
|
2201
|
+
strokeWidth="2"
|
|
2202
|
+
strokeLinecap="round"
|
|
2203
|
+
strokeLinejoin="round"
|
|
2204
|
+
/>
|
|
2205
|
+
</svg>
|
|
2206
|
+
Filters
|
|
2207
|
+
{/* Show active filters count when collapsed */}
|
|
2208
|
+
{filtersCollapsed && (
|
|
2209
|
+
<div
|
|
2210
|
+
className={
|
|
2211
|
+
styles.activeFiltersIndicator
|
|
2212
|
+
}
|
|
2213
|
+
>
|
|
2214
|
+
{Object.values(
|
|
2215
|
+
settingsFilterValues
|
|
2216
|
+
).some((value) => value !== '') && (
|
|
2217
|
+
<>
|
|
2218
|
+
<span>Active:</span>
|
|
2219
|
+
<span
|
|
2220
|
+
className={
|
|
2221
|
+
styles.filterBadge
|
|
2222
|
+
}
|
|
2223
|
+
>
|
|
2224
|
+
{
|
|
2225
|
+
Object.values(
|
|
2226
|
+
settingsFilterValues
|
|
2227
|
+
).filter(
|
|
2228
|
+
(value) =>
|
|
2229
|
+
value !== ''
|
|
2230
|
+
).length
|
|
2231
|
+
}
|
|
2232
|
+
</span>
|
|
2233
|
+
</>
|
|
2234
|
+
)}
|
|
2235
|
+
</div>
|
|
2236
|
+
)}
|
|
2237
|
+
</div>
|
|
2238
|
+
|
|
2239
|
+
{/* Toggle icon */}
|
|
2240
|
+
<svg
|
|
2241
|
+
className={`${styles.toggleIcon} ${
|
|
2242
|
+
filtersCollapsed ? styles.collapsed : ''
|
|
2243
|
+
}`}
|
|
2244
|
+
width="16"
|
|
2245
|
+
height="16"
|
|
2246
|
+
viewBox="0 0 24 24"
|
|
2247
|
+
fill="none"
|
|
2248
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2249
|
+
>
|
|
2250
|
+
<path
|
|
2251
|
+
d="M19 9l-7 7-7-7"
|
|
2252
|
+
stroke="currentColor"
|
|
2253
|
+
strokeWidth="2"
|
|
2254
|
+
strokeLinecap="round"
|
|
2255
|
+
strokeLinejoin="round"
|
|
2256
|
+
/>
|
|
2257
|
+
</svg>
|
|
2258
|
+
</div>
|
|
2259
|
+
|
|
2260
|
+
{/* Clear filters button */}
|
|
2261
|
+
<button
|
|
2262
|
+
className={styles.clearFiltersButton}
|
|
2263
|
+
onClick={(e) => {
|
|
2264
|
+
e.stopPropagation(); // Prevent toggling when clicking the button
|
|
2265
|
+
// Initialize with default values
|
|
2266
|
+
const initialValues = {};
|
|
2267
|
+
settings.filters.forEach((filter) => {
|
|
2268
|
+
if (
|
|
2269
|
+
(filter.type === 'dropdown' ||
|
|
2270
|
+
filter.type === 'multiselect') &&
|
|
2271
|
+
(filter.id === 'year' ||
|
|
2272
|
+
filter.startYear) &&
|
|
2273
|
+
filter.value === 'now'
|
|
2274
|
+
) {
|
|
2275
|
+
const currentYear = new Date()
|
|
2276
|
+
.getFullYear()
|
|
2277
|
+
.toString();
|
|
2278
|
+
if (filter.type === 'multiselect') {
|
|
2279
|
+
initialValues[filter.id] =
|
|
2280
|
+
filter.isMulti === false
|
|
2281
|
+
? currentYear
|
|
2282
|
+
: [currentYear];
|
|
2283
|
+
} else {
|
|
2284
|
+
initialValues[filter.id] =
|
|
2285
|
+
currentYear;
|
|
2286
|
+
}
|
|
2287
|
+
} else if (filter.type === 'multiselect') {
|
|
2288
|
+
if (filter.value === 'now') {
|
|
2289
|
+
const currentYear = new Date()
|
|
2290
|
+
.getFullYear()
|
|
2291
|
+
.toString();
|
|
2292
|
+
initialValues[filter.id] =
|
|
2293
|
+
filter.isMulti === false
|
|
2294
|
+
? currentYear
|
|
2295
|
+
: [currentYear];
|
|
2296
|
+
} else {
|
|
2297
|
+
if (filter.isMulti === false) {
|
|
2298
|
+
initialValues[filter.id] =
|
|
2299
|
+
Array.isArray(filter.value)
|
|
2300
|
+
? filter.value[0] || ''
|
|
2301
|
+
: filter.value || '';
|
|
2302
|
+
} else {
|
|
2303
|
+
initialValues[filter.id] =
|
|
2304
|
+
Array.isArray(filter.value)
|
|
2305
|
+
? filter.value
|
|
2306
|
+
: filter.value
|
|
2307
|
+
? [filter.value]
|
|
2308
|
+
: [];
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
} else {
|
|
2312
|
+
if (filter.value === 'now') {
|
|
2313
|
+
initialValues[filter.id] =
|
|
2314
|
+
new Date()
|
|
2315
|
+
.getFullYear()
|
|
2316
|
+
.toString();
|
|
2317
|
+
} else {
|
|
2318
|
+
initialValues[filter.id] =
|
|
2319
|
+
filter.value || '';
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
setSettingsFilterValues(initialValues);
|
|
2324
|
+
|
|
2325
|
+
// Reset fetch params
|
|
2326
|
+
prevFetchParamsRef.current = null;
|
|
2327
|
+
}}
|
|
2328
|
+
>
|
|
2329
|
+
<svg
|
|
2330
|
+
viewBox="0 0 24 24"
|
|
2331
|
+
fill="none"
|
|
2332
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2333
|
+
>
|
|
2334
|
+
<path
|
|
2335
|
+
d="M19 7l-7 7-7-7"
|
|
2336
|
+
stroke="currentColor"
|
|
2337
|
+
strokeWidth="2"
|
|
2338
|
+
strokeLinecap="round"
|
|
2339
|
+
strokeLinejoin="round"
|
|
2340
|
+
transform="rotate(90, 12, 12)"
|
|
2341
|
+
/>
|
|
2342
|
+
</svg>
|
|
2343
|
+
Clear
|
|
2344
|
+
</button>
|
|
2345
|
+
</div>
|
|
2346
|
+
|
|
2347
|
+
<div
|
|
2348
|
+
className={`${styles.settingsFiltersContent} ${
|
|
2349
|
+
filtersCollapsed ? styles.collapsed : ''
|
|
2350
|
+
}`}
|
|
2351
|
+
>
|
|
2352
|
+
{settings.filters.map((filter, index) => (
|
|
2353
|
+
<div
|
|
2354
|
+
key={`settings-filter-${index}`}
|
|
2355
|
+
className={styles.settingsFilterItem}
|
|
2356
|
+
>
|
|
2357
|
+
<label className={styles.settingsFilterLabel}>
|
|
2358
|
+
{filter.label}
|
|
2359
|
+
</label>
|
|
2360
|
+
{filter.type === 'dropdown' &&
|
|
2361
|
+
filter.id === 'year' ? (
|
|
2362
|
+
<select
|
|
2363
|
+
className={
|
|
2364
|
+
styles.settingsFilterDropdown
|
|
2365
|
+
}
|
|
2366
|
+
value={
|
|
2367
|
+
settingsFilterValues[filter.id] ||
|
|
2368
|
+
''
|
|
2369
|
+
}
|
|
2370
|
+
onChange={(e) =>
|
|
2371
|
+
handleSettingsFilterChange(
|
|
2372
|
+
filter.id,
|
|
2373
|
+
e.target.value
|
|
2374
|
+
)
|
|
2375
|
+
}
|
|
2376
|
+
>
|
|
2377
|
+
{generateYearOptions(
|
|
2378
|
+
filter.startYear || 2000
|
|
2379
|
+
).map((option) => (
|
|
2380
|
+
<option
|
|
2381
|
+
key={option.value}
|
|
2382
|
+
value={option.value}
|
|
2383
|
+
>
|
|
2384
|
+
{option.label}
|
|
2385
|
+
</option>
|
|
2386
|
+
))}
|
|
2387
|
+
</select>
|
|
2388
|
+
) : filter.type === 'dropdown' && filter.url ? (
|
|
2389
|
+
<select
|
|
2390
|
+
className={
|
|
2391
|
+
styles.settingsFilterDropdown
|
|
2392
|
+
}
|
|
2393
|
+
value={
|
|
2394
|
+
settingsFilterValues[filter.id] ||
|
|
2395
|
+
''
|
|
2396
|
+
}
|
|
2397
|
+
onChange={(e) =>
|
|
2398
|
+
handleSettingsFilterChange(
|
|
2399
|
+
filter.id,
|
|
2400
|
+
e.target.value
|
|
2401
|
+
)
|
|
2402
|
+
}
|
|
2403
|
+
>
|
|
2404
|
+
<option value="">
|
|
2405
|
+
Select {filter.label}
|
|
2406
|
+
</option>
|
|
2407
|
+
{filterOptions[filter.id] &&
|
|
2408
|
+
filterOptions[filter.id].map(
|
|
2409
|
+
(option) => (
|
|
2410
|
+
<option
|
|
2411
|
+
key={
|
|
2412
|
+
option.value ||
|
|
2413
|
+
option.id
|
|
2414
|
+
}
|
|
2415
|
+
value={
|
|
2416
|
+
option.value ||
|
|
2417
|
+
option.id
|
|
2418
|
+
}
|
|
2419
|
+
>
|
|
2420
|
+
{option.label ||
|
|
2421
|
+
option.name ||
|
|
2422
|
+
option.title}
|
|
2423
|
+
</option>
|
|
2424
|
+
)
|
|
2425
|
+
)}
|
|
2426
|
+
</select>
|
|
2427
|
+
) : filter.type === 'dropdown' ? (
|
|
2428
|
+
<select
|
|
2429
|
+
className={
|
|
2430
|
+
styles.settingsFilterDropdown
|
|
2431
|
+
}
|
|
2432
|
+
value={
|
|
2433
|
+
settingsFilterValues[filter.id] ||
|
|
2434
|
+
''
|
|
2435
|
+
}
|
|
2436
|
+
onChange={(e) =>
|
|
2437
|
+
handleSettingsFilterChange(
|
|
2438
|
+
filter.id,
|
|
2439
|
+
e.target.value
|
|
2440
|
+
)
|
|
2441
|
+
}
|
|
2442
|
+
>
|
|
2443
|
+
<option value="">
|
|
2444
|
+
Select {filter.label}
|
|
2445
|
+
</option>
|
|
2446
|
+
{filter.options &&
|
|
2447
|
+
filter.options.map((option) => (
|
|
2448
|
+
<option
|
|
2449
|
+
key={option.value}
|
|
2450
|
+
value={option.value}
|
|
2451
|
+
>
|
|
2452
|
+
{option.label}
|
|
2453
|
+
</option>
|
|
2454
|
+
))}
|
|
2455
|
+
</select>
|
|
2456
|
+
) : filter.type === 'multiselect' ? (
|
|
2457
|
+
<div
|
|
2458
|
+
className={styles.multiselectContainer}
|
|
2459
|
+
>
|
|
2460
|
+
<MultiSelect
|
|
2461
|
+
options={
|
|
2462
|
+
filter.id === 'year' ||
|
|
2463
|
+
filter.startYear
|
|
2464
|
+
? generateYearOptions(
|
|
2465
|
+
filter.startYear ||
|
|
2466
|
+
2000
|
|
2467
|
+
)
|
|
2468
|
+
: filter.url
|
|
2469
|
+
? (
|
|
2470
|
+
filterOptions[
|
|
2471
|
+
filter.id
|
|
2472
|
+
] || []
|
|
2473
|
+
).map((option) => ({
|
|
2474
|
+
value:
|
|
2475
|
+
option.value ||
|
|
2476
|
+
option.id,
|
|
2477
|
+
label:
|
|
2478
|
+
option.label ||
|
|
2479
|
+
option.name ||
|
|
2480
|
+
option.title,
|
|
2481
|
+
}))
|
|
2482
|
+
: filter.options || []
|
|
2483
|
+
}
|
|
2484
|
+
inputValue={(() => {
|
|
2485
|
+
const currentValue =
|
|
2486
|
+
settingsFilterValues[
|
|
2487
|
+
filter.id
|
|
2488
|
+
];
|
|
2489
|
+
const availableOptions =
|
|
2490
|
+
filter.id === 'year' ||
|
|
2491
|
+
filter.startYear
|
|
2492
|
+
? generateYearOptions(
|
|
2493
|
+
filter.startYear ||
|
|
2494
|
+
2000
|
|
2495
|
+
)
|
|
2496
|
+
: filter.url
|
|
2497
|
+
? (
|
|
2498
|
+
filterOptions[
|
|
2499
|
+
filter.id
|
|
2500
|
+
] || []
|
|
2501
|
+
).map((option) => ({
|
|
2502
|
+
value:
|
|
2503
|
+
option.value ||
|
|
2504
|
+
option.id,
|
|
2505
|
+
label:
|
|
2506
|
+
option.label ||
|
|
2507
|
+
option.name ||
|
|
2508
|
+
option.title,
|
|
2509
|
+
}))
|
|
2510
|
+
: filter.options || [];
|
|
2511
|
+
|
|
2512
|
+
if (filter.isMulti === false) {
|
|
2513
|
+
// Single select mode - find the matching option object
|
|
2514
|
+
if (currentValue) {
|
|
2515
|
+
const matchingOption =
|
|
2516
|
+
availableOptions.find(
|
|
2517
|
+
(opt) =>
|
|
2518
|
+
opt.value ===
|
|
2519
|
+
currentValue ||
|
|
2520
|
+
opt.value ===
|
|
2521
|
+
currentValue.toString()
|
|
2522
|
+
);
|
|
2523
|
+
return (
|
|
2524
|
+
matchingOption ||
|
|
2525
|
+
null
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
return null;
|
|
2529
|
+
} else {
|
|
2530
|
+
// Multi select mode - return array of matching option objects
|
|
2531
|
+
if (
|
|
2532
|
+
Array.isArray(
|
|
2533
|
+
currentValue
|
|
2534
|
+
) &&
|
|
2535
|
+
currentValue.length > 0
|
|
2536
|
+
) {
|
|
2537
|
+
return currentValue
|
|
2538
|
+
.map((val) => {
|
|
2539
|
+
const matchingOption =
|
|
2540
|
+
availableOptions.find(
|
|
2541
|
+
(opt) =>
|
|
2542
|
+
opt.value ===
|
|
2543
|
+
val ||
|
|
2544
|
+
opt.value ===
|
|
2545
|
+
val.toString()
|
|
2546
|
+
);
|
|
2547
|
+
return (
|
|
2548
|
+
matchingOption || {
|
|
2549
|
+
value: val,
|
|
2550
|
+
label: val,
|
|
2551
|
+
}
|
|
2552
|
+
);
|
|
2553
|
+
})
|
|
2554
|
+
.filter(Boolean);
|
|
2555
|
+
}
|
|
2556
|
+
return [];
|
|
2557
|
+
}
|
|
2558
|
+
})()}
|
|
2559
|
+
onChange={(selectedOptions) => {
|
|
2560
|
+
if (filter.isMulti === false) {
|
|
2561
|
+
// Single select mode - extract value from the selected option object
|
|
2562
|
+
const value =
|
|
2563
|
+
selectedOptions
|
|
2564
|
+
? selectedOptions.value
|
|
2565
|
+
: '';
|
|
2566
|
+
handleSettingsFilterChange(
|
|
2567
|
+
filter.id,
|
|
2568
|
+
value
|
|
2569
|
+
);
|
|
2570
|
+
} else {
|
|
2571
|
+
// Multi select mode - extract values from array of option objects
|
|
2572
|
+
const values =
|
|
2573
|
+
Array.isArray(
|
|
2574
|
+
selectedOptions
|
|
2575
|
+
)
|
|
2576
|
+
? selectedOptions.map(
|
|
2577
|
+
(opt) =>
|
|
2578
|
+
opt.value
|
|
2579
|
+
)
|
|
2580
|
+
: [];
|
|
2581
|
+
handleSettingsFilterChange(
|
|
2582
|
+
filter.id,
|
|
2583
|
+
values
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
}}
|
|
2587
|
+
placeholder={`Select ${filter.label}`}
|
|
2588
|
+
multi={filter.isMulti !== false} // Default to true, false only if explicitly set
|
|
2589
|
+
settings={{
|
|
2590
|
+
id: filter.id,
|
|
2591
|
+
limit:
|
|
2592
|
+
filter.limit || undefined,
|
|
2593
|
+
}}
|
|
2594
|
+
/>
|
|
2595
|
+
</div>
|
|
2596
|
+
) : filter.type === 'text' ? (
|
|
2597
|
+
<input
|
|
2598
|
+
type="text"
|
|
2599
|
+
className={styles.settingsFilterInput}
|
|
2600
|
+
value={
|
|
2601
|
+
settingsFilterValues[filter.id] ||
|
|
2602
|
+
''
|
|
2603
|
+
}
|
|
2604
|
+
onChange={(e) =>
|
|
2605
|
+
handleSettingsFilterChange(
|
|
2606
|
+
filter.id,
|
|
2607
|
+
e.target.value
|
|
2608
|
+
)
|
|
2609
|
+
}
|
|
2610
|
+
placeholder={filter.placeholder || ''}
|
|
2611
|
+
/>
|
|
2612
|
+
) : null}
|
|
2613
|
+
</div>
|
|
2614
|
+
))}
|
|
2615
|
+
</div>
|
|
2616
|
+
</div>
|
|
2617
|
+
)}
|
|
2618
|
+
|
|
2619
|
+
{loading ? (
|
|
2620
|
+
<div className={styles.loadingContainer}>
|
|
2621
|
+
<div className={styles.loadingSpinner}></div>
|
|
2622
|
+
<p>Loading data...</p>
|
|
2623
|
+
</div>
|
|
2624
|
+
) : (
|
|
2625
|
+
<table className={styles.gridTable}>
|
|
2626
|
+
<thead>
|
|
2627
|
+
<tr>
|
|
2628
|
+
{/* Use gridHeaders if available, otherwise fall back to headers from columns */}
|
|
2629
|
+
{gridHeaders.length > 0
|
|
2630
|
+
? gridHeaders.map((header, index) => (
|
|
2631
|
+
<th
|
|
2632
|
+
key={`header-${index}`}
|
|
2633
|
+
className={`${styles.gridHeader} ${styles.sortableHeader}`}
|
|
2634
|
+
onClick={() =>
|
|
2635
|
+
requestSort(header.key)
|
|
2636
|
+
}
|
|
2637
|
+
style={{ cursor: 'pointer' }}
|
|
2638
|
+
>
|
|
2639
|
+
<div className={styles.headerContent}>
|
|
2640
|
+
{header.label}
|
|
2641
|
+
<span
|
|
2642
|
+
className={
|
|
2643
|
+
styles.sortIndicator
|
|
2644
|
+
}
|
|
2645
|
+
style={getSortIndicatorStyle(
|
|
2646
|
+
header.key
|
|
2647
|
+
)}
|
|
2648
|
+
>
|
|
2649
|
+
{sortConfig.key ===
|
|
2650
|
+
header.key &&
|
|
2651
|
+
sortConfig.direction === 'asc'
|
|
2652
|
+
? ' ▲'
|
|
2653
|
+
: ' ▼'}
|
|
2654
|
+
</span>
|
|
2655
|
+
</div>
|
|
2656
|
+
</th>
|
|
2657
|
+
))
|
|
2658
|
+
: headers.map((header, index) => (
|
|
2659
|
+
<th
|
|
2660
|
+
key={`header-${index}`}
|
|
2661
|
+
className={styles.gridHeader}
|
|
2662
|
+
>
|
|
2663
|
+
{header}
|
|
2664
|
+
</th>
|
|
2665
|
+
))}
|
|
2666
|
+
</tr>
|
|
2667
|
+
{/* Filter row */}
|
|
2668
|
+
{gridHeaders.some((header) => header.filter) && (
|
|
2669
|
+
<tr className={styles.filterRow}>
|
|
2670
|
+
{gridHeaders.map((header, index) => (
|
|
2671
|
+
<th
|
|
2672
|
+
key={`filter-${index}`}
|
|
2673
|
+
className={styles.filterCell}
|
|
2674
|
+
>
|
|
2675
|
+
{header.filter ? (
|
|
2676
|
+
<input
|
|
2677
|
+
type={
|
|
2678
|
+
header.filter.type ===
|
|
2679
|
+
'date'
|
|
2680
|
+
? 'date'
|
|
2681
|
+
: 'text'
|
|
2682
|
+
}
|
|
2683
|
+
className={styles.filterInput}
|
|
2684
|
+
placeholder={`Filter ${header.label}...`}
|
|
2685
|
+
value={
|
|
2686
|
+
filterValues[header.key] ||
|
|
2687
|
+
''
|
|
2688
|
+
}
|
|
2689
|
+
onChange={(e) =>
|
|
2690
|
+
handleFilterChange(
|
|
2691
|
+
header.key,
|
|
2692
|
+
e.target.value
|
|
2693
|
+
)
|
|
2694
|
+
}
|
|
2695
|
+
/>
|
|
2696
|
+
) : null}
|
|
2697
|
+
</th>
|
|
2698
|
+
))}
|
|
2699
|
+
</tr>
|
|
2700
|
+
)}
|
|
2701
|
+
</thead>
|
|
2702
|
+
<tbody>
|
|
2703
|
+
{sortedData && sortedData.length > 0 ? (
|
|
2704
|
+
sortedData.map((row, rowIndex) => (
|
|
2705
|
+
<tr
|
|
2706
|
+
key={`row-${rowIndex}`}
|
|
2707
|
+
className={`${styles.gridRow} ${
|
|
2708
|
+
onRowClick ? styles.clickableRow : ''
|
|
2709
|
+
}`}
|
|
2710
|
+
style={getCustomStyles().hoverColor}
|
|
2711
|
+
onClick={() =>
|
|
2712
|
+
handleRowClick(row, rowIndex)
|
|
2713
|
+
}
|
|
2714
|
+
>
|
|
2715
|
+
{/* Use gridHeaders if available, otherwise fall back to columns */}
|
|
2716
|
+
{gridHeaders.length > 0
|
|
2717
|
+
? gridHeaders.map(
|
|
2718
|
+
(header, colIndex) => (
|
|
2719
|
+
<td
|
|
2720
|
+
key={`cell-${rowIndex}-${colIndex}`}
|
|
2721
|
+
className={`${
|
|
2722
|
+
styles.gridCell
|
|
2723
|
+
} ${
|
|
2724
|
+
header.onClick &&
|
|
2725
|
+
!quarterCellHasContent(
|
|
2726
|
+
header,
|
|
2727
|
+
row
|
|
2728
|
+
) &&
|
|
2729
|
+
isQuarterCellClickable(
|
|
2730
|
+
header,
|
|
2731
|
+
row
|
|
2732
|
+
)
|
|
2733
|
+
? styles.clickableCell
|
|
2734
|
+
: ''
|
|
2735
|
+
}`}
|
|
2736
|
+
onClick={
|
|
2737
|
+
header.onClick &&
|
|
2738
|
+
!quarterCellHasContent(
|
|
2739
|
+
header,
|
|
2740
|
+
row
|
|
2741
|
+
) &&
|
|
2742
|
+
isQuarterCellClickable(
|
|
2743
|
+
header,
|
|
2744
|
+
row
|
|
2745
|
+
)
|
|
2746
|
+
? (e) =>
|
|
2747
|
+
handleCellClick(
|
|
2748
|
+
e,
|
|
2749
|
+
header,
|
|
2750
|
+
row
|
|
2751
|
+
)
|
|
2752
|
+
: undefined
|
|
2753
|
+
}
|
|
2754
|
+
>
|
|
2755
|
+
{quarterCellHasContent(
|
|
2756
|
+
header,
|
|
2757
|
+
row
|
|
2758
|
+
) ? (
|
|
2759
|
+
header.onClick ? (
|
|
2760
|
+
<span
|
|
2761
|
+
className={
|
|
2762
|
+
styles.cellWithValue
|
|
2763
|
+
}
|
|
2764
|
+
>
|
|
2765
|
+
{formatCellContent(
|
|
2766
|
+
row[
|
|
2767
|
+
header
|
|
2768
|
+
.key
|
|
2769
|
+
],
|
|
2770
|
+
{
|
|
2771
|
+
...header,
|
|
2772
|
+
type:
|
|
2773
|
+
header.type ||
|
|
2774
|
+
(typeof row[
|
|
2775
|
+
header
|
|
2776
|
+
.key
|
|
2777
|
+
] ===
|
|
2778
|
+
'number'
|
|
2779
|
+
? 'number'
|
|
2780
|
+
: ''),
|
|
2781
|
+
},
|
|
2782
|
+
row
|
|
2783
|
+
)}
|
|
2784
|
+
</span>
|
|
2785
|
+
) : (
|
|
2786
|
+
<span
|
|
2787
|
+
className={
|
|
2788
|
+
styles.cellContent
|
|
2789
|
+
}
|
|
2790
|
+
data-type={
|
|
2791
|
+
header.type ||
|
|
2792
|
+
(typeof row[
|
|
2793
|
+
header
|
|
2794
|
+
.key
|
|
2795
|
+
] ===
|
|
2796
|
+
'number'
|
|
2797
|
+
? 'number'
|
|
2798
|
+
: '')
|
|
2799
|
+
}
|
|
2800
|
+
>
|
|
2801
|
+
{formatCellContent(
|
|
2802
|
+
row[
|
|
2803
|
+
header
|
|
2804
|
+
.key
|
|
2805
|
+
],
|
|
2806
|
+
{
|
|
2807
|
+
...header,
|
|
2808
|
+
type:
|
|
2809
|
+
header.type ||
|
|
2810
|
+
(typeof row[
|
|
2811
|
+
header
|
|
2812
|
+
.key
|
|
2813
|
+
] ===
|
|
2814
|
+
'number'
|
|
2815
|
+
? 'number'
|
|
2816
|
+
: ''),
|
|
2817
|
+
},
|
|
2818
|
+
row
|
|
2819
|
+
)}
|
|
2820
|
+
</span>
|
|
2821
|
+
)
|
|
2822
|
+
) : header.onClick &&
|
|
2823
|
+
isQuarterCellClickable(
|
|
2824
|
+
header,
|
|
2825
|
+
row
|
|
2826
|
+
) ? (
|
|
2827
|
+
<span
|
|
2828
|
+
className={
|
|
2829
|
+
styles.placeholderText
|
|
2830
|
+
}
|
|
2831
|
+
>
|
|
2832
|
+
{header.onClick
|
|
2833
|
+
.placeholder ||
|
|
2834
|
+
'Click to set'}
|
|
2835
|
+
</span>
|
|
2836
|
+
) : (
|
|
2837
|
+
''
|
|
2838
|
+
)}
|
|
2839
|
+
</td>
|
|
2840
|
+
)
|
|
2841
|
+
)
|
|
2842
|
+
: columns.map((col, colIndex) => (
|
|
2843
|
+
<td
|
|
2844
|
+
key={`cell-${rowIndex}-${colIndex}`}
|
|
2845
|
+
className={styles.gridCell}
|
|
2846
|
+
>
|
|
2847
|
+
<span
|
|
2848
|
+
className={
|
|
2849
|
+
styles.cellContent
|
|
2850
|
+
}
|
|
2851
|
+
data-type={
|
|
2852
|
+
col.type ||
|
|
2853
|
+
(typeof row[
|
|
2854
|
+
col.name
|
|
2855
|
+
] === 'number'
|
|
2856
|
+
? 'number'
|
|
2857
|
+
: '')
|
|
2858
|
+
}
|
|
2859
|
+
>
|
|
2860
|
+
{formatCellContent(
|
|
2861
|
+
row[col.name],
|
|
2862
|
+
col
|
|
2863
|
+
)}
|
|
2864
|
+
</span>
|
|
2865
|
+
</td>
|
|
2866
|
+
))}
|
|
2867
|
+
</tr>
|
|
2868
|
+
))
|
|
2869
|
+
) : (
|
|
2870
|
+
<tr>
|
|
2871
|
+
<td
|
|
2872
|
+
colSpan={
|
|
2873
|
+
gridHeaders.length || headers.length
|
|
2874
|
+
}
|
|
2875
|
+
className={styles.noData}
|
|
2876
|
+
>
|
|
2877
|
+
No data available
|
|
2878
|
+
</td>
|
|
2879
|
+
</tr>
|
|
2880
|
+
)}
|
|
2881
|
+
</tbody>
|
|
2882
|
+
</table>
|
|
2883
|
+
)}
|
|
2884
|
+
</div>
|
|
2885
|
+
);
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2888
|
+
export default GenericGrid;
|