@visns-studio/visns-components 5.10.11 → 5.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/package.json +7 -7
- package/src/components/cms/DataGrid.jsx +8 -8
- package/src/components/cms/DropZone.jsx +1 -1
- package/src/components/cms/Form.jsx +1 -1
- package/src/components/cms/Gallery.jsx +1 -1
- package/src/components/cms/sorting/Item.jsx +1 -1
- package/src/components/crm/Autocomplete.jsx +3 -3
- package/src/components/crm/Breadcrumb.jsx +4 -4
- package/src/components/crm/BusinessCardOcr.jsx +681 -95
- package/src/components/crm/DataGrid.jsx +34 -32
- package/src/components/crm/Field.jsx +12 -12
- package/src/components/crm/Form.jsx +2 -2
- package/src/components/crm/Navigation.jsx +11 -11
- package/src/components/crm/Notification.jsx +2 -2
- package/src/components/crm/QuickAction.jsx +3 -3
- package/src/components/crm/SectionHeader.jsx +1 -1
- package/src/components/crm/SwitchAccount.jsx +4 -4
- package/src/components/crm/auth/ClientLogin.jsx +2 -2
- package/src/components/crm/auth/ClientOTPVerify.jsx +2 -2
- package/src/components/crm/auth/Login.jsx +3 -3
- package/src/components/crm/auth/Reset.jsx +2 -2
- package/src/components/crm/auth/TwoFactorAuth.jsx +2 -2
- package/src/components/crm/columns/ColumnRenderers.jsx +320 -259
- package/src/components/crm/controls/DataGridSearch.jsx +5 -5
- package/src/components/crm/generic/AlternativeActionModal.jsx +100 -0
- package/src/components/crm/generic/ContactSelectorModal.jsx +423 -0
- package/src/components/crm/generic/DatePickerDialog.jsx +302 -0
- package/src/components/crm/generic/DateRangeSelectorModal.jsx +181 -0
- package/src/components/crm/generic/GenericClientPortal.jsx +1 -1
- package/src/components/crm/generic/GenericDashboard.jsx +4 -4
- package/src/components/crm/generic/GenericDetail.jsx +6 -6
- package/src/components/crm/generic/GenericDynamic.jsx +3 -3
- package/src/components/crm/generic/GenericEditableTable.jsx +4 -4
- package/src/components/crm/generic/GenericFormBuilder.jsx +6 -6
- package/src/components/crm/generic/GenericGrid.jsx +2888 -0
- package/src/components/crm/generic/GenericIndex.jsx +5 -1255
- package/src/components/crm/generic/GenericReport.jsx +966 -646
- package/src/components/crm/generic/GenericReportForm.jsx +194 -0
- package/src/components/crm/generic/NotificationList.jsx +1 -1
- package/src/components/crm/generic/ReasonCollectorModal.jsx +194 -0
- package/src/components/crm/generic/SortableQuoteItems.jsx +3 -3
- package/src/components/crm/generic/shared/formatters.js +231 -0
- package/src/components/crm/generic/shared/gridUtils.js +194 -0
- package/src/components/crm/generic/styles/AlternativeActionModal.css +127 -0
- package/src/components/crm/generic/styles/ContactSelectorModal.css +367 -0
- package/src/components/crm/generic/styles/DatePickerDialog.css +84 -0
- package/src/components/crm/generic/styles/DateRangeSelectorModal.css +115 -0
- package/src/components/crm/generic/styles/GenericIndex.module.scss +382 -0
- package/src/components/crm/generic/styles/GenericReport.module.scss +96 -0
- package/src/components/crm/generic/styles/ReasonCollectorModal.css +217 -0
- package/src/components/crm/modals/GalleryModal.jsx +2 -2
- package/src/components/crm/sorting/Item.jsx +3 -3
- package/src/components/crm/styles/BusinessCardOcr.module.scss +197 -4
- package/src/index.js +4 -0
|
@@ -0,0 +1,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 '
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
+
};
|