@visns-studio/visns-components 5.11.1 → 5.11.3

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.
Files changed (60) hide show
  1. package/package.json +2 -2
  2. package/src/components/cms/DataGrid.jsx +8 -8
  3. package/src/components/cms/DropZone.jsx +1 -1
  4. package/src/components/cms/Form.jsx +1 -1
  5. package/src/components/cms/Gallery.jsx +1 -1
  6. package/src/components/cms/sorting/Item.jsx +1 -1
  7. package/src/components/crm/AssociationManager.jsx +431 -0
  8. package/src/components/crm/Autocomplete.jsx +3 -3
  9. package/src/components/crm/Breadcrumb.jsx +4 -4
  10. package/src/components/crm/BusinessCardOcr.jsx +681 -95
  11. package/src/components/crm/ClientAssociationManager.jsx +362 -0
  12. package/src/components/crm/DataGrid.jsx +34 -32
  13. package/src/components/crm/Field.jsx +12 -12
  14. package/src/components/crm/Form.jsx +2 -2
  15. package/src/components/crm/MergeEntity.jsx +1049 -0
  16. package/src/components/crm/MultiSelect.jsx +14 -3
  17. package/src/components/crm/Navigation.jsx +14 -15
  18. package/src/components/crm/Notification.jsx +2 -2
  19. package/src/components/crm/QuickAction.jsx +3 -3
  20. package/src/components/crm/SectionHeader.jsx +1 -1
  21. package/src/components/crm/SwitchAccount.jsx +4 -4
  22. package/src/components/crm/auth/ClientLogin.jsx +2 -2
  23. package/src/components/crm/auth/ClientOTPVerify.jsx +2 -2
  24. package/src/components/crm/auth/Login.jsx +3 -3
  25. package/src/components/crm/auth/Reset.jsx +2 -2
  26. package/src/components/crm/auth/TwoFactorAuth.jsx +2 -2
  27. package/src/components/crm/cells/CellWithTooltip.jsx +35 -28
  28. package/src/components/crm/columns/ColumnRenderers.jsx +320 -259
  29. package/src/components/crm/controls/DataGridSearch.jsx +5 -5
  30. package/src/components/crm/generic/AlternativeActionModal.jsx +100 -0
  31. package/src/components/crm/generic/ContactSelectorModal.jsx +59 -16
  32. package/src/components/crm/generic/DateRangeSelectorModal.jsx +181 -0
  33. package/src/components/crm/generic/GenericClientPortal.jsx +1 -1
  34. package/src/components/crm/generic/GenericDashboard.jsx +4 -4
  35. package/src/components/crm/generic/GenericDetail.jsx +31 -6
  36. package/src/components/crm/generic/GenericDynamic.jsx +3 -3
  37. package/src/components/crm/generic/GenericEditableTable.jsx +4 -4
  38. package/src/components/crm/generic/GenericFormBuilder.jsx +6 -6
  39. package/src/components/crm/generic/GenericGrid.jsx +1622 -268
  40. package/src/components/crm/generic/GenericReport.jsx +966 -646
  41. package/src/components/crm/generic/GenericReportForm.jsx +1 -1
  42. package/src/components/crm/generic/NotificationList.jsx +1 -1
  43. package/src/components/crm/generic/ReasonCollectorModal.jsx +194 -0
  44. package/src/components/crm/generic/SortableQuoteItems.jsx +3 -3
  45. package/src/components/crm/generic/shared/formatters.js +107 -1
  46. package/src/components/crm/generic/styles/AlternativeActionModal.css +127 -0
  47. package/src/components/crm/generic/styles/DateRangeSelectorModal.css +115 -0
  48. package/src/components/crm/generic/styles/GenericIndex.module.scss +206 -0
  49. package/src/components/crm/generic/styles/GenericReport.module.scss +96 -0
  50. package/src/components/crm/generic/styles/ReasonCollectorModal.css +217 -0
  51. package/src/components/crm/modals/GalleryModal.jsx +2 -2
  52. package/src/components/crm/sorting/Item.jsx +3 -3
  53. package/src/components/crm/styles/AssociationManager.module.scss +364 -0
  54. package/src/components/crm/styles/BusinessCardOcr.module.scss +197 -4
  55. package/src/components/crm/styles/ClientAssociationManager.module.scss +290 -0
  56. package/src/components/crm/styles/MergeEntity.module.scss +690 -0
  57. package/src/components/crm/styles/MultiSelect.module.scss +18 -0
  58. package/src/components/crm/styles/Navigation.module.scss +68 -8
  59. package/src/components/crm/styles/global-datagrid.css +9 -0
  60. package/src/index.js +6 -0
