@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
@@ -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
- LinkOut,
5
+ ExternalLink,
6
6
  Plus,
7
7
  Search,
8
- Sort,
9
- } from 'akar-icons';
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
- <LinkOut
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
- <Sort
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;