@visns-studio/visns-components 5.10.11 → 5.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +85 -0
  2. package/package.json +7 -7
  3. package/src/components/cms/DataGrid.jsx +8 -8
  4. package/src/components/cms/DropZone.jsx +1 -1
  5. package/src/components/cms/Form.jsx +1 -1
  6. package/src/components/cms/Gallery.jsx +1 -1
  7. package/src/components/cms/sorting/Item.jsx +1 -1
  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/DataGrid.jsx +34 -32
  12. package/src/components/crm/Field.jsx +12 -12
  13. package/src/components/crm/Form.jsx +2 -2
  14. package/src/components/crm/Navigation.jsx +11 -11
  15. package/src/components/crm/Notification.jsx +2 -2
  16. package/src/components/crm/QuickAction.jsx +3 -3
  17. package/src/components/crm/SectionHeader.jsx +1 -1
  18. package/src/components/crm/SwitchAccount.jsx +4 -4
  19. package/src/components/crm/auth/ClientLogin.jsx +2 -2
  20. package/src/components/crm/auth/ClientOTPVerify.jsx +2 -2
  21. package/src/components/crm/auth/Login.jsx +3 -3
  22. package/src/components/crm/auth/Reset.jsx +2 -2
  23. package/src/components/crm/auth/TwoFactorAuth.jsx +2 -2
  24. package/src/components/crm/columns/ColumnRenderers.jsx +320 -259
  25. package/src/components/crm/controls/DataGridSearch.jsx +5 -5
  26. package/src/components/crm/generic/AlternativeActionModal.jsx +100 -0
  27. package/src/components/crm/generic/ContactSelectorModal.jsx +423 -0
  28. package/src/components/crm/generic/DatePickerDialog.jsx +302 -0
  29. package/src/components/crm/generic/DateRangeSelectorModal.jsx +181 -0
  30. package/src/components/crm/generic/GenericClientPortal.jsx +1 -1
  31. package/src/components/crm/generic/GenericDashboard.jsx +4 -4
  32. package/src/components/crm/generic/GenericDetail.jsx +6 -6
  33. package/src/components/crm/generic/GenericDynamic.jsx +3 -3
  34. package/src/components/crm/generic/GenericEditableTable.jsx +4 -4
  35. package/src/components/crm/generic/GenericFormBuilder.jsx +6 -6
  36. package/src/components/crm/generic/GenericGrid.jsx +2888 -0
  37. package/src/components/crm/generic/GenericIndex.jsx +5 -1255
  38. package/src/components/crm/generic/GenericReport.jsx +966 -646
  39. package/src/components/crm/generic/GenericReportForm.jsx +194 -0
  40. package/src/components/crm/generic/NotificationList.jsx +1 -1
  41. package/src/components/crm/generic/ReasonCollectorModal.jsx +194 -0
  42. package/src/components/crm/generic/SortableQuoteItems.jsx +3 -3
  43. package/src/components/crm/generic/shared/formatters.js +231 -0
  44. package/src/components/crm/generic/shared/gridUtils.js +194 -0
  45. package/src/components/crm/generic/styles/AlternativeActionModal.css +127 -0
  46. package/src/components/crm/generic/styles/ContactSelectorModal.css +367 -0
  47. package/src/components/crm/generic/styles/DatePickerDialog.css +84 -0
  48. package/src/components/crm/generic/styles/DateRangeSelectorModal.css +115 -0
  49. package/src/components/crm/generic/styles/GenericIndex.module.scss +382 -0
  50. package/src/components/crm/generic/styles/GenericReport.module.scss +96 -0
  51. package/src/components/crm/generic/styles/ReasonCollectorModal.css +217 -0
  52. package/src/components/crm/modals/GalleryModal.jsx +2 -2
  53. package/src/components/crm/sorting/Item.jsx +3 -3
  54. package/src/components/crm/styles/BusinessCardOcr.module.scss +197 -4
  55. package/src/index.js +4 -0