@@ -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 = contact.type === 'contact' ? 'Existing Contact' : 'Manual Entry';
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>{contacts.length} Contact{contacts.length !== 1 ? 's' : ''}</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 } = formatContact(contact);
80
+ const { name, email, type } =
81
+ formatContact(contact);
78
82
  return (
79
- <div key={index} className={styles.contactTooltipItem}>
80
- <div className={styles.contactName}>{name}</div>
81
- {email && <div className={styles.contactEmail}>{email}</div>}
82
- <div className={styles.contactType}>{type}</div>
83
+ <div
84
+ 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 = ({ config, userProfile, data, onDataChange, onRowClick }) => {
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 (column.type === 'date' && column.onClick?.type === 'date_with_contacts' && row) {
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{contacts.length !== 1 ? 's' : ''}
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 ((filter.type === 'dropdown' || filter.type === 'multiselect') && (filter.id === 'year' || filter.startYear)) {
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 = filter.isMulti === false ? currentYear : [currentYear];
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] = filter.isMulti === false ? (filter.value || '') : (filter.value || []);
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] = filter.isMulti === false ? currentYear : [currentYear];
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(filter.value) ? filter.value[0] || '' : (filter.value || '');
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(filter.value) ? filter.value : (filter.value ? [filter.value] : []);
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().getFullYear().toString();
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 ((filter.type === 'dropdown' || filter.type === 'multiselect') && filter.url) {
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((row, rowIndex) => {
733
- if (onRowClick) {
734
- onRowClick(row, rowIndex);
735
- }
736
- }, [onRowClick]);
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(async (e, header, row) => {
740
- e.stopPropagation(); // Prevent row click
741
-
742
- try {
743
- // Debug logging to check configuration
744
- const dateOnlyConfig = header.onClick.dateOnly || userProfile?.settings?.dateOnly || false;
745
- console.log('Cell click configuration:', {
746
- column: header.label,
747
- clickType: header.onClick.type || 'date',
748
- hasContactSelection: !!header.onClick.contactSelection,
749
- headerDateOnly: header.onClick.dateOnly,
750
- userProfileDateOnly: userProfile?.settings?.dateOnly,
751
- finalDateOnly: dateOnlyConfig
752
- });
1305
+ const handleCellClick = useCallback(
1306
+ async (e, header, row) => {
1307
+ e.stopPropagation(); // Prevent row click
753
1308
 
754
- // Determine the interaction type
755
- const clickType = header.onClick.type || 'date';
756
-
757
- if (clickType === 'date_with_contacts') {
758
- // Multi-step process: Date selection first, then contact selection
759
- await handleDateWithContactsFlow(header, row, dateOnlyConfig);
760
- } else {
761
- // Simple date selection (backward compatibility)
762
- await handleSimpleDateSelection(header, row, dateOnlyConfig);
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
- } catch (error) {
765
- console.error('Error handling cell click:', error);
766
- toast.error('An error occurred while opening the dialog');
767
- }
768
- }, [settings, settingsFilterValues, onDataChange, userProfile]);
1373
+ },
1374
+ [settings, settingsFilterValues, onDataChange, userProfile]
1375
+ );
769
1376
 
770
1377
  // Handle simple date selection (existing functionality)
771
- const handleSimpleDateSelection = useCallback(async (header, row, dateOnlyConfig) => {
772
- await showDatePickerDialog({
773
- title: header.onClick.title || 'Select Date',
774
- message: header.onClick.message || header.onClick.placeholder || `Please select a date for ${header.label}:`,
775
- confirmLabel: 'Set Date',
776
- cancelLabel: 'Cancel',
777
- defaultDate: header.onClick.params?.value === 'now' ? 'now' : (row[header.key] || null),
778
- data: { name: header.key, label: header.label, id: row.id },
779
- // Timezone configuration - can be set via userProfile or header config
780
- timezone: header.onClick.timezone || userProfile?.settings?.timezone || 'auto',
781
- forcePerth: header.onClick.forcePerth || userProfile?.settings?.forcePerth || false,
782
- // Date field type - set to true for DATE fields, false for DATETIME fields
783
- dateOnly: dateOnlyConfig,
784
- onConfirm: async (selectedDateISO) => {
785
- await submitDateUpdate(header, row, selectedDateISO);
786
- },
787
- onCancel: () => {
788
- // Do nothing when cancelled
789
- }
790
- });
791
- }, [settings, settingsFilterValues, onDataChange, userProfile]);
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(async (header, row, dateOnlyConfig) => {
795
- // Step 1: Date selection
796
- await showDatePickerDialog({
797
- title: header.onClick.title || 'Select Date',
798
- message: header.onClick.message || header.onClick.placeholder || `Please select a date for ${header.label}:`,
799
- confirmLabel: 'Next: Select Contacts',
800
- cancelLabel: 'Cancel',
801
- defaultDate: header.onClick.params?.value === 'now' ? 'now' : (row[header.key] || null),
802
- data: { name: header.key, label: header.label, id: row.id },
803
- timezone: header.onClick.timezone || userProfile?.settings?.timezone || 'auto',
804
- forcePerth: header.onClick.forcePerth || userProfile?.settings?.forcePerth || false,
805
- dateOnly: dateOnlyConfig,
806
- onConfirm: async (selectedDateISO) => {
807
- // Step 2: Contact selection
808
- const contactFieldKey = header.key + '_contacts';
809
- const existingContacts = row[contactFieldKey] || [];
810
-
811
- console.log('Opening contact selector with row context:', {
812
- rowKeys: Object.keys(row),
813
- rowClientId: row.client_id,
814
- urlParams: header.onClick.contactSelection?.urlParams,
815
- fullRow: row
816
- });
817
-
818
- await showContactSelectorModal({
819
- title: header.onClick.contactSelection?.title || 'Select Contacts',
820
- message: header.onClick.contactSelection?.message || `Add contacts for ${header.label}:`,
821
- confirmLabel: 'Save',
822
- cancelLabel: 'Cancel',
823
- existingContacts: Array.isArray(existingContacts) ? existingContacts : [],
824
- contactConfig: {
825
- contactsUrl: header.onClick.contactSelection?.contactsUrl || '/ajax/contacts/dropdown',
826
- urlParams: header.onClick.contactSelection?.urlParams || {},
827
- allowManualEntry: header.onClick.contactSelection?.allowManualEntry !== false,
828
- fields: header.onClick.contactSelection?.fields || ['name', 'email'],
829
- multiple: header.onClick.contactSelection?.multiple !== false,
830
- maxContacts: header.onClick.contactSelection?.maxContacts || 10
831
- },
832
- data: { name: header.key, label: header.label, id: row.id },
833
- rowContext: row,
834
- onConfirm: async (selectedContacts) => {
835
- // Submit both date and contacts
836
- await submitDateAndContactsUpdate(header, row, selectedDateISO, selectedContacts);
837
- },
838
- onCancel: () => {
839
- // User cancelled contact selection - do nothing
840
- }
841
- });
842
- },
843
- onCancel: () => {
844
- // User cancelled date selection - do nothing
845
- }
846
- });
847
- }, [settings, settingsFilterValues, onDataChange, userProfile]);
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(async (header, row, selectedDateISO) => {
851
- try {
852
- // Replace {id} in the URL with the row's ID
853
- const url = header.onClick.url.replace('{id}', row.id);
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
- // Show loading toast
856
- toast.info('Updating...', { autoClose: 1000 });
1521
+ // Show loading toast
1522
+ toast.info('Updating...', { autoClose: 1000 });
857
1523
 
858
- // Extract field and value from params
859
- const { field, ...otherParams } = header.onClick.params;
1524
+ // Extract field and value from params
1525
+ const { field, ...otherParams } = header.onClick.params;
860
1526
 
861
- // Create a new params object with the field as the key
862
- const params = {
863
- ...otherParams,
864
- _method: header.onClick.method, // Add _method for PUT/DELETE requests
865
- };
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
- // Set the field as the key with the selected date value
868
- params[field] = selectedDateISO;
1533
+ // Set the field as the key with the selected date value
1534
+ params[field] = selectedDateISO;
869
1535
 
870
- // Make the API call
871
- const response = await CustomFetch(url, header.onClick.method, params);
1536
+ // Make the API call
1537
+ const response = await CustomFetch(
1538
+ url,
1539
+ header.onClick.method,
1540
+ params
1541
+ );
872
1542
 
873
- if (response.data.error) {
874
- toast.error(response.data.error);
875
- } else {
876
- toast.success('Date updated successfully', { autoClose: 1000 });
877
- await refreshGridData();
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
- } catch (error) {
880
- console.error('Error updating date:', error);
881
- toast.error('An error occurred while updating');
882
- }
883
- }, []);
1555
+ },
1556
+ []
1557
+ );
884
1558
 
885
1559
  // Submit date and contacts update
886
- const submitDateAndContactsUpdate = useCallback(async (header, row, selectedDateISO, selectedContacts) => {
887
- try {
888
- // Replace {id} in the URL with the row's ID
889
- const url = header.onClick.url.replace('{id}', row.id);
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
- // Show loading toast
892
- toast.info('Saving date and contacts...', { autoClose: 1000 });
1566
+ // Show loading toast
1567
+ toast.info('Saving date and contacts...', { autoClose: 1000 });
893
1568
 
894
- // Extract field and value from params
895
- const { field, ...otherParams } = header.onClick.params;
1569
+ // Extract field and value from params
1570
+ const { field, ...otherParams } = header.onClick.params;
896
1571
 
897
- // Create a new params object with both date and contacts
898
- const params = {
899
- ...otherParams,
900
- _method: header.onClick.method, // Add _method for PUT/DELETE requests
901
- };
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
- // Set the date field
904
- params[field] = selectedDateISO;
905
-
906
- // Set the contacts field (field name + '_contacts')
907
- const contactsField = field + '_contacts';
908
- params[contactsField] = selectedContacts;
1581
+ // Set the contacts field (field name + '_contacts')
1582
+ const contactsField = field + '_contacts';
1583
+ params[contactsField] = selectedContacts;
909
1584
 
910
- console.log('Submitting date and contacts:', params);
1585
+ console.log('Submitting date and contacts:', params);
911
1586
 
912
- // Make the API call
913
- const response = await CustomFetch(url, header.onClick.method, params);
1587
+ // Make the API call
1588
+ const response = await CustomFetch(
1589
+ url,
1590
+ header.onClick.method,
1591
+ params
1592
+ );
914
1593
 
915
- if (response.data.error) {
916
- toast.error(response.data.error);
917
- } else {
918
- toast.success('Date and contacts saved successfully', { autoClose: 1500 });
919
- await refreshGridData();
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
- } catch (error) {
922
- console.error('Error updating date and contacts:', error);
923
- toast.error('An error occurred while saving');
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(fetchUrl, fetchMethod, fetchParams);
2145
+ const fetchResponse = await CustomFetch(
2146
+ fetchUrl,
2147
+ fetchMethod,
2148
+ fetchParams
2149
+ );
943
2150
  if (fetchResponse.data.data) {
944
- if (fetchResponse.data.data.header && fetchResponse.data.data.rows) {
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' || filter.type === 'multiselect') &&
1060
- (filter.id === 'year' || filter.startYear) &&
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] = filter.isMulti === false ? currentYear : [currentYear];
2279
+ initialValues[filter.id] =
2280
+ filter.isMulti === false
2281
+ ? currentYear
2282
+ : [currentYear];
1068
2283
  } else {
1069
- initialValues[filter.id] = currentYear;
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().getFullYear().toString();
1074
- initialValues[filter.id] = filter.isMulti === false ? currentYear : [currentYear];
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] = Array.isArray(filter.value) ? filter.value[0] || '' : (filter.value || '');
2298
+ initialValues[filter.id] =
2299
+ Array.isArray(filter.value)
2300
+ ? filter.value[0] || ''
2301
+ : filter.value || '';
1078
2302
  } else {
1079
- initialValues[filter.id] = Array.isArray(filter.value) ? filter.value : (filter.value ? [filter.value] : []);
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] = new Date().getFullYear().toString();
2313
+ initialValues[filter.id] =
2314
+ new Date()
2315
+ .getFullYear()
2316
+ .toString();
1085
2317
  } else {
1086
- initialValues[filter.id] = filter.value || '';
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="">Select {filter.label}</option>
2404
+ <option value="">
2405
+ Select {filter.label}
2406
+ </option>
1172
2407
  {filterOptions[filter.id] &&
1173
- filterOptions[filter.id].map((option) => (
1174
- <option
1175
- key={option.value || option.id}
1176
- value={option.value || option.id}
1177
- >
1178
- {option.label || option.name || option.title}
1179
- </option>
1180
- ))}
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="">Select {filter.label}</option>
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 className={styles.multiselectContainer}>
2457
+ <div
2458
+ className={styles.multiselectContainer}
2459
+ >
1211
2460
  <MultiSelect
1212
2461
  options={
1213
- (filter.id === 'year' || filter.startYear) ?
1214
- generateYearOptions(filter.startYear || 2000) :
1215
- filter.url ?
1216
- (filterOptions[filter.id] || []).map(option => ({
1217
- value: option.value || option.id,
1218
- label: option.label || option.name || option.title
1219
- })) :
1220
- filter.options || []
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 = settingsFilterValues[filter.id];
1224
- const availableOptions = (filter.id === 'year' || filter.startYear) ?
1225
- generateYearOptions(filter.startYear || 2000) :
1226
- filter.url ?
1227
- (filterOptions[filter.id] || []).map(option => ({
1228
- value: option.value || option.id,
1229
- label: option.label || option.name || option.title
1230
- })) :
1231
- filter.options || [];
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 = availableOptions.find(opt => opt.value === currentValue || opt.value === currentValue.toString());
1237
- return matchingOption || null;
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 (Array.isArray(currentValue) && currentValue.length > 0) {
1243
- return currentValue.map(val => {
1244
- const matchingOption = availableOptions.find(opt => opt.value === val || opt.value === val.toString());
1245
- return matchingOption || { value: val, label: val };
1246
- }).filter(Boolean);
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 = selectedOptions ? selectedOptions.value : '';
1255
- handleSettingsFilterChange(filter.id, value);
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 = Array.isArray(selectedOptions)
1259
- ? selectedOptions.map(opt => opt.value)
1260
- : [];
1261
- handleSettingsFilterChange(filter.id, values);
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: filter.limit || undefined
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} ${onRowClick ? styles.clickableRow : ''}`}
2707
+ className={`${styles.gridRow} ${
2708
+ onRowClick ? styles.clickableRow : ''
2709
+ }`}
1384
2710
  style={getCustomStyles().hoverColor}
1385
- onClick={() => handleRowClick(row, rowIndex)}
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
- !row[header.key]
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
- !row[header.key]
1404
- ? (e) => handleCellClick(e, header, row)
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
- {row[header.key] ? (
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;