@visns-studio/visns-components 5.11.1 → 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/package.json +2 -2
- 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 +59 -16
- 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 +1622 -268
- package/src/components/crm/generic/GenericReport.jsx +966 -646
- package/src/components/crm/generic/GenericReportForm.jsx +1 -1
- 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 +107 -1
- package/src/components/crm/generic/styles/AlternativeActionModal.css +127 -0
- package/src/components/crm/generic/styles/DateRangeSelectorModal.css +115 -0
- package/src/components/crm/generic/styles/GenericIndex.module.scss +206 -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
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useCallback,
|
|
3
|
-
useEffect,
|
|
4
|
-
useMemo,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
} from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
2
|
import { toast } from 'react-toastify';
|
|
9
3
|
import parse from 'html-react-parser';
|
|
10
4
|
import moment from 'moment';
|
|
11
5
|
import numeral from 'numeral';
|
|
6
|
+
import { Calendar, X, Mail, FileText, Users } from 'lucide-react';
|
|
7
|
+
import { Tooltip } from 'react-tooltip';
|
|
12
8
|
|
|
13
9
|
import CustomFetch from '../Fetch';
|
|
14
10
|
import MultiSelect from '../MultiSelect';
|
|
15
11
|
import { showDatePickerDialog } from './DatePickerDialog';
|
|
16
12
|
import { showContactSelectorModal } from './ContactSelectorModal';
|
|
13
|
+
import { showDateRangeSelectorModal } from './DateRangeSelectorModal';
|
|
14
|
+
import { showAlternativeActionModal } from './AlternativeActionModal';
|
|
15
|
+
import { showReasonCollectorModal } from './ReasonCollectorModal';
|
|
17
16
|
import styles from './styles/GenericIndex.module.scss';
|
|
18
17
|
|
|
19
18
|
// ContactTooltip component for enhanced contact display
|
|
@@ -25,11 +24,11 @@ const ContactTooltip = ({ contacts, children }) => {
|
|
|
25
24
|
|
|
26
25
|
const handleMouseEnter = (e) => {
|
|
27
26
|
if (!contacts || contacts.length === 0) return;
|
|
28
|
-
|
|
27
|
+
|
|
29
28
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
30
29
|
setPosition({
|
|
31
30
|
x: rect.left + rect.width / 2,
|
|
32
|
-
y: rect.top - 10
|
|
31
|
+
y: rect.top - 10,
|
|
33
32
|
});
|
|
34
33
|
setIsVisible(true);
|
|
35
34
|
};
|
|
@@ -41,8 +40,9 @@ const ContactTooltip = ({ contacts, children }) => {
|
|
|
41
40
|
const formatContact = (contact) => {
|
|
42
41
|
const name = contact.name || 'Unknown Contact';
|
|
43
42
|
const email = contact.email || '';
|
|
44
|
-
const type =
|
|
45
|
-
|
|
43
|
+
const type =
|
|
44
|
+
contact.type === 'contact' ? 'Existing Contact' : 'Manual Entry';
|
|
45
|
+
|
|
46
46
|
return { name, email, type };
|
|
47
47
|
};
|
|
48
48
|
|
|
@@ -56,7 +56,7 @@ const ContactTooltip = ({ contacts, children }) => {
|
|
|
56
56
|
>
|
|
57
57
|
{children}
|
|
58
58
|
</div>
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
{isVisible && contacts && contacts.length > 0 && (
|
|
61
61
|
<div
|
|
62
62
|
ref={tooltipRef}
|
|
@@ -70,16 +70,31 @@ const ContactTooltip = ({ contacts, children }) => {
|
|
|
70
70
|
}}
|
|
71
71
|
>
|
|
72
72
|
<div className={styles.contactTooltipHeader}>
|
|
73
|
-
<strong>
|
|
73
|
+
<strong>
|
|
74
|
+
{contacts.length} Contact
|
|
75
|
+
{contacts.length !== 1 ? 's' : ''}
|
|
76
|
+
</strong>
|
|
74
77
|
</div>
|
|
75
78
|
<div className={styles.contactTooltipBody}>
|
|
76
79
|
{contacts.map((contact, index) => {
|
|
77
|
-
const { name, email, type } =
|
|
80
|
+
const { name, email, type } =
|
|
81
|
+
formatContact(contact);
|
|
78
82
|
return (
|
|
79
|
-
<div
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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>
|
|
83
98
|
</div>
|
|
84
99
|
);
|
|
85
100
|
})}
|
|
@@ -91,7 +106,13 @@ const ContactTooltip = ({ contacts, children }) => {
|
|
|
91
106
|
);
|
|
92
107
|
};
|
|
93
108
|
|
|
94
|
-
const GenericGrid = ({
|
|
109
|
+
const GenericGrid = ({
|
|
110
|
+
config,
|
|
111
|
+
userProfile,
|
|
112
|
+
data,
|
|
113
|
+
onDataChange,
|
|
114
|
+
onRowClick,
|
|
115
|
+
}) => {
|
|
95
116
|
const [gridData, setGridData] = useState([]);
|
|
96
117
|
const [sortedData, setSortedData] = useState([]);
|
|
97
118
|
const [, setFilteredData] = useState([]);
|
|
@@ -128,15 +149,40 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
128
149
|
|
|
129
150
|
// Format cell content based on column type or value type
|
|
130
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
|
+
|
|
131
163
|
if (value === null || value === undefined) return '';
|
|
132
164
|
if (value === 0) return '0'; // Explicitly handle zero values
|
|
133
165
|
if (value === '') return '';
|
|
134
166
|
|
|
135
167
|
// Check if this is a date field with contacts (survey workflow)
|
|
136
|
-
if (
|
|
168
|
+
if (
|
|
169
|
+
column.type === 'date' &&
|
|
170
|
+
column.onClick?.type === 'date_with_contacts' &&
|
|
171
|
+
row
|
|
172
|
+
) {
|
|
137
173
|
return formatDateWithContacts(value, column, row);
|
|
138
174
|
}
|
|
139
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
|
+
|
|
140
186
|
// Try to infer the type if not provided
|
|
141
187
|
const valueType = column.type || typeof value;
|
|
142
188
|
|
|
@@ -214,7 +260,7 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
214
260
|
const formatDateWithContacts = (dateValue, column, row) => {
|
|
215
261
|
const contactsField = column.key + '_contacts';
|
|
216
262
|
const contacts = row[contactsField];
|
|
217
|
-
|
|
263
|
+
|
|
218
264
|
// Format the date
|
|
219
265
|
const formattedDate = moment(dateValue).isValid()
|
|
220
266
|
? moment(dateValue).format(column.format || 'DD/MM/YYYY')
|
|
@@ -231,13 +277,501 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
231
277
|
<div className={styles.dateValue}>{formattedDate}</div>
|
|
232
278
|
<ContactTooltip contacts={contacts}>
|
|
233
279
|
<div className={styles.contactsSummary}>
|
|
234
|
-
📧 {contacts.length} contact
|
|
280
|
+
📧 {contacts.length} contact
|
|
281
|
+
{contacts.length !== 1 ? 's' : ''}
|
|
235
282
|
</div>
|
|
236
283
|
</ContactTooltip>
|
|
237
284
|
</div>
|
|
238
285
|
);
|
|
239
286
|
};
|
|
240
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
|
+
};
|
|
241
775
|
|
|
242
776
|
// State for headers from API response
|
|
243
777
|
const [gridHeaders, setGridHeaders] = useState([]);
|
|
@@ -439,7 +973,7 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
439
973
|
// Check if we should use static data instead of fetching
|
|
440
974
|
if (settings?.useStaticData && settings?.staticData) {
|
|
441
975
|
const staticData = settings.staticData;
|
|
442
|
-
|
|
976
|
+
|
|
443
977
|
if (staticData.header && staticData.rows) {
|
|
444
978
|
// Set the headers from the static data
|
|
445
979
|
const headerEntries = Object.entries(staticData.header);
|
|
@@ -467,7 +1001,7 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
467
1001
|
const newData = staticData.rows || [];
|
|
468
1002
|
setGridData(newData);
|
|
469
1003
|
setSortedData(newData);
|
|
470
|
-
|
|
1004
|
+
|
|
471
1005
|
// Notify parent component of data change
|
|
472
1006
|
if (onDataChange) {
|
|
473
1007
|
onDataChange(newData);
|
|
@@ -540,7 +1074,7 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
540
1074
|
const newData = response.data.data.rows || [];
|
|
541
1075
|
setGridData(newData);
|
|
542
1076
|
setSortedData(newData);
|
|
543
|
-
|
|
1077
|
+
|
|
544
1078
|
// Notify parent component of data change
|
|
545
1079
|
if (onDataChange) {
|
|
546
1080
|
onDataChange(newData);
|
|
@@ -550,7 +1084,7 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
550
1084
|
const newData = response.data.data.data || [];
|
|
551
1085
|
setGridData(newData);
|
|
552
1086
|
setSortedData(newData);
|
|
553
|
-
|
|
1087
|
+
|
|
554
1088
|
// Notify parent component of data change
|
|
555
1089
|
if (onDataChange) {
|
|
556
1090
|
onDataChange(newData);
|
|
@@ -600,9 +1134,9 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
600
1134
|
try {
|
|
601
1135
|
const response = await CustomFetch(filter.url, 'POST', {});
|
|
602
1136
|
if (response.data && response.data.data) {
|
|
603
|
-
setFilterOptions(prev => ({
|
|
1137
|
+
setFilterOptions((prev) => ({
|
|
604
1138
|
...prev,
|
|
605
|
-
[filter.id]: response.data.data
|
|
1139
|
+
[filter.id]: response.data.data,
|
|
606
1140
|
}));
|
|
607
1141
|
}
|
|
608
1142
|
} catch (error) {
|
|
@@ -617,20 +1151,30 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
617
1151
|
const initialValues = {};
|
|
618
1152
|
|
|
619
1153
|
settings.filters.forEach((filter) => {
|
|
620
|
-
if (
|
|
1154
|
+
if (
|
|
1155
|
+
(filter.type === 'dropdown' ||
|
|
1156
|
+
filter.type === 'multiselect') &&
|
|
1157
|
+
(filter.id === 'year' || filter.startYear)
|
|
1158
|
+
) {
|
|
621
1159
|
// For year filters with 'now' value, set to current year
|
|
622
1160
|
if (filter.value === 'now') {
|
|
623
1161
|
const currentYear = new Date().getFullYear().toString();
|
|
624
1162
|
// For multiselect, check isMulti to determine if it should be array or single value
|
|
625
1163
|
if (filter.type === 'multiselect') {
|
|
626
|
-
const value =
|
|
1164
|
+
const value =
|
|
1165
|
+
filter.isMulti === false
|
|
1166
|
+
? currentYear
|
|
1167
|
+
: [currentYear];
|
|
627
1168
|
initialValues[filter.id] = value;
|
|
628
1169
|
} else {
|
|
629
1170
|
initialValues[filter.id] = currentYear;
|
|
630
1171
|
}
|
|
631
1172
|
} else {
|
|
632
1173
|
if (filter.type === 'multiselect') {
|
|
633
|
-
initialValues[filter.id] =
|
|
1174
|
+
initialValues[filter.id] =
|
|
1175
|
+
filter.isMulti === false
|
|
1176
|
+
? filter.value || ''
|
|
1177
|
+
: filter.value || [];
|
|
634
1178
|
} else {
|
|
635
1179
|
initialValues[filter.id] = filter.value || '';
|
|
636
1180
|
}
|
|
@@ -639,27 +1183,46 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
639
1183
|
// For multiselect, handle 'now' value and initialize based on isMulti
|
|
640
1184
|
if (filter.value === 'now') {
|
|
641
1185
|
const currentYear = new Date().getFullYear().toString();
|
|
642
|
-
initialValues[filter.id] =
|
|
1186
|
+
initialValues[filter.id] =
|
|
1187
|
+
filter.isMulti === false
|
|
1188
|
+
? currentYear
|
|
1189
|
+
: [currentYear];
|
|
643
1190
|
} else {
|
|
644
1191
|
if (filter.isMulti === false) {
|
|
645
1192
|
// Single select mode - use single value
|
|
646
|
-
initialValues[filter.id] = Array.isArray(
|
|
1193
|
+
initialValues[filter.id] = Array.isArray(
|
|
1194
|
+
filter.value
|
|
1195
|
+
)
|
|
1196
|
+
? filter.value[0] || ''
|
|
1197
|
+
: filter.value || '';
|
|
647
1198
|
} else {
|
|
648
1199
|
// Multi select mode - use array
|
|
649
|
-
initialValues[filter.id] = Array.isArray(
|
|
1200
|
+
initialValues[filter.id] = Array.isArray(
|
|
1201
|
+
filter.value
|
|
1202
|
+
)
|
|
1203
|
+
? filter.value
|
|
1204
|
+
: filter.value
|
|
1205
|
+
? [filter.value]
|
|
1206
|
+
: [];
|
|
650
1207
|
}
|
|
651
1208
|
}
|
|
652
1209
|
} else {
|
|
653
1210
|
// For other filter types
|
|
654
1211
|
if (filter.value === 'now') {
|
|
655
|
-
initialValues[filter.id] = new Date()
|
|
1212
|
+
initialValues[filter.id] = new Date()
|
|
1213
|
+
.getFullYear()
|
|
1214
|
+
.toString();
|
|
656
1215
|
} else {
|
|
657
1216
|
initialValues[filter.id] = filter.value || '';
|
|
658
1217
|
}
|
|
659
1218
|
}
|
|
660
1219
|
|
|
661
1220
|
// Fetch options for dropdowns with URL
|
|
662
|
-
if (
|
|
1221
|
+
if (
|
|
1222
|
+
(filter.type === 'dropdown' ||
|
|
1223
|
+
filter.type === 'multiselect') &&
|
|
1224
|
+
filter.url
|
|
1225
|
+
) {
|
|
663
1226
|
fetchFilterOptions(filter);
|
|
664
1227
|
}
|
|
665
1228
|
});
|
|
@@ -679,13 +1242,13 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
679
1242
|
// Update fetch params with the new filter value
|
|
680
1243
|
if (settings?.fetch?.params) {
|
|
681
1244
|
let processedValue = value;
|
|
682
|
-
|
|
1245
|
+
|
|
683
1246
|
// For multiselect, convert array to appropriate format for API
|
|
684
1247
|
if (Array.isArray(value)) {
|
|
685
1248
|
// Keep as array - most APIs expect this format for multiple values
|
|
686
1249
|
processedValue = value;
|
|
687
1250
|
}
|
|
688
|
-
|
|
1251
|
+
|
|
689
1252
|
const updatedParams = {
|
|
690
1253
|
...settings.fetch.params,
|
|
691
1254
|
[filterId]: processedValue,
|
|
@@ -729,207 +1292,847 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
729
1292
|
};
|
|
730
1293
|
|
|
731
1294
|
// Handle row click
|
|
732
|
-
const handleRowClick = useCallback(
|
|
733
|
-
|
|
734
|
-
onRowClick
|
|
735
|
-
|
|
736
|
-
|
|
1295
|
+
const handleRowClick = useCallback(
|
|
1296
|
+
(row, rowIndex) => {
|
|
1297
|
+
if (onRowClick) {
|
|
1298
|
+
onRowClick(row, rowIndex);
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
[onRowClick]
|
|
1302
|
+
);
|
|
737
1303
|
|
|
738
1304
|
// Handle cell click with support for date picker and contact selection
|
|
739
|
-
const handleCellClick = useCallback(
|
|
740
|
-
e
|
|
741
|
-
|
|
742
|
-
try {
|
|
743
|
-
// Debug logging to check configuration
|
|
744
|
-
const dateOnlyConfig = header.onClick.dateOnly || userProfile?.settings?.dateOnly || false;
|
|
745
|
-
console.log('Cell click configuration:', {
|
|
746
|
-
column: header.label,
|
|
747
|
-
clickType: header.onClick.type || 'date',
|
|
748
|
-
hasContactSelection: !!header.onClick.contactSelection,
|
|
749
|
-
headerDateOnly: header.onClick.dateOnly,
|
|
750
|
-
userProfileDateOnly: userProfile?.settings?.dateOnly,
|
|
751
|
-
finalDateOnly: dateOnlyConfig
|
|
752
|
-
});
|
|
1305
|
+
const handleCellClick = useCallback(
|
|
1306
|
+
async (e, header, row) => {
|
|
1307
|
+
e.stopPropagation(); // Prevent row click
|
|
753
1308
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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');
|
|
763
1372
|
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
}, [settings, settingsFilterValues, onDataChange, userProfile]);
|
|
1373
|
+
},
|
|
1374
|
+
[settings, settingsFilterValues, onDataChange, userProfile]
|
|
1375
|
+
);
|
|
769
1376
|
|
|
770
1377
|
// Handle simple date selection (existing functionality)
|
|
771
|
-
const handleSimpleDateSelection = useCallback(
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
+
);
|
|
792
1414
|
|
|
793
1415
|
// Handle date selection with contact selection flow
|
|
794
|
-
const handleDateWithContactsFlow = useCallback(
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
+
);
|
|
848
1513
|
|
|
849
1514
|
// Submit date update only
|
|
850
|
-
const submitDateUpdate = useCallback(
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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);
|
|
854
1520
|
|
|
855
|
-
|
|
856
|
-
|
|
1521
|
+
// Show loading toast
|
|
1522
|
+
toast.info('Updating...', { autoClose: 1000 });
|
|
857
1523
|
|
|
858
|
-
|
|
859
|
-
|
|
1524
|
+
// Extract field and value from params
|
|
1525
|
+
const { field, ...otherParams } = header.onClick.params;
|
|
860
1526
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
+
};
|
|
866
1532
|
|
|
867
|
-
|
|
868
|
-
|
|
1533
|
+
// Set the field as the key with the selected date value
|
|
1534
|
+
params[field] = selectedDateISO;
|
|
869
1535
|
|
|
870
|
-
|
|
871
|
-
|
|
1536
|
+
// Make the API call
|
|
1537
|
+
const response = await CustomFetch(
|
|
1538
|
+
url,
|
|
1539
|
+
header.onClick.method,
|
|
1540
|
+
params
|
|
1541
|
+
);
|
|
872
1542
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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');
|
|
878
1554
|
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
883
|
-
}, []);
|
|
1555
|
+
},
|
|
1556
|
+
[]
|
|
1557
|
+
);
|
|
884
1558
|
|
|
885
1559
|
// Submit date and contacts update
|
|
886
|
-
const submitDateAndContactsUpdate = useCallback(
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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);
|
|
890
1565
|
|
|
891
|
-
|
|
892
|
-
|
|
1566
|
+
// Show loading toast
|
|
1567
|
+
toast.info('Saving date and contacts...', { autoClose: 1000 });
|
|
893
1568
|
|
|
894
|
-
|
|
895
|
-
|
|
1569
|
+
// Extract field and value from params
|
|
1570
|
+
const { field, ...otherParams } = header.onClick.params;
|
|
896
1571
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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;
|
|
902
1580
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
// Set the contacts field (field name + '_contacts')
|
|
907
|
-
const contactsField = field + '_contacts';
|
|
908
|
-
params[contactsField] = selectedContacts;
|
|
1581
|
+
// Set the contacts field (field name + '_contacts')
|
|
1582
|
+
const contactsField = field + '_contacts';
|
|
1583
|
+
params[contactsField] = selectedContacts;
|
|
909
1584
|
|
|
910
|
-
|
|
1585
|
+
console.log('Submitting date and contacts:', params);
|
|
911
1586
|
|
|
912
|
-
|
|
913
|
-
|
|
1587
|
+
// Make the API call
|
|
1588
|
+
const response = await CustomFetch(
|
|
1589
|
+
url,
|
|
1590
|
+
header.onClick.method,
|
|
1591
|
+
params
|
|
1592
|
+
);
|
|
914
1593
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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');
|
|
920
1605
|
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
+
);
|
|
926
2129
|
|
|
927
2130
|
// Refresh grid data helper function
|
|
928
2131
|
const refreshGridData = useCallback(async () => {
|
|
929
2132
|
try {
|
|
930
2133
|
// Refresh the data
|
|
931
2134
|
prevFetchParamsRef.current = null;
|
|
932
|
-
|
|
2135
|
+
|
|
933
2136
|
// Force a re-fetch
|
|
934
2137
|
const fetchUrl = settings.fetch.url;
|
|
935
2138
|
const fetchMethod = settings.fetch.method || 'POST';
|
|
@@ -939,9 +2142,16 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
939
2142
|
};
|
|
940
2143
|
|
|
941
2144
|
setLoading(true);
|
|
942
|
-
const fetchResponse = await CustomFetch(
|
|
2145
|
+
const fetchResponse = await CustomFetch(
|
|
2146
|
+
fetchUrl,
|
|
2147
|
+
fetchMethod,
|
|
2148
|
+
fetchParams
|
|
2149
|
+
);
|
|
943
2150
|
if (fetchResponse.data.data) {
|
|
944
|
-
if (
|
|
2151
|
+
if (
|
|
2152
|
+
fetchResponse.data.data.header &&
|
|
2153
|
+
fetchResponse.data.data.rows
|
|
2154
|
+
) {
|
|
945
2155
|
const newData = fetchResponse.data.data.rows || [];
|
|
946
2156
|
setGridData(newData);
|
|
947
2157
|
setSortedData(newData);
|
|
@@ -1056,34 +2266,57 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
1056
2266
|
const initialValues = {};
|
|
1057
2267
|
settings.filters.forEach((filter) => {
|
|
1058
2268
|
if (
|
|
1059
|
-
(filter.type === 'dropdown' ||
|
|
1060
|
-
|
|
2269
|
+
(filter.type === 'dropdown' ||
|
|
2270
|
+
filter.type === 'multiselect') &&
|
|
2271
|
+
(filter.id === 'year' ||
|
|
2272
|
+
filter.startYear) &&
|
|
1061
2273
|
filter.value === 'now'
|
|
1062
2274
|
) {
|
|
1063
2275
|
const currentYear = new Date()
|
|
1064
2276
|
.getFullYear()
|
|
1065
2277
|
.toString();
|
|
1066
2278
|
if (filter.type === 'multiselect') {
|
|
1067
|
-
initialValues[filter.id] =
|
|
2279
|
+
initialValues[filter.id] =
|
|
2280
|
+
filter.isMulti === false
|
|
2281
|
+
? currentYear
|
|
2282
|
+
: [currentYear];
|
|
1068
2283
|
} else {
|
|
1069
|
-
initialValues[filter.id] =
|
|
2284
|
+
initialValues[filter.id] =
|
|
2285
|
+
currentYear;
|
|
1070
2286
|
}
|
|
1071
2287
|
} else if (filter.type === 'multiselect') {
|
|
1072
2288
|
if (filter.value === 'now') {
|
|
1073
|
-
const currentYear = new Date()
|
|
1074
|
-
|
|
2289
|
+
const currentYear = new Date()
|
|
2290
|
+
.getFullYear()
|
|
2291
|
+
.toString();
|
|
2292
|
+
initialValues[filter.id] =
|
|
2293
|
+
filter.isMulti === false
|
|
2294
|
+
? currentYear
|
|
2295
|
+
: [currentYear];
|
|
1075
2296
|
} else {
|
|
1076
2297
|
if (filter.isMulti === false) {
|
|
1077
|
-
initialValues[filter.id] =
|
|
2298
|
+
initialValues[filter.id] =
|
|
2299
|
+
Array.isArray(filter.value)
|
|
2300
|
+
? filter.value[0] || ''
|
|
2301
|
+
: filter.value || '';
|
|
1078
2302
|
} else {
|
|
1079
|
-
initialValues[filter.id] =
|
|
2303
|
+
initialValues[filter.id] =
|
|
2304
|
+
Array.isArray(filter.value)
|
|
2305
|
+
? filter.value
|
|
2306
|
+
: filter.value
|
|
2307
|
+
? [filter.value]
|
|
2308
|
+
: [];
|
|
1080
2309
|
}
|
|
1081
2310
|
}
|
|
1082
2311
|
} else {
|
|
1083
2312
|
if (filter.value === 'now') {
|
|
1084
|
-
initialValues[filter.id] =
|
|
2313
|
+
initialValues[filter.id] =
|
|
2314
|
+
new Date()
|
|
2315
|
+
.getFullYear()
|
|
2316
|
+
.toString();
|
|
1085
2317
|
} else {
|
|
1086
|
-
initialValues[filter.id] =
|
|
2318
|
+
initialValues[filter.id] =
|
|
2319
|
+
filter.value || '';
|
|
1087
2320
|
}
|
|
1088
2321
|
}
|
|
1089
2322
|
});
|
|
@@ -1168,16 +2401,28 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
1168
2401
|
)
|
|
1169
2402
|
}
|
|
1170
2403
|
>
|
|
1171
|
-
<option value="">
|
|
2404
|
+
<option value="">
|
|
2405
|
+
Select {filter.label}
|
|
2406
|
+
</option>
|
|
1172
2407
|
{filterOptions[filter.id] &&
|
|
1173
|
-
filterOptions[filter.id].map(
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
+
)}
|
|
1181
2426
|
</select>
|
|
1182
2427
|
) : filter.type === 'dropdown' ? (
|
|
1183
2428
|
<select
|
|
@@ -1195,7 +2440,9 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
1195
2440
|
)
|
|
1196
2441
|
}
|
|
1197
2442
|
>
|
|
1198
|
-
<option value="">
|
|
2443
|
+
<option value="">
|
|
2444
|
+
Select {filter.label}
|
|
2445
|
+
</option>
|
|
1199
2446
|
{filter.options &&
|
|
1200
2447
|
filter.options.map((option) => (
|
|
1201
2448
|
<option
|
|
@@ -1207,43 +2454,104 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
1207
2454
|
))}
|
|
1208
2455
|
</select>
|
|
1209
2456
|
) : filter.type === 'multiselect' ? (
|
|
1210
|
-
<div
|
|
2457
|
+
<div
|
|
2458
|
+
className={styles.multiselectContainer}
|
|
2459
|
+
>
|
|
1211
2460
|
<MultiSelect
|
|
1212
2461
|
options={
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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 || []
|
|
1221
2483
|
}
|
|
1222
2484
|
inputValue={(() => {
|
|
1223
|
-
const currentValue =
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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 || [];
|
|
1232
2511
|
|
|
1233
2512
|
if (filter.isMulti === false) {
|
|
1234
2513
|
// Single select mode - find the matching option object
|
|
1235
2514
|
if (currentValue) {
|
|
1236
|
-
const matchingOption =
|
|
1237
|
-
|
|
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
|
+
);
|
|
1238
2527
|
}
|
|
1239
2528
|
return null;
|
|
1240
2529
|
} else {
|
|
1241
2530
|
// Multi select mode - return array of matching option objects
|
|
1242
|
-
if (
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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);
|
|
1247
2555
|
}
|
|
1248
2556
|
return [];
|
|
1249
2557
|
}
|
|
@@ -1251,21 +2559,37 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
1251
2559
|
onChange={(selectedOptions) => {
|
|
1252
2560
|
if (filter.isMulti === false) {
|
|
1253
2561
|
// Single select mode - extract value from the selected option object
|
|
1254
|
-
const value =
|
|
1255
|
-
|
|
2562
|
+
const value =
|
|
2563
|
+
selectedOptions
|
|
2564
|
+
? selectedOptions.value
|
|
2565
|
+
: '';
|
|
2566
|
+
handleSettingsFilterChange(
|
|
2567
|
+
filter.id,
|
|
2568
|
+
value
|
|
2569
|
+
);
|
|
1256
2570
|
} else {
|
|
1257
2571
|
// Multi select mode - extract values from array of option objects
|
|
1258
|
-
const values =
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
+
);
|
|
1262
2585
|
}
|
|
1263
2586
|
}}
|
|
1264
2587
|
placeholder={`Select ${filter.label}`}
|
|
1265
2588
|
multi={filter.isMulti !== false} // Default to true, false only if explicitly set
|
|
1266
2589
|
settings={{
|
|
1267
2590
|
id: filter.id,
|
|
1268
|
-
limit:
|
|
2591
|
+
limit:
|
|
2592
|
+
filter.limit || undefined,
|
|
1269
2593
|
}}
|
|
1270
2594
|
/>
|
|
1271
2595
|
</div>
|
|
@@ -1380,9 +2704,13 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
1380
2704
|
sortedData.map((row, rowIndex) => (
|
|
1381
2705
|
<tr
|
|
1382
2706
|
key={`row-${rowIndex}`}
|
|
1383
|
-
className={`${styles.gridRow} ${
|
|
2707
|
+
className={`${styles.gridRow} ${
|
|
2708
|
+
onRowClick ? styles.clickableRow : ''
|
|
2709
|
+
}`}
|
|
1384
2710
|
style={getCustomStyles().hoverColor}
|
|
1385
|
-
onClick={() =>
|
|
2711
|
+
onClick={() =>
|
|
2712
|
+
handleRowClick(row, rowIndex)
|
|
2713
|
+
}
|
|
1386
2714
|
>
|
|
1387
2715
|
{/* Use gridHeaders if available, otherwise fall back to columns */}
|
|
1388
2716
|
{gridHeaders.length > 0
|
|
@@ -1394,18 +2722,40 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
1394
2722
|
styles.gridCell
|
|
1395
2723
|
} ${
|
|
1396
2724
|
header.onClick &&
|
|
1397
|
-
!
|
|
2725
|
+
!quarterCellHasContent(
|
|
2726
|
+
header,
|
|
2727
|
+
row
|
|
2728
|
+
) &&
|
|
2729
|
+
isQuarterCellClickable(
|
|
2730
|
+
header,
|
|
2731
|
+
row
|
|
2732
|
+
)
|
|
1398
2733
|
? styles.clickableCell
|
|
1399
2734
|
: ''
|
|
1400
2735
|
}`}
|
|
1401
2736
|
onClick={
|
|
1402
2737
|
header.onClick &&
|
|
1403
|
-
!
|
|
1404
|
-
|
|
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
|
+
)
|
|
1405
2752
|
: undefined
|
|
1406
2753
|
}
|
|
1407
2754
|
>
|
|
1408
|
-
{
|
|
2755
|
+
{quarterCellHasContent(
|
|
2756
|
+
header,
|
|
2757
|
+
row
|
|
2758
|
+
) ? (
|
|
1409
2759
|
header.onClick ? (
|
|
1410
2760
|
<span
|
|
1411
2761
|
className={
|
|
@@ -1469,7 +2819,11 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
1469
2819
|
)}
|
|
1470
2820
|
</span>
|
|
1471
2821
|
)
|
|
1472
|
-
) : header.onClick
|
|
2822
|
+
) : header.onClick &&
|
|
2823
|
+
isQuarterCellClickable(
|
|
2824
|
+
header,
|
|
2825
|
+
row
|
|
2826
|
+
) ? (
|
|
1473
2827
|
<span
|
|
1474
2828
|
className={
|
|
1475
2829
|
styles.placeholderText
|
|
@@ -1531,4 +2885,4 @@ const GenericGrid = ({ config, userProfile, data, onDataChange, onRowClick }) =>
|
|
|
1531
2885
|
);
|
|
1532
2886
|
};
|
|
1533
2887
|
|
|
1534
|
-
export default GenericGrid;
|
|
2888
|
+
export default GenericGrid;
|