@@ -0,0 +1,194 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { toast } from 'react-toastify';
3
+ import { saveAs } from 'file-saver';
4
+ import { Download as CloudDownload } from 'lucide-react';
5
+
6
+ import Download from '../Download';
7
+ import Field from '../Field';
8
+
9
+ import reportStyles from './styles/ReportForm.module.scss';
10
+
11
+ const GenericReportForm = ({ config, userProfile, onExport, onFormChange }) => {
12
+ const [formData, setFormData] = useState({});
13
+
14
+ const handleChange = (e) => {
15
+ const {
16
+ value,
17
+ dataset: { name },
18
+ } = e.target;
19
+
20
+ const newFormData = { ...formData, [name]: value };
21
+ setFormData(newFormData);
22
+
23
+ // Notify parent component of form change
24
+ if (onFormChange) {
25
+ onFormChange(newFormData);
26
+ }
27
+ };
28
+
29
+ const handleChangeDate = (date, id) => {
30
+ const newFormData = { ...formData, [id]: date };
31
+ setFormData(newFormData);
32
+
33
+ // Notify parent component of form change
34
+ if (onFormChange) {
35
+ onFormChange(newFormData);
36
+ }
37
+ };
38
+
39
+ const handleChangeSelect = (inputValue, _, id) => {
40
+ const newFormData = { ...formData, [id]: inputValue };
41
+ setFormData(newFormData);
42
+
43
+ // Notify parent component of form change
44
+ if (onFormChange) {
45
+ onFormChange(newFormData);
46
+ }
47
+ };
48
+
49
+ const handleExport = async (e) => {
50
+ try {
51
+ if (e) {
52
+ e.preventDefault();
53
+ }
54
+
55
+ // Check if there's a custom export handler
56
+ if (onExport) {
57
+ return onExport(formData);
58
+ }
59
+
60
+ // Default export logic
61
+ const missingFields = config.form.filters
62
+ .filter(
63
+ (item) => item.required === true && formData[item.id] === ''
64
+ )
65
+ .map((item) => item.label);
66
+
67
+ if (missingFields.length > 0) {
68
+ const fieldNames = missingFields.join(', ');
69
+ const errorMessage = `Please fill in a value for the following field(s): ${fieldNames}.`;
70
+ toast.error(errorMessage);
71
+ } else {
72
+ toast.info('Preparing your export...');
73
+
74
+ const res = await Download(
75
+ config.form.url,
76
+ config.form.method,
77
+ formData
78
+ );
79
+
80
+ if (res.data.error) {
81
+ toast.error(res.data.error);
82
+ } else {
83
+ // Assuming the response contains a Blob or similar data
84
+ if (res.data && res.data instanceof Blob) {
85
+ const fileName = `${config.form.filename}`;
86
+ saveAs(res.data, fileName);
87
+ toast.success('Export completed successfully');
88
+ } else {
89
+ // Handle the case where the response is not a Blob
90
+ toast.error('Invalid file format received.');
91
+ }
92
+ }
93
+ }
94
+ } catch (err) {
95
+ console.error(err);
96
+ toast.error('An error occurred during export');
97
+ }
98
+ };
99
+
100
+ useEffect(() => {
101
+ if (config.form?.filters) {
102
+ const tmpForm = config.form.filters.reduce((acc, item) => {
103
+ acc[item.id] = '';
104
+ return acc;
105
+ }, {});
106
+ setFormData(tmpForm);
107
+
108
+ // Notify parent component of initial form data
109
+ if (onFormChange) {
110
+ onFormChange(tmpForm);
111
+ }
112
+ }
113
+ }, [config.form?.filters, onFormChange]);
114
+
115
+ // Determine if the form has a title
116
+ const formTitle = config.form?.title || 'Export Report';
117
+ const formDescription =
118
+ config.form?.description || 'Select your export criteria below.';
119
+
120
+ // Check if form configuration is valid
121
+ if (!config.form?.filters) {
122
+ return (
123
+ <div className={reportStyles.reportFormContainer}>
124
+ <div className={reportStyles.reportForm}>
125
+ <h2 className={reportStyles.formTitle}>No Report Configuration</h2>
126
+ <p className={reportStyles.formDescription}>
127
+ Report form configuration is missing.
128
+ </p>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ return (
135
+ <div className={reportStyles.reportFormContainer}>
136
+ <form className={reportStyles.reportForm}>
137
+ <h2 className={reportStyles.formTitle}>{formTitle}</h2>
138
+ <p className={reportStyles.formDescription}>
139
+ {formDescription}
140
+ </p>
141
+
142
+ {config.form.filters.map((item, key) => (
143
+ <div
144
+ key={`filter-container-${key}`}
145
+ className={`${reportStyles.formField} ${
146
+ item.fullWidth ? reportStyles.fullWidthField : ''
147
+ }`}
148
+ >
149
+ <Field
150
+ api={userProfile?.settings?.api}
151
+ autocompleteCallback={() => {}}
152
+ childDropdownCallback={() => {}}
153
+ fetchData={() => {}}
154
+ formData={formData}
155
+ inputClass={''}
156
+ inputValue={formData[item.id]}
157
+ key={`filter-fields-${key}`}
158
+ mapbox={{}}
159
+ onChange={handleChange}
160
+ onChangeCheckbox={() => {}}
161
+ onChangeDate={handleChangeDate}
162
+ onChangeNumberFormat={() => {}}
163
+ onChangeRicheditor={() => {}}
164
+ onChangeSelect={handleChangeSelect}
165
+ onChangeToggle={() => {}}
166
+ settings={item}
167
+ setFormData={setFormData}
168
+ style={userProfile?.settings?.style}
169
+ uploadProgress={0}
170
+ userProfile={userProfile}
171
+ />
172
+ </div>
173
+ ))}
174
+
175
+ <div className={reportStyles.formActions}>
176
+ <button
177
+ className={reportStyles.exportButton}
178
+ onClick={handleExport}
179
+ type="button"
180
+ >
181
+ <CloudDownload
182
+ strokeWidth={2}
183
+ size={18}
184
+ style={{ marginRight: '8px' }}
185
+ />{' '}
186
+ Export
187
+ </button>
188
+ </div>
189
+ </form>
190
+ </div>
191
+ );
192
+ };
193
+
194
+ export default GenericReportForm;
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useCallback } from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
3
  import Table from '../DataGrid';
4
- import { Bell, Check, ArrowLeft } from 'akar-icons';
4
+ import { Bell, Check, ArrowLeft } from 'lucide-react';
5
5
  import { toast } from 'react-toastify';
6
6
  import CustomFetch from '../Fetch';
7
7
  import styles from './styles/NotificationList.module.scss';
@@ -0,0 +1,194 @@
1
+ import Swal from 'sweetalert2';
2
+ import './styles/SweetAlert.module.css';
3
+ import './styles/ReasonCollectorModal.css';
4
+
5
+ /**
6
+ * Show a reason collector modal for collecting "not sent" reasons
7
+ * @param {Object} options - Configuration options
8
+ * @param {string} options.title - Modal title
9
+ * @param {string} options.message - Modal message
10
+ * @param {string} options.confirmLabel - Confirm button label
11
+ * @param {string} options.cancelLabel - Cancel button label
12
+ * @param {Array} options.reasonOptions - Array of predefined reason options
13
+ * @param {boolean} options.allowCustomReason - Allow custom reason input
14
+ * @param {boolean} options.reasonRequired - Whether reason is required
15
+ * @param {string} options.placeholder - Placeholder for custom reason input
16
+ * @param {string} options.defaultReason - Default selected reason
17
+ * @param {Object} options.data - Additional data to pass to callbacks
18
+ * @param {Function} options.onConfirm - Callback when confirmed
19
+ * @param {Function} options.onCancel - Callback when cancelled
20
+ */
21
+ export const showReasonCollectorModal = async ({
22
+ title = 'Provide Reason',
23
+ message = 'Please provide a reason:',
24
+ confirmLabel = 'Save',
25
+ cancelLabel = 'Cancel',
26
+ reasonOptions = [
27
+ 'Survey not applicable',
28
+ 'Client does not want survey',
29
+ 'Other'
30
+ ],
31
+ allowCustomReason = true,
32
+ reasonRequired = true,
33
+ placeholder = 'Enter custom reason...',
34
+ defaultReason = '',
35
+ data = {},
36
+ onConfirm,
37
+ onCancel
38
+ }) => {
39
+ console.log('Reason Collector Modal - Configuration:', {
40
+ title,
41
+ reasonOptions,
42
+ allowCustomReason,
43
+ reasonRequired,
44
+ defaultReason,
45
+ data
46
+ });
47
+
48
+ // Generate HTML for reason options (handle "Other" specially for custom input)
49
+ const reasonOptionsHtml = reasonOptions.map((option, index) => {
50
+ const isSelected = option === defaultReason;
51
+ const isOther = option.toLowerCase() === 'other';
52
+
53
+ if (isOther && allowCustomReason) {
54
+ return `
55
+ <div class="reason-option reason-option-other">
56
+ <div class="reason-radio-container">
57
+ <input
58
+ type="radio"
59
+ id="reason-${index}"
60
+ name="reason"
61
+ value="custom"
62
+ ${isSelected ? 'checked' : ''}
63
+ class="reason-radio"
64
+ />
65
+ <label for="reason-${index}" class="reason-label">${option}</label>
66
+ </div>
67
+ <div class="custom-reason-input-container" style="display: none;">
68
+ <input
69
+ type="text"
70
+ id="custom-reason-input"
71
+ class="custom-reason-input compact"
72
+ placeholder="${placeholder}"
73
+ />
74
+ </div>
75
+ </div>
76
+ `;
77
+ } else {
78
+ return `
79
+ <div class="reason-option">
80
+ <input
81
+ type="radio"
82
+ id="reason-${index}"
83
+ name="reason"
84
+ value="${option}"
85
+ ${isSelected ? 'checked' : ''}
86
+ class="reason-radio"
87
+ />
88
+ <label for="reason-${index}" class="reason-label">${option}</label>
89
+ </div>
90
+ `;
91
+ }
92
+ }).join('');
93
+
94
+ const result = await Swal.fire({
95
+ title: title,
96
+ html: `
97
+ <div class="reason-collector-modal compact">
98
+ <p class="reason-message">${message}</p>
99
+ <div class="reason-options-container compact">
100
+ ${reasonOptionsHtml}
101
+ </div>
102
+ </div>
103
+ `,
104
+ showCancelButton: true,
105
+ confirmButtonText: confirmLabel,
106
+ cancelButtonText: cancelLabel,
107
+ customClass: {
108
+ container: 'reason-swal-container',
109
+ popup: 'reason-swal-popup',
110
+ content: 'reason-swal-content',
111
+ confirmButton: 'reason-swal-confirm',
112
+ cancelButton: 'reason-swal-cancel'
113
+ },
114
+ didOpen: () => {
115
+ // Handle custom reason toggle for "Other" option
116
+ if (allowCustomReason) {
117
+ const customInputContainer = document.querySelector('.custom-reason-input-container');
118
+ const customInput = document.getElementById('custom-reason-input');
119
+ const allRadios = document.querySelectorAll('input[name="reason"]');
120
+
121
+ // Find the "Other" radio button
122
+ let otherRadio = null;
123
+ allRadios.forEach(radio => {
124
+ if (radio.value === 'custom') {
125
+ otherRadio = radio;
126
+ }
127
+ });
128
+
129
+ if (otherRadio && customInputContainer && customInput) {
130
+ // Show/hide custom input based on selection
131
+ allRadios.forEach(radio => {
132
+ radio.addEventListener('change', () => {
133
+ if (radio.value === 'custom') {
134
+ customInputContainer.style.display = 'block';
135
+ customInput.focus();
136
+ } else {
137
+ customInputContainer.style.display = 'none';
138
+ customInput.value = ''; // Clear input when switching away
139
+ }
140
+ });
141
+ });
142
+
143
+ // Auto-select "Other" radio when typing in input
144
+ customInput.addEventListener('input', () => {
145
+ if (customInput.value.trim()) {
146
+ otherRadio.checked = true;
147
+ customInputContainer.style.display = 'block';
148
+ }
149
+ });
150
+ }
151
+ }
152
+ },
153
+ preConfirm: () => {
154
+ const selectedRadio = document.querySelector('input[name="reason"]:checked');
155
+
156
+ if (reasonRequired && !selectedRadio) {
157
+ Swal.showValidationMessage('Please select a reason');
158
+ return false;
159
+ }
160
+
161
+ let reason = '';
162
+
163
+ if (selectedRadio) {
164
+ if (selectedRadio.value === 'custom') {
165
+ const customInput = document.getElementById('custom-reason-input');
166
+ reason = customInput.value.trim();
167
+
168
+ if (reasonRequired && !reason) {
169
+ Swal.showValidationMessage('Please enter a custom reason');
170
+ return false;
171
+ }
172
+ } else {
173
+ reason = selectedRadio.value;
174
+ }
175
+ }
176
+
177
+ console.log('Reason selected:', {
178
+ reason: reason,
179
+ isCustom: selectedRadio?.value === 'custom'
180
+ });
181
+
182
+ return {
183
+ reason: reason,
184
+ isCustom: selectedRadio?.value === 'custom'
185
+ };
186
+ }
187
+ });
188
+
189
+ if (result.isConfirmed && onConfirm) {
190
+ await onConfirm(result.value.reason, result.value.isCustom);
191
+ } else if (result.isDismissed && onCancel) {
192
+ onCancel();
193
+ }
194
+ };
@@ -4,7 +4,7 @@ import {
4
4
  SortableElement,
5
5
  sortableHandle,
6
6
  } from 'react-sortable-hoc';
7
- import { ChevronVertical, Pencil, TrashCan } from 'akar-icons';
7
+ import { GripVertical, Pencil, Trash2 } from 'lucide-react';
8
8
  import get from 'lodash/get';
9
9
  import { formatCurrency } from './utils/formatters';
10
10
 
@@ -12,7 +12,7 @@ import styles from './styles/SortableQuoteItems.module.scss';
12
12
 
13
13
  // Drag handle component
14
14
  const DragHandle = sortableHandle(() => (
15
- <ChevronVertical className={styles.dragHandle} strokeWidth={1} size={22} />
15
+ <GripVertical className={styles.dragHandle} strokeWidth={1} size={22} />
16
16
  ));
17
17
 
18
18
  // Sortable table row component
@@ -61,7 +61,7 @@ const SortableRow = SortableElement(
61
61
  onEdit(row);
62
62
  }}
