@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
|
@@ -2,11 +2,11 @@ import { useNavigate } from 'react-router-dom';
|
|
|
2
2
|
import { toast } from 'react-toastify';
|
|
3
3
|
import { saveAs } from 'file-saver';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
ExternalLink,
|
|
6
6
|
Plus,
|
|
7
7
|
Search,
|
|
8
|
-
|
|
9
|
-
} from '
|
|
8
|
+
ArrowUpDown,
|
|
9
|
+
} from 'lucide-react';
|
|
10
10
|
import Download from '../Download';
|
|
11
11
|
import styles from '../styles/DataGrid.module.scss';
|
|
12
12
|
|
|
@@ -100,7 +100,7 @@ const DataGridSearch = ({
|
|
|
100
100
|
<div className={buttonClassName}>
|
|
101
101
|
{form.export && form.export.url && (
|
|
102
102
|
<button className={styles.btn} onClick={exportTrigger}>
|
|
103
|
-
<
|
|
103
|
+
<ExternalLink
|
|
104
104
|
strokeWidth={2}
|
|
105
105
|
size={16}
|
|
106
106
|
style={{ marginRight: '10px' }}
|
|
@@ -110,7 +110,7 @@ const DataGridSearch = ({
|
|
|
110
110
|
)}
|
|
111
111
|
{form.sort && form.sort.title && (
|
|
112
112
|
<button className={styles.btn} onClick={sortTrigger}>
|
|
113
|
-
<
|
|
113
|
+
<ArrowUpDown
|
|
114
114
|
strokeWidth={2}
|
|
115
115
|
size={16}
|
|
116
116
|
style={{ marginRight: '10px' }}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import Swal from 'sweetalert2';
|
|
2
|
+
import './styles/SweetAlert.module.css';
|
|
3
|
+
import './styles/AlternativeActionModal.css';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Show an alternative action modal for choosing between different workflows
|
|
7
|
+
* @param {Object} options - Configuration options
|
|
8
|
+
* @param {string} options.title - Modal title
|
|
9
|
+
* @param {string} options.message - Modal message
|
|
10
|
+
* @param {Array} options.alternatives - Array of alternative actions
|
|
11
|
+
* @param {Object} options.data - Additional data to pass to callbacks
|
|
12
|
+
* @param {Function} options.onSelect - Callback when alternative is selected
|
|
13
|
+
* @param {Function} options.onCancel - Callback when cancelled
|
|
14
|
+
*/
|
|
15
|
+
export const showAlternativeActionModal = async ({
|
|
16
|
+
title = 'Choose Action',
|
|
17
|
+
message = 'Please select how you would like to proceed:',
|
|
18
|
+
alternatives = [],
|
|
19
|
+
data = {},
|
|
20
|
+
onSelect,
|
|
21
|
+
onCancel
|
|
22
|
+
}) => {
|
|
23
|
+
console.log('Alternative Action Modal - Configuration:', {
|
|
24
|
+
title,
|
|
25
|
+
alternatives: alternatives.map(alt => ({ id: alt.id, label: alt.label, workflow: alt.workflow })),
|
|
26
|
+
data
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Generate HTML for alternatives
|
|
30
|
+
const alternativesHtml = alternatives.map(alternative => {
|
|
31
|
+
const icon = alternative.icon || '📋';
|
|
32
|
+
const description = alternative.description || '';
|
|
33
|
+
|
|
34
|
+
return `
|
|
35
|
+
<div class="alternative-action-option" data-alternative-id="${alternative.id}">
|
|
36
|
+
<div class="alternative-icon">${icon}</div>
|
|
37
|
+
<div class="alternative-content">
|
|
38
|
+
<div class="alternative-label">${alternative.label}</div>
|
|
39
|
+
${description ? `<div class="alternative-description">${description}</div>` : ''}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
`;
|
|
43
|
+
}).join('');
|
|
44
|
+
|
|
45
|
+
const result = await Swal.fire({
|
|
46
|
+
title: title,
|
|
47
|
+
html: `
|
|
48
|
+
<div class="alternative-action-modal">
|
|
49
|
+
<p class="alternative-message">${message}</p>
|
|
50
|
+
<div class="alternative-options">
|
|
51
|
+
${alternativesHtml}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
`,
|
|
55
|
+
showConfirmButton: false,
|
|
56
|
+
showCancelButton: true,
|
|
57
|
+
cancelButtonText: 'Cancel',
|
|
58
|
+
customClass: {
|
|
59
|
+
container: 'alternative-swal-container',
|
|
60
|
+
popup: 'alternative-swal-popup',
|
|
61
|
+
content: 'alternative-swal-content',
|
|
62
|
+
cancelButton: 'alternative-swal-cancel'
|
|
63
|
+
},
|
|
64
|
+
didOpen: () => {
|
|
65
|
+
// Add click handlers for each alternative
|
|
66
|
+
const options = document.querySelectorAll('.alternative-action-option');
|
|
67
|
+
options.forEach(option => {
|
|
68
|
+
option.addEventListener('click', () => {
|
|
69
|
+
const alternativeId = option.getAttribute('data-alternative-id');
|
|
70
|
+
const selectedAlternative = alternatives.find(alt => alt.id === alternativeId);
|
|
71
|
+
|
|
72
|
+
if (selectedAlternative) {
|
|
73
|
+
console.log('Alternative selected:', selectedAlternative);
|
|
74
|
+
Swal.close();
|
|
75
|
+
|
|
76
|
+
// Call the onSelect callback with the selected alternative
|
|
77
|
+
if (onSelect) {
|
|
78
|
+
onSelect(selectedAlternative);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Add hover effects
|
|
84
|
+
option.addEventListener('mouseenter', () => {
|
|
85
|
+
option.style.backgroundColor = '#f8f9fa';
|
|
86
|
+
option.style.borderColor = '#007bff';
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
option.addEventListener('mouseleave', () => {
|
|
90
|
+
option.style.backgroundColor = '';
|
|
91
|
+
option.style.borderColor = '';
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (result.isDismissed && onCancel) {
|
|
98
|
+
onCancel();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import Swal from 'sweetalert2';
|
|
3
|
+
import './styles/SweetAlert.module.css';
|
|
4
|
+
import './styles/ContactSelectorModal.css';
|
|
5
|
+
import CustomFetch from '../Fetch';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A utility function that shows a contact selector modal using SweetAlert2
|
|
9
|
+
* Used after date selection to add/select contacts for a survey field
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options - Configuration options
|
|
12
|
+
* @param {string} options.title - The title of the dialog (default: 'Select Contacts')
|
|
13
|
+
* @param {string} options.message - The message to display in the dialog
|
|
14
|
+
* @param {string} options.confirmLabel - Label for the confirm button (default: 'Save')
|
|
15
|
+
* @param {string} options.cancelLabel - Label for the cancel button (default: 'Cancel')
|
|
16
|
+
* @param {Function} options.onConfirm - Function to call when confirmed with selected contacts
|
|
17
|
+
* @param {Function} options.onCancel - Function to call when cancelled
|
|
18
|
+
* @param {Array} options.existingContacts - Array of existing contacts for this field
|
|
19
|
+
* @param {Object} options.contactConfig - Contact selection configuration
|
|
20
|
+
* @param {Object} options.data - Optional data object to pass context
|
|
21
|
+
* @returns {Promise} - A promise that resolves when the dialog is closed
|
|
22
|
+
*/
|
|
23
|
+
export const showContactSelectorModal = ({
|
|
24
|
+
title = 'Select Contacts',
|
|
25
|
+
message = 'Add contacts for this survey:',
|
|
26
|
+
confirmLabel = 'Save',
|
|
27
|
+
cancelLabel = 'Cancel',
|
|
28
|
+
onConfirm = () => {},
|
|
29
|
+
onCancel = () => {},
|
|
30
|
+
existingContacts = [],
|
|
31
|
+
contactConfig = {},
|
|
32
|
+
data = null,
|
|
33
|
+
rowContext = null,
|
|
34
|
+
}) => {
|
|
35
|
+
const {
|
|
36
|
+
contactsUrl = '/ajax/contacts/dropdown',
|
|
37
|
+
urlParams = {},
|
|
38
|
+
allowManualEntry = true,
|
|
39
|
+
fields = ['name', 'email'],
|
|
40
|
+
fieldTypes = {},
|
|
41
|
+
fieldLabels = {},
|
|
42
|
+
validation = {},
|
|
43
|
+
multiple = true,
|
|
44
|
+
maxContacts = 10
|
|
45
|
+
} = contactConfig;
|
|
46
|
+
|
|
47
|
+
// State management for the modal
|
|
48
|
+
let modalState = {
|
|
49
|
+
contacts: [...existingContacts],
|
|
50
|
+
availableContacts: [],
|
|
51
|
+
isAddingManual: false,
|
|
52
|
+
newContact: {}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Initialize new contact object
|
|
56
|
+
fields.forEach(field => {
|
|
57
|
+
modalState.newContact[field] = '';
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Process URL parameters with row context substitution
|
|
61
|
+
const processUrlParams = (params, rowData) => {
|
|
62
|
+
if (!params || typeof params !== 'object') return {};
|
|
63
|
+
|
|
64
|
+
console.log('Processing URL params:', {
|
|
65
|
+
params,
|
|
66
|
+
rowData,
|
|
67
|
+
rowDataKeys: rowData ? Object.keys(rowData) : 'no rowData'
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const processedParams = {};
|
|
71
|
+
|
|
72
|
+
for (const [key, value] of Object.entries(params)) {
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
// Handle arrays (like 'where' parameter)
|
|
75
|
+
processedParams[key] = value.map(item => {
|
|
76
|
+
if (typeof item === 'object' && item !== null) {
|
|
77
|
+
const processedItem = {};
|
|
78
|
+
for (const [itemKey, itemValue] of Object.entries(item)) {
|
|
79
|
+
if (typeof itemValue === 'string' && itemValue.startsWith('{row.') && itemValue.endsWith('}')) {
|
|
80
|
+
// Extract field name from {row.fieldName}
|
|
81
|
+
const fieldName = itemValue.slice(5, -1);
|
|
82
|
+
const actualValue = rowData?.[fieldName];
|
|
83
|
+
console.log(`Replacing ${itemValue} with field ${fieldName}:`, actualValue);
|
|
84
|
+
processedItem[itemKey] = actualValue !== undefined ? actualValue : itemValue;
|
|
85
|
+
} else {
|
|
86
|
+
processedItem[itemKey] = itemValue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return processedItem;
|
|
90
|
+
}
|
|
91
|
+
return item;
|
|
92
|
+
});
|
|
93
|
+
} else if (typeof value === 'string' && value.startsWith('{row.') && value.endsWith('}')) {
|
|
94
|
+
// Handle simple string substitution
|
|
95
|
+
const fieldName = value.slice(5, -1);
|
|
96
|
+
const actualValue = rowData?.[fieldName];
|
|
97
|
+
console.log(`Replacing ${value} with field ${fieldName}:`, actualValue);
|
|
98
|
+
processedParams[key] = actualValue !== undefined ? actualValue : value;
|
|
99
|
+
} else {
|
|
100
|
+
processedParams[key] = value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log('Processed params result:', processedParams);
|
|
105
|
+
return processedParams;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Fetch available contacts from API
|
|
109
|
+
const fetchContacts = async () => {
|
|
110
|
+
try {
|
|
111
|
+
// Process URL parameters with row context
|
|
112
|
+
const requestParams = processUrlParams(urlParams, rowContext);
|
|
113
|
+
|
|
114
|
+
console.log('Fetching contacts with params:', {
|
|
115
|
+
url: contactsUrl,
|
|
116
|
+
originalParams: urlParams,
|
|
117
|
+
processedParams: requestParams,
|
|
118
|
+
rowContext: rowContext
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const response = await CustomFetch(contactsUrl, 'POST', requestParams);
|
|
122
|
+
if (response.data && response.data.data) {
|
|
123
|
+
modalState.availableContacts = response.data.data.map(contact => ({
|
|
124
|
+
value: contact.id || contact.value,
|
|
125
|
+
label: contact.name || contact.label || contact.title,
|
|
126
|
+
...contact
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
console.log(`Fetched ${modalState.availableContacts.length} contacts`);
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.warn('Failed to fetch contacts:', error);
|
|
133
|
+
modalState.availableContacts = [];
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Generate HTML for the modal
|
|
138
|
+
const generateModalHTML = () => {
|
|
139
|
+
const contactsHtml = modalState.contacts.map((contact, index) => `
|
|
140
|
+
<div class="contact-item" data-index="${index}">
|
|
141
|
+
<div class="contact-info">
|
|
142
|
+
<strong>${contact.name || 'Unknown Contact'}</strong>
|
|
143
|
+
${contact.email ? `<br><small>📧 ${contact.email}</small>` : ''}
|
|
144
|
+
${contact.notes ? `<br><small>📝 ${contact.notes.length > 50 ? contact.notes.substring(0, 50) + '...' : contact.notes}</small>` : ''}
|
|
145
|
+
<span class="contact-type">${contact.type === 'contact' ? 'Existing Contact' : 'Manual Entry'}</span>
|
|
146
|
+
</div>
|
|
147
|
+
<button type="button" class="remove-contact-btn" data-index="${index}">×</button>
|
|
148
|
+
</div>
|
|
149
|
+
`).join('');
|
|
150
|
+
|
|
151
|
+
const availableContactsOptions = modalState.availableContacts.map(contact =>
|
|
152
|
+
`<option value="${contact.value}">${contact.label}</option>`
|
|
153
|
+
).join('');
|
|
154
|
+
|
|
155
|
+
const manualFieldsHtml = fields.map(field => {
|
|
156
|
+
const fieldType = fieldTypes[field] || (field === 'email' ? 'email' : 'text');
|
|
157
|
+
const fieldLabel = fieldLabels[field] || field.charAt(0).toUpperCase() + field.slice(1);
|
|
158
|
+
const fieldValidation = validation[field] || {};
|
|
159
|
+
const isRequired = fieldValidation.required !== false && (field === 'name' || field === 'email');
|
|
160
|
+
const maxLength = fieldValidation.maxLength || (fieldType === 'textarea' ? 500 : 255);
|
|
161
|
+
|
|
162
|
+
if (fieldType === 'textarea') {
|
|
163
|
+
return `
|
|
164
|
+
<div class="form-group">
|
|
165
|
+
<div class="form-field-row">
|
|
166
|
+
<label for="manual-${field}">${fieldLabel}:</label>
|
|
167
|
+
<textarea
|
|
168
|
+
id="manual-${field}"
|
|
169
|
+
class="swal2-textarea"
|
|
170
|
+
placeholder="Enter ${fieldLabel.toLowerCase()}"
|
|
171
|
+
rows="3"
|
|
172
|
+
maxlength="${maxLength}"
|
|
173
|
+
${isRequired ? 'required' : ''}
|
|
174
|
+
></textarea>
|
|
175
|
+
<small class="field-help">Max ${maxLength} characters</small>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
`;
|
|
179
|
+
} else {
|
|
180
|
+
return `
|
|
181
|
+
<div class="form-group">
|
|
182
|
+
<div class="form-field-row">
|
|
183
|
+
<label for="manual-${field}">${fieldLabel}:</label>
|
|
184
|
+
<input
|
|
185
|
+
type="${fieldType}"
|
|
186
|
+
id="manual-${field}"
|
|
187
|
+
class="swal2-input"
|
|
188
|
+
placeholder="Enter ${fieldLabel.toLowerCase()}"
|
|
189
|
+
maxlength="${maxLength}"
|
|
190
|
+
${isRequired ? 'required' : ''}
|
|
191
|
+
>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
}).join('');
|
|
197
|
+
|
|
198
|
+
return `
|
|
199
|
+
<div class="contact-selector-modal">
|
|
200
|
+
<div class="contact-modal-header">
|
|
201
|
+
<p class="contact-modal-message">${message}</p>
|
|
202
|
+
${data ? `<small class="field-info">Field: ${data.label || data.name}</small>` : ''}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div class="existing-contacts">
|
|
206
|
+
<h4>Selected Contacts (${modalState.contacts.length})</h4>
|
|
207
|
+
<div class="contacts-list">
|
|
208
|
+
${contactsHtml || '<p class="no-contacts">No contacts selected</p>'}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div class="add-contact-section">
|
|
213
|
+
<h4>Add Contact</h4>
|
|
214
|
+
|
|
215
|
+
${modalState.availableContacts.length > 0 ? `
|
|
216
|
+
<div class="existing-contact-selector">
|
|
217
|
+
<label for="existing-contact-select">Select Existing Contact:</label>
|
|
218
|
+
<div class="contact-select-row">
|
|
219
|
+
<select id="existing-contact-select" class="swal2-select">
|
|
220
|
+
<option value="">Choose a contact...</option>
|
|
221
|
+
${availableContactsOptions}
|
|
222
|
+
</select>
|
|
223
|
+
<button type="button" id="add-existing-contact" class="add-contact-btn">Add</button>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
` : ''}
|
|
227
|
+
|
|
228
|
+
${allowManualEntry ? `
|
|
229
|
+
<div class="manual-contact-section">
|
|
230
|
+
<div class="manual-contact-toggle">
|
|
231
|
+
<button type="button" id="toggle-manual-entry" class="toggle-manual-btn">
|
|
232
|
+
${modalState.isAddingManual ? 'Cancel Manual Entry' : 'Add Manual Contact'}
|
|
233
|
+
</button>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div id="manual-contact-form" class="manual-contact-form" style="display: ${modalState.isAddingManual ? 'block' : 'none'}">
|
|
237
|
+
${manualFieldsHtml}
|
|
238
|
+
<button type="button" id="add-manual-contact" class="add-contact-btn">Add Manual Contact</button>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
` : ''}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
${multiple && maxContacts > 1 ? `
|
|
245
|
+
<div class="contact-limits">
|
|
246
|
+
<small>Maximum ${maxContacts} contacts allowed</small>
|
|
247
|
+
</div>
|
|
248
|
+
` : ''}
|
|
249
|
+
</div>
|
|
250
|
+
`;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Event handlers
|
|
254
|
+
const setupEventHandlers = () => {
|
|
255
|
+
// Remove contact handler
|
|
256
|
+
document.addEventListener('click', (e) => {
|
|
257
|
+
if (e.target.classList.contains('remove-contact-btn')) {
|
|
258
|
+
const index = parseInt(e.target.dataset.index);
|
|
259
|
+
modalState.contacts.splice(index, 1);
|
|
260
|
+
updateModalContent();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Add existing contact handler
|
|
265
|
+
const addExistingBtn = document.getElementById('add-existing-contact');
|
|
266
|
+
if (addExistingBtn) {
|
|
267
|
+
addExistingBtn.addEventListener('click', () => {
|
|
268
|
+
const select = document.getElementById('existing-contact-select');
|
|
269
|
+
const selectedValue = select.value;
|
|
270
|
+
|
|
271
|
+
if (selectedValue) {
|
|
272
|
+
const selectedContact = modalState.availableContacts.find(c => c.value == selectedValue);
|
|
273
|
+
if (selectedContact && !modalState.contacts.find(c => c.contact_id == selectedValue)) {
|
|
274
|
+
modalState.contacts.push({
|
|
275
|
+
type: 'contact',
|
|
276
|
+
contact_id: selectedContact.value,
|
|
277
|
+
name: selectedContact.label,
|
|
278
|
+
email: selectedContact.email || ''
|
|
279
|
+
});
|
|
280
|
+
select.value = '';
|
|
281
|
+
updateModalContent();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Toggle manual entry
|
|
288
|
+
const toggleBtn = document.getElementById('toggle-manual-entry');
|
|
289
|
+
if (toggleBtn) {
|
|
290
|
+
toggleBtn.addEventListener('click', () => {
|
|
291
|
+
modalState.isAddingManual = !modalState.isAddingManual;
|
|
292
|
+
updateModalContent();
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Add manual contact handler
|
|
297
|
+
const addManualBtn = document.getElementById('add-manual-contact');
|
|
298
|
+
if (addManualBtn) {
|
|
299
|
+
addManualBtn.addEventListener('click', () => {
|
|
300
|
+
const newContact = { type: 'manual' };
|
|
301
|
+
let isValid = true;
|
|
302
|
+
|
|
303
|
+
fields.forEach(field => {
|
|
304
|
+
const input = document.getElementById(`manual-${field}`);
|
|
305
|
+
const value = input ? input.value.trim() : '';
|
|
306
|
+
const fieldValidation = validation[field] || {};
|
|
307
|
+
const isRequired = fieldValidation.required !== false && (field === 'name' || field === 'email');
|
|
308
|
+
|
|
309
|
+
// Validate email format
|
|
310
|
+
if (field === 'email' && value && !isValidEmail(value)) {
|
|
311
|
+
isValid = false;
|
|
312
|
+
input.style.borderColor = '#dc2626';
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Validate required fields
|
|
317
|
+
if (isRequired && !value) {
|
|
318
|
+
isValid = false;
|
|
319
|
+
input.style.borderColor = '#dc2626';
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Validate max length
|
|
324
|
+
if (fieldValidation.maxLength && value.length > fieldValidation.maxLength) {
|
|
325
|
+
isValid = false;
|
|
326
|
+
input.style.borderColor = '#dc2626';
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
newContact[field] = value;
|
|
331
|
+
if (input) input.style.borderColor = '';
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (isValid && newContact.name) {
|
|
335
|
+
modalState.contacts.push(newContact);
|
|
336
|
+
|
|
337
|
+
// Clear form
|
|
338
|
+
fields.forEach(field => {
|
|
339
|
+
const input = document.getElementById(`manual-${field}`);
|
|
340
|
+
if (input) input.value = '';
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
modalState.isAddingManual = false;
|
|
344
|
+
updateModalContent();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Update modal content
|
|
351
|
+
const updateModalContent = () => {
|
|
352
|
+
const container = document.querySelector('.contact-selector-modal');
|
|
353
|
+
if (container) {
|
|
354
|
+
container.innerHTML = generateModalHTML().match(/<div class="contact-selector-modal">([\s\S]*)<\/div>/)[1];
|
|
355
|
+
setupEventHandlers();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Email validation helper
|
|
360
|
+
const isValidEmail = (email) => {
|
|
361
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Validation function
|
|
365
|
+
const validateContacts = () => {
|
|
366
|
+
if (!multiple && modalState.contacts.length > 1) {
|
|
367
|
+
Swal.showValidationMessage('Only one contact is allowed for this field');
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (modalState.contacts.length > maxContacts) {
|
|
372
|
+
Swal.showValidationMessage(`Maximum ${maxContacts} contacts allowed`);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return true;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Initialize and show modal
|
|
380
|
+
const showModal = async () => {
|
|
381
|
+
await fetchContacts();
|
|
382
|
+
|
|
383
|
+
const swalOptions = {
|
|
384
|
+
title: title,
|
|
385
|
+
html: generateModalHTML(),
|
|
386
|
+
showCancelButton: true,
|
|
387
|
+
confirmButtonText: confirmLabel,
|
|
388
|
+
cancelButtonText: cancelLabel,
|
|
389
|
+
confirmButtonColor: 'var(--primary-color, #4f46e5)',
|
|
390
|
+
cancelButtonColor: '#6b7280',
|
|
391
|
+
allowOutsideClick: false,
|
|
392
|
+
allowEscapeKey: true,
|
|
393
|
+
width: '600px',
|
|
394
|
+
padding: '1.25rem',
|
|
395
|
+
customClass: {
|
|
396
|
+
popup: 'contact-selector-dialog',
|
|
397
|
+
htmlContainer: 'contact-selector-content'
|
|
398
|
+
},
|
|
399
|
+
preConfirm: () => {
|
|
400
|
+
if (!validateContacts()) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
return modalState.contacts;
|
|
404
|
+
},
|
|
405
|
+
didOpen: () => {
|
|
406
|
+
setupEventHandlers();
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return Swal.fire(swalOptions).then((result) => {
|
|
411
|
+
if (result.isConfirmed && result.value) {
|
|
412
|
+
console.log('Contacts selected:', result.value);
|
|
413
|
+
onConfirm(result.value);
|
|
414
|
+
} else if (result.dismiss === Swal.DismissReason.cancel) {
|
|
415
|
+
onCancel();
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
return showModal();
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
export default showContactSelectorModal;
|