63
63
  />
64
- <TrashCan
64
+ <Trash2
65
65
  className={styles.deleteButton}
66
66
  strokeWidth={2}
67
67
  size={20}
@@ -0,0 +1,231 @@
1
+ import parse from 'html-react-parser';
2
+ import moment from 'moment';
3
+ import numeral from 'numeral';
4
+
5
+ /**
6
+ * Format quarter columns with status, contacts, and reasons in tooltips
7
+ * @param {*} value - The date value
8
+ * @param {Object} column - Column configuration
9
+ * @param {Object} row - Row data with status and contacts
10
+ * @returns {Object} Formatted quarter cell content with tooltip data
11
+ */
12
+ const formatQuarterWithStatus = (value, column, row) => {
13
+ const statusField = column.key + '_status';
14
+ const contactsField = column.key + '_contacts';
15
+ const reasonField = column.key + '_reason';
16
+
17
+ const status = row[statusField] || 'pending';
18
+ const contacts = row[contactsField] || [];
19
+ const reason = row[reasonField];
20
+
21
+ console.log('Quarter formatting:', {
22
+ column: column.key,
23
+ status,
24
+ contacts,
25
+ reason,
26
+ value
27
+ });
28
+
29
+ // If status is 'sent' and we have a date
30
+ if (status === 'sent' && value) {
31
+ const formattedDate = moment(value).isValid()
32
+ ? moment(value).format('DD/MM/YYYY')
33
+ : value;
34
+
35
+ const contactCount = Array.isArray(contacts) ? contacts.length : 0;
36
+
37
+ // Build tooltip content
38
+ let tooltipText = `Status: Sent on ${formattedDate}`;
39
+ if (contactCount > 0) {
40
+ tooltipText += `\nContacts: ${contactCount} contact${contactCount !== 1 ? 's' : ''}`;
41
+ if (Array.isArray(contacts)) {
42
+ contacts.forEach((contact, index) => {
43
+ if (index < 3) { // Show first 3 contacts
44
+ tooltipText += `\n • ${contact.name || 'Unknown'} ${contact.email ? `(${contact.email})` : ''}`;
45
+ }
46
+ });
47
+ if (contacts.length > 3) {
48
+ tooltipText += `\n ... and ${contacts.length - 3} more`;
49
+ }
50
+ }
51
+ }
52
+
53
+ return {
54
+ display: `📅 ${formattedDate}${contactCount > 0 ? ` (${contactCount})` : ''}`,
55
+ tooltip: tooltipText,
56
+ style: { color: '#059669', fontWeight: '500' }
57
+ };
58
+ }
59
+
60
+ // If status is 'not_sent' with reason
61
+ if (status === 'not_sent' && reason) {
62
+ const tooltipText = `Status: Not Sent\nReason: ${reason}`;
63
+
64
+ return {
65
+ display: `❌ Not Sent`,
66
+ tooltip: tooltipText,
67
+ style: { color: '#dc2626', fontWeight: '500' }
68
+ };
69
+ }
70
+
71
+ // If we have a date but no clear status (legacy data)
72
+ if (value) {
73
+ const formattedDate = moment(value).isValid()
74
+ ? moment(value).format('DD/MM/YYYY')
75
+ : value;
76
+
77
+ const contactCount = Array.isArray(contacts) ? contacts.length : 0;
78
+
79
+ let tooltipText = `Date: ${formattedDate}`;
80
+ if (contactCount > 0) {
81
+ tooltipText += `\nContacts: ${contactCount} contact${contactCount !== 1 ? 's' : ''}`;
82
+ if (Array.isArray(contacts)) {
83
+ contacts.forEach((contact, index) => {
84
+ if (index < 3) {
85
+ tooltipText += `\n • ${contact.name || 'Unknown'} ${contact.email ? `(${contact.email})` : ''}`;
86
+ }
87
+ });
88
+ if (contacts.length > 3) {
89
+ tooltipText += `\n ... and ${contacts.length - 3} more`;
90
+ }
91
+ }
92
+ }
93
+
94
+ return {
95
+ display: `${formattedDate}${contactCount > 0 ? ` (${contactCount})` : ''}`,
96
+ tooltip: tooltipText,
97
+ style: { color: '#374151' }
98
+ };
99
+ }
100
+
101
+ // Default case - return empty or pending
102
+ return null;
103
+ };
104
+
105
+ /**
106
+ * Format cell content based on column type or value type
107
+ * @param {*} value - The value to format
108
+ * @param {Object} column - Column configuration object
109
+ * @param {Object} row - Row data (optional, for quarter status display)
110
+ * @returns {*} Formatted value
111
+ */
112
+ export const formatCellContent = (value, column, row = null) => {
113
+ if (value === null || value === undefined) return '';
114
+ if (value === 0) return '0'; // Explicitly handle zero values
115
+ if (value === '') return '';
116
+
117
+ // Special handling for quarter columns with status
118
+ if (column.key && (column.key.startsWith('quarter_') || column.key === 'anniversary') && row) {
119
+ return formatQuarterWithStatus(value, column, row);
120
+ }
121
+
122
+ // Try to infer the type if not provided
123
+ const valueType = column.type || typeof value;
124
+
125
+ switch (valueType) {
126
+ case 'date':
127
+ return moment(value).isValid()
128
+ ? moment(value).format(column.format || 'DD/MM/YYYY')
129
+ : value;
130
+ case 'datetime':
131
+ return moment(value).isValid()
132
+ ? moment(value).format(column.format || 'DD/MM/YYYY HH:mm')
133
+ : value;
134
+ case 'currency':
135
+ return numeral(value).format(column.format || '$0,0.00');
136
+ case 'number':
137
+ // Check if it's actually a number before formatting
138
+ return !isNaN(parseFloat(value)) && isFinite(value)
139
+ ? numeral(value).format(column.format || '0,0')
140
+ : value;
141
+ case 'boolean':
142
+ if (
143
+ value === 1 ||
144
+ value === true ||
145
+ value === 'true' ||
146
+ value === 'yes' ||
147
+ value === 'Yes'
148
+ ) {
149
+ return 'Yes';
150
+ }
151
+ if (
152
+ value === 0 ||
153
+ value === false ||
154
+ value === 'false' ||
155
+ value === 'no' ||
156
+ value === 'No'
157
+ ) {
158
+ return 'No';
159
+ }
160
+ return value;
161
+ case 'year':
162
+ // Special handling for year values
163
+ return value.toString();
164
+ case 'quarter':
165
+ // Special handling for quarter values (Q1, Q2, etc.)
166
+ if (value && value.toString().startsWith('Q')) {
167
+ return value;
168
+ }
169
+ return value;
170
+ case 'html':
171
+ // Explicitly parse HTML content
172
+ return parse(String(value));
173
+ case 'richtext':
174
+ // Parse rich text content
175
+ return parse(String(value));
176
+ default:
177
+ // For text values, check if it contains HTML tags
178
+ if (
179
+ typeof value === 'string' &&
180
+ (value.includes('<br') ||
181
+ value.includes('<p>') ||
182
+ value.includes('<div') ||
183
+ value.includes('<span'))
184
+ ) {
185
+ // Ensure <br /> tags are properly handled
186
+ const processedValue = String(value)
187
+ .replace(/<br \/>/g, '<br>')
188
+ .replace(/<br\/>/g, '<br>');
189
+ return parse(processedValue);
190
+ }
191
+ return value;
192
+ }
193
+ };
194
+
195
+ /**
196
+ * Generate year options for dropdown
197
+ * @param {number} startYear - Starting year
198
+ * @returns {Array} Array of year options
199
+ */
200
+ export const generateYearOptions = (startYear) => {
201
+ const currentYear = new Date().getFullYear();
202
+ const years = [];
203
+
204
+ // Start from the specified year and go up to current year
205
+ for (let year = startYear; year <= currentYear + 1; year++) {
206
+ years.push({ value: year.toString(), label: year.toString() });
207
+ }
208
+
209
+ return years;
210
+ };
211
+
212
+ /**
213
+ * Apply custom styling from userProfile
214
+ * @param {Object} userProfile - User profile object
215
+ * @returns {Object} Custom styles object
216
+ */
217
+ export const getCustomStyles = (userProfile) => {
218
+ const customStyles = {};
219
+
220
+ if (userProfile?.settings?.style) {
221
+ const { style } = userProfile.settings;
222
+
223
+ if (style.hoverColor) {
224
+ customStyles.hoverColor = {
225
+ '--hover-color': style.hoverColor,
226
+ };
227
+ }
228
+ }
229
+
230
+ return customStyles;
231
+